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, NavigationEntry, OpenOptions, ViewId,
59 item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
60};
61
62#[gpui::test]
63fn test_edit_events(cx: &mut TestAppContext) {
64 init_test(cx, |_| {});
65
66 let buffer = cx.new(|cx| {
67 let mut buffer = language::Buffer::local("123456", cx);
68 buffer.set_group_interval(Duration::from_secs(1));
69 buffer
70 });
71
72 let events = Rc::new(RefCell::new(Vec::new()));
73 let editor1 = cx.add_window({
74 let events = events.clone();
75 |window, cx| {
76 let entity = cx.entity().clone();
77 cx.subscribe_in(
78 &entity,
79 window,
80 move |_, _, event: &EditorEvent, _, _| match event {
81 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
82 EditorEvent::BufferEdited => {
83 events.borrow_mut().push(("editor1", "buffer edited"))
84 }
85 _ => {}
86 },
87 )
88 .detach();
89 Editor::for_buffer(buffer.clone(), None, window, cx)
90 }
91 });
92
93 let editor2 = cx.add_window({
94 let events = events.clone();
95 |window, cx| {
96 cx.subscribe_in(
97 &cx.entity().clone(),
98 window,
99 move |_, _, event: &EditorEvent, _, _| match event {
100 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
101 EditorEvent::BufferEdited => {
102 events.borrow_mut().push(("editor2", "buffer edited"))
103 }
104 _ => {}
105 },
106 )
107 .detach();
108 Editor::for_buffer(buffer.clone(), None, window, cx)
109 }
110 });
111
112 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
113
114 // Mutating editor 1 will emit an `Edited` event only for that editor.
115 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
116 assert_eq!(
117 mem::take(&mut *events.borrow_mut()),
118 [
119 ("editor1", "edited"),
120 ("editor1", "buffer edited"),
121 ("editor2", "buffer edited"),
122 ]
123 );
124
125 // Mutating editor 2 will emit an `Edited` event only for that editor.
126 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
127 assert_eq!(
128 mem::take(&mut *events.borrow_mut()),
129 [
130 ("editor2", "edited"),
131 ("editor1", "buffer edited"),
132 ("editor2", "buffer edited"),
133 ]
134 );
135
136 // Undoing on editor 1 will emit an `Edited` event only for that editor.
137 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
138 assert_eq!(
139 mem::take(&mut *events.borrow_mut()),
140 [
141 ("editor1", "edited"),
142 ("editor1", "buffer edited"),
143 ("editor2", "buffer edited"),
144 ]
145 );
146
147 // Redoing on editor 1 will emit an `Edited` event only for that editor.
148 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
149 assert_eq!(
150 mem::take(&mut *events.borrow_mut()),
151 [
152 ("editor1", "edited"),
153 ("editor1", "buffer edited"),
154 ("editor2", "buffer edited"),
155 ]
156 );
157
158 // Undoing on editor 2 will emit an `Edited` event only for that editor.
159 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
160 assert_eq!(
161 mem::take(&mut *events.borrow_mut()),
162 [
163 ("editor2", "edited"),
164 ("editor1", "buffer edited"),
165 ("editor2", "buffer edited"),
166 ]
167 );
168
169 // Redoing on editor 2 will emit an `Edited` event only for that editor.
170 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
171 assert_eq!(
172 mem::take(&mut *events.borrow_mut()),
173 [
174 ("editor2", "edited"),
175 ("editor1", "buffer edited"),
176 ("editor2", "buffer edited"),
177 ]
178 );
179
180 // No event is emitted when the mutation is a no-op.
181 _ = editor2.update(cx, |editor, window, cx| {
182 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
183
184 editor.backspace(&Backspace, window, cx);
185 });
186 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
187}
188
189#[gpui::test]
190fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
191 init_test(cx, |_| {});
192
193 let mut now = Instant::now();
194 let group_interval = Duration::from_millis(1);
195 let buffer = cx.new(|cx| {
196 let mut buf = language::Buffer::local("123456", cx);
197 buf.set_group_interval(group_interval);
198 buf
199 });
200 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
201 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
202
203 _ = editor.update(cx, |editor, window, cx| {
204 editor.start_transaction_at(now, window, cx);
205 editor.change_selections(None, window, cx, |s| s.select_ranges([2..4]));
206
207 editor.insert("cd", window, cx);
208 editor.end_transaction_at(now, cx);
209 assert_eq!(editor.text(cx), "12cd56");
210 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
211
212 editor.start_transaction_at(now, window, cx);
213 editor.change_selections(None, window, cx, |s| s.select_ranges([4..5]));
214 editor.insert("e", window, cx);
215 editor.end_transaction_at(now, cx);
216 assert_eq!(editor.text(cx), "12cde6");
217 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
218
219 now += group_interval + Duration::from_millis(1);
220 editor.change_selections(None, window, cx, |s| s.select_ranges([2..2]));
221
222 // Simulate an edit in another editor
223 buffer.update(cx, |buffer, cx| {
224 buffer.start_transaction_at(now, cx);
225 buffer.edit([(0..1, "a")], None, cx);
226 buffer.edit([(1..1, "b")], None, cx);
227 buffer.end_transaction_at(now, cx);
228 });
229
230 assert_eq!(editor.text(cx), "ab2cde6");
231 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
232
233 // Last transaction happened past the group interval in a different editor.
234 // Undo it individually and don't restore selections.
235 editor.undo(&Undo, window, cx);
236 assert_eq!(editor.text(cx), "12cde6");
237 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
238
239 // First two transactions happened within the group interval in this editor.
240 // Undo them together and restore selections.
241 editor.undo(&Undo, window, cx);
242 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
243 assert_eq!(editor.text(cx), "123456");
244 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
245
246 // Redo the first two transactions together.
247 editor.redo(&Redo, window, cx);
248 assert_eq!(editor.text(cx), "12cde6");
249 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
250
251 // Redo the last transaction on its own.
252 editor.redo(&Redo, window, cx);
253 assert_eq!(editor.text(cx), "ab2cde6");
254 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
255
256 // Test empty transactions.
257 editor.start_transaction_at(now, window, cx);
258 editor.end_transaction_at(now, cx);
259 editor.undo(&Undo, window, cx);
260 assert_eq!(editor.text(cx), "12cde6");
261 });
262}
263
264#[gpui::test]
265fn test_ime_composition(cx: &mut TestAppContext) {
266 init_test(cx, |_| {});
267
268 let buffer = cx.new(|cx| {
269 let mut buffer = language::Buffer::local("abcde", cx);
270 // Ensure automatic grouping doesn't occur.
271 buffer.set_group_interval(Duration::ZERO);
272 buffer
273 });
274
275 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
276 cx.add_window(|window, cx| {
277 let mut editor = build_editor(buffer.clone(), window, cx);
278
279 // Start a new IME composition.
280 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
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 assert_eq!(editor.text(cx), "äbcde");
284 assert_eq!(
285 editor.marked_text_ranges(cx),
286 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
287 );
288
289 // Finalize IME composition.
290 editor.replace_text_in_range(None, "ā", window, cx);
291 assert_eq!(editor.text(cx), "ābcde");
292 assert_eq!(editor.marked_text_ranges(cx), None);
293
294 // IME composition edits are grouped and are undone/redone at once.
295 editor.undo(&Default::default(), window, cx);
296 assert_eq!(editor.text(cx), "abcde");
297 assert_eq!(editor.marked_text_ranges(cx), None);
298 editor.redo(&Default::default(), window, cx);
299 assert_eq!(editor.text(cx), "ābcde");
300 assert_eq!(editor.marked_text_ranges(cx), None);
301
302 // Start a new IME composition.
303 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
304 assert_eq!(
305 editor.marked_text_ranges(cx),
306 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
307 );
308
309 // Undoing during an IME composition cancels it.
310 editor.undo(&Default::default(), window, cx);
311 assert_eq!(editor.text(cx), "ābcde");
312 assert_eq!(editor.marked_text_ranges(cx), None);
313
314 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
315 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
316 assert_eq!(editor.text(cx), "ābcdè");
317 assert_eq!(
318 editor.marked_text_ranges(cx),
319 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
320 );
321
322 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
323 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
324 assert_eq!(editor.text(cx), "ābcdę");
325 assert_eq!(editor.marked_text_ranges(cx), None);
326
327 // Start a new IME composition with multiple cursors.
328 editor.change_selections(None, window, cx, |s| {
329 s.select_ranges([
330 OffsetUtf16(1)..OffsetUtf16(1),
331 OffsetUtf16(3)..OffsetUtf16(3),
332 OffsetUtf16(5)..OffsetUtf16(5),
333 ])
334 });
335 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
336 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
337 assert_eq!(
338 editor.marked_text_ranges(cx),
339 Some(vec![
340 OffsetUtf16(0)..OffsetUtf16(3),
341 OffsetUtf16(4)..OffsetUtf16(7),
342 OffsetUtf16(8)..OffsetUtf16(11)
343 ])
344 );
345
346 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
347 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
348 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
349 assert_eq!(
350 editor.marked_text_ranges(cx),
351 Some(vec![
352 OffsetUtf16(1)..OffsetUtf16(2),
353 OffsetUtf16(5)..OffsetUtf16(6),
354 OffsetUtf16(9)..OffsetUtf16(10)
355 ])
356 );
357
358 // Finalize IME composition with multiple cursors.
359 editor.replace_text_in_range(Some(9..10), "2", window, cx);
360 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
361 assert_eq!(editor.marked_text_ranges(cx), None);
362
363 editor
364 });
365}
366
367#[gpui::test]
368fn test_selection_with_mouse(cx: &mut TestAppContext) {
369 init_test(cx, |_| {});
370
371 let editor = cx.add_window(|window, cx| {
372 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
373 build_editor(buffer, window, cx)
374 });
375
376 _ = editor.update(cx, |editor, window, cx| {
377 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
378 });
379 assert_eq!(
380 editor
381 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
382 .unwrap(),
383 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
384 );
385
386 _ = editor.update(cx, |editor, window, cx| {
387 editor.update_selection(
388 DisplayPoint::new(DisplayRow(3), 3),
389 0,
390 gpui::Point::<f32>::default(),
391 window,
392 cx,
393 );
394 });
395
396 assert_eq!(
397 editor
398 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
399 .unwrap(),
400 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
401 );
402
403 _ = editor.update(cx, |editor, window, cx| {
404 editor.update_selection(
405 DisplayPoint::new(DisplayRow(1), 1),
406 0,
407 gpui::Point::<f32>::default(),
408 window,
409 cx,
410 );
411 });
412
413 assert_eq!(
414 editor
415 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
416 .unwrap(),
417 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
418 );
419
420 _ = editor.update(cx, |editor, window, cx| {
421 editor.end_selection(window, cx);
422 editor.update_selection(
423 DisplayPoint::new(DisplayRow(3), 3),
424 0,
425 gpui::Point::<f32>::default(),
426 window,
427 cx,
428 );
429 });
430
431 assert_eq!(
432 editor
433 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
434 .unwrap(),
435 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
436 );
437
438 _ = editor.update(cx, |editor, window, cx| {
439 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
440 editor.update_selection(
441 DisplayPoint::new(DisplayRow(0), 0),
442 0,
443 gpui::Point::<f32>::default(),
444 window,
445 cx,
446 );
447 });
448
449 assert_eq!(
450 editor
451 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
452 .unwrap(),
453 [
454 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
455 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
456 ]
457 );
458
459 _ = editor.update(cx, |editor, window, cx| {
460 editor.end_selection(window, cx);
461 });
462
463 assert_eq!(
464 editor
465 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
466 .unwrap(),
467 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
468 );
469}
470
471#[gpui::test]
472fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
473 init_test(cx, |_| {});
474
475 let editor = cx.add_window(|window, cx| {
476 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
477 build_editor(buffer, window, cx)
478 });
479
480 _ = editor.update(cx, |editor, window, cx| {
481 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
482 });
483
484 _ = editor.update(cx, |editor, window, cx| {
485 editor.end_selection(window, cx);
486 });
487
488 _ = editor.update(cx, |editor, window, cx| {
489 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
490 });
491
492 _ = editor.update(cx, |editor, window, cx| {
493 editor.end_selection(window, cx);
494 });
495
496 assert_eq!(
497 editor
498 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
499 .unwrap(),
500 [
501 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
502 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
503 ]
504 );
505
506 _ = editor.update(cx, |editor, window, cx| {
507 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
508 });
509
510 _ = editor.update(cx, |editor, window, cx| {
511 editor.end_selection(window, cx);
512 });
513
514 assert_eq!(
515 editor
516 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
517 .unwrap(),
518 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
519 );
520}
521
522#[gpui::test]
523fn test_canceling_pending_selection(cx: &mut TestAppContext) {
524 init_test(cx, |_| {});
525
526 let editor = cx.add_window(|window, cx| {
527 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
528 build_editor(buffer, window, cx)
529 });
530
531 _ = editor.update(cx, |editor, window, cx| {
532 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
533 assert_eq!(
534 editor.selections.display_ranges(cx),
535 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
536 );
537 });
538
539 _ = editor.update(cx, |editor, window, cx| {
540 editor.update_selection(
541 DisplayPoint::new(DisplayRow(3), 3),
542 0,
543 gpui::Point::<f32>::default(),
544 window,
545 cx,
546 );
547 assert_eq!(
548 editor.selections.display_ranges(cx),
549 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
550 );
551 });
552
553 _ = editor.update(cx, |editor, window, cx| {
554 editor.cancel(&Cancel, window, cx);
555 editor.update_selection(
556 DisplayPoint::new(DisplayRow(1), 1),
557 0,
558 gpui::Point::<f32>::default(),
559 window,
560 cx,
561 );
562 assert_eq!(
563 editor.selections.display_ranges(cx),
564 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
565 );
566 });
567}
568
569#[gpui::test]
570fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
571 init_test(cx, |_| {});
572
573 let editor = cx.add_window(|window, cx| {
574 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
575 build_editor(buffer, window, cx)
576 });
577
578 _ = editor.update(cx, |editor, window, cx| {
579 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
580 assert_eq!(
581 editor.selections.display_ranges(cx),
582 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
583 );
584
585 editor.move_down(&Default::default(), window, cx);
586 assert_eq!(
587 editor.selections.display_ranges(cx),
588 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
589 );
590
591 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
592 assert_eq!(
593 editor.selections.display_ranges(cx),
594 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
595 );
596
597 editor.move_up(&Default::default(), window, cx);
598 assert_eq!(
599 editor.selections.display_ranges(cx),
600 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
601 );
602 });
603}
604
605#[gpui::test]
606fn test_clone(cx: &mut TestAppContext) {
607 init_test(cx, |_| {});
608
609 let (text, selection_ranges) = marked_text_ranges(
610 indoc! {"
611 one
612 two
613 threeˇ
614 four
615 fiveˇ
616 "},
617 true,
618 );
619
620 let editor = cx.add_window(|window, cx| {
621 let buffer = MultiBuffer::build_simple(&text, cx);
622 build_editor(buffer, window, cx)
623 });
624
625 _ = editor.update(cx, |editor, window, cx| {
626 editor.change_selections(None, window, cx, |s| {
627 s.select_ranges(selection_ranges.clone())
628 });
629 editor.fold_creases(
630 vec![
631 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
632 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
633 ],
634 true,
635 window,
636 cx,
637 );
638 });
639
640 let cloned_editor = editor
641 .update(cx, |editor, _, cx| {
642 cx.open_window(Default::default(), |window, cx| {
643 cx.new(|cx| editor.clone(window, cx))
644 })
645 })
646 .unwrap()
647 .unwrap();
648
649 let snapshot = editor
650 .update(cx, |e, window, cx| e.snapshot(window, cx))
651 .unwrap();
652 let cloned_snapshot = cloned_editor
653 .update(cx, |e, window, cx| e.snapshot(window, cx))
654 .unwrap();
655
656 assert_eq!(
657 cloned_editor
658 .update(cx, |e, _, cx| e.display_text(cx))
659 .unwrap(),
660 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
661 );
662 assert_eq!(
663 cloned_snapshot
664 .folds_in_range(0..text.len())
665 .collect::<Vec<_>>(),
666 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
667 );
668 assert_set_eq!(
669 cloned_editor
670 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
671 .unwrap(),
672 editor
673 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
674 .unwrap()
675 );
676 assert_set_eq!(
677 cloned_editor
678 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
679 .unwrap(),
680 editor
681 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
682 .unwrap()
683 );
684}
685
686#[gpui::test]
687async fn test_navigation_history(cx: &mut TestAppContext) {
688 init_test(cx, |_| {});
689
690 use workspace::item::Item;
691
692 let fs = FakeFs::new(cx.executor());
693 let project = Project::test(fs, [], cx).await;
694 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
695 let pane = workspace
696 .update(cx, |workspace, _, _| workspace.active_pane().clone())
697 .unwrap();
698
699 _ = workspace.update(cx, |_v, window, cx| {
700 cx.new(|cx| {
701 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
702 let mut editor = build_editor(buffer.clone(), window, cx);
703 let handle = cx.entity();
704 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
705
706 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
707 editor.nav_history.as_mut().unwrap().pop_backward(cx)
708 }
709
710 // Move the cursor a small distance.
711 // Nothing is added to the navigation history.
712 editor.change_selections(None, window, cx, |s| {
713 s.select_display_ranges([
714 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
715 ])
716 });
717 editor.change_selections(None, window, cx, |s| {
718 s.select_display_ranges([
719 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
720 ])
721 });
722 assert!(pop_history(&mut editor, cx).is_none());
723
724 // Move the cursor a large distance.
725 // The history can jump back to the previous position.
726 editor.change_selections(None, window, cx, |s| {
727 s.select_display_ranges([
728 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
729 ])
730 });
731 let nav_entry = pop_history(&mut editor, cx).unwrap();
732 editor.navigate(nav_entry.data.unwrap(), window, cx);
733 assert_eq!(nav_entry.item.id(), cx.entity_id());
734 assert_eq!(
735 editor.selections.display_ranges(cx),
736 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
737 );
738 assert!(pop_history(&mut editor, cx).is_none());
739
740 // Move the cursor a small distance via the mouse.
741 // Nothing is added to the navigation history.
742 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
743 editor.end_selection(window, cx);
744 assert_eq!(
745 editor.selections.display_ranges(cx),
746 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
747 );
748 assert!(pop_history(&mut editor, cx).is_none());
749
750 // Move the cursor a large distance via the mouse.
751 // The history can jump back to the previous position.
752 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
753 editor.end_selection(window, cx);
754 assert_eq!(
755 editor.selections.display_ranges(cx),
756 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
757 );
758 let nav_entry = pop_history(&mut editor, cx).unwrap();
759 editor.navigate(nav_entry.data.unwrap(), window, cx);
760 assert_eq!(nav_entry.item.id(), cx.entity_id());
761 assert_eq!(
762 editor.selections.display_ranges(cx),
763 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
764 );
765 assert!(pop_history(&mut editor, cx).is_none());
766
767 // Set scroll position to check later
768 editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), window, cx);
769 let original_scroll_position = editor.scroll_manager.anchor();
770
771 // Jump to the end of the document and adjust scroll
772 editor.move_to_end(&MoveToEnd, window, cx);
773 editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), window, cx);
774 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
775
776 let nav_entry = pop_history(&mut editor, cx).unwrap();
777 editor.navigate(nav_entry.data.unwrap(), window, cx);
778 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
779
780 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
781 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
782 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
783 let invalid_point = Point::new(9999, 0);
784 editor.navigate(
785 Box::new(NavigationData {
786 cursor_anchor: invalid_anchor,
787 cursor_position: invalid_point,
788 scroll_anchor: ScrollAnchor {
789 anchor: invalid_anchor,
790 offset: Default::default(),
791 },
792 scroll_top_row: invalid_point.row,
793 }),
794 window,
795 cx,
796 );
797 assert_eq!(
798 editor.selections.display_ranges(cx),
799 &[editor.max_point(cx)..editor.max_point(cx)]
800 );
801 assert_eq!(
802 editor.scroll_position(cx),
803 gpui::Point::new(0., editor.max_point(cx).row().as_f32())
804 );
805
806 editor
807 })
808 });
809}
810
811#[gpui::test]
812fn test_cancel(cx: &mut TestAppContext) {
813 init_test(cx, |_| {});
814
815 let editor = cx.add_window(|window, cx| {
816 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
817 build_editor(buffer, window, cx)
818 });
819
820 _ = editor.update(cx, |editor, window, cx| {
821 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
822 editor.update_selection(
823 DisplayPoint::new(DisplayRow(1), 1),
824 0,
825 gpui::Point::<f32>::default(),
826 window,
827 cx,
828 );
829 editor.end_selection(window, cx);
830
831 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
832 editor.update_selection(
833 DisplayPoint::new(DisplayRow(0), 3),
834 0,
835 gpui::Point::<f32>::default(),
836 window,
837 cx,
838 );
839 editor.end_selection(window, cx);
840 assert_eq!(
841 editor.selections.display_ranges(cx),
842 [
843 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
844 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
845 ]
846 );
847 });
848
849 _ = editor.update(cx, |editor, window, cx| {
850 editor.cancel(&Cancel, window, cx);
851 assert_eq!(
852 editor.selections.display_ranges(cx),
853 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
854 );
855 });
856
857 _ = editor.update(cx, |editor, window, cx| {
858 editor.cancel(&Cancel, window, cx);
859 assert_eq!(
860 editor.selections.display_ranges(cx),
861 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
862 );
863 });
864}
865
866#[gpui::test]
867fn test_fold_action(cx: &mut TestAppContext) {
868 init_test(cx, |_| {});
869
870 let editor = cx.add_window(|window, cx| {
871 let buffer = MultiBuffer::build_simple(
872 &"
873 impl Foo {
874 // Hello!
875
876 fn a() {
877 1
878 }
879
880 fn b() {
881 2
882 }
883
884 fn c() {
885 3
886 }
887 }
888 "
889 .unindent(),
890 cx,
891 );
892 build_editor(buffer.clone(), window, cx)
893 });
894
895 _ = editor.update(cx, |editor, window, cx| {
896 editor.change_selections(None, window, cx, |s| {
897 s.select_display_ranges([
898 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
899 ]);
900 });
901 editor.fold(&Fold, window, cx);
902 assert_eq!(
903 editor.display_text(cx),
904 "
905 impl Foo {
906 // Hello!
907
908 fn a() {
909 1
910 }
911
912 fn b() {⋯
913 }
914
915 fn c() {⋯
916 }
917 }
918 "
919 .unindent(),
920 );
921
922 editor.fold(&Fold, window, cx);
923 assert_eq!(
924 editor.display_text(cx),
925 "
926 impl Foo {⋯
927 }
928 "
929 .unindent(),
930 );
931
932 editor.unfold_lines(&UnfoldLines, window, cx);
933 assert_eq!(
934 editor.display_text(cx),
935 "
936 impl Foo {
937 // Hello!
938
939 fn a() {
940 1
941 }
942
943 fn b() {⋯
944 }
945
946 fn c() {⋯
947 }
948 }
949 "
950 .unindent(),
951 );
952
953 editor.unfold_lines(&UnfoldLines, window, cx);
954 assert_eq!(
955 editor.display_text(cx),
956 editor.buffer.read(cx).read(cx).text()
957 );
958 });
959}
960
961#[gpui::test]
962fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
963 init_test(cx, |_| {});
964
965 let editor = cx.add_window(|window, cx| {
966 let buffer = MultiBuffer::build_simple(
967 &"
968 class Foo:
969 # Hello!
970
971 def a():
972 print(1)
973
974 def b():
975 print(2)
976
977 def c():
978 print(3)
979 "
980 .unindent(),
981 cx,
982 );
983 build_editor(buffer.clone(), window, cx)
984 });
985
986 _ = editor.update(cx, |editor, window, cx| {
987 editor.change_selections(None, window, cx, |s| {
988 s.select_display_ranges([
989 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
990 ]);
991 });
992 editor.fold(&Fold, window, cx);
993 assert_eq!(
994 editor.display_text(cx),
995 "
996 class Foo:
997 # Hello!
998
999 def a():
1000 print(1)
1001
1002 def b():⋯
1003
1004 def c():⋯
1005 "
1006 .unindent(),
1007 );
1008
1009 editor.fold(&Fold, window, cx);
1010 assert_eq!(
1011 editor.display_text(cx),
1012 "
1013 class Foo:⋯
1014 "
1015 .unindent(),
1016 );
1017
1018 editor.unfold_lines(&UnfoldLines, window, cx);
1019 assert_eq!(
1020 editor.display_text(cx),
1021 "
1022 class Foo:
1023 # Hello!
1024
1025 def a():
1026 print(1)
1027
1028 def b():⋯
1029
1030 def c():⋯
1031 "
1032 .unindent(),
1033 );
1034
1035 editor.unfold_lines(&UnfoldLines, window, cx);
1036 assert_eq!(
1037 editor.display_text(cx),
1038 editor.buffer.read(cx).read(cx).text()
1039 );
1040 });
1041}
1042
1043#[gpui::test]
1044fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1045 init_test(cx, |_| {});
1046
1047 let editor = cx.add_window(|window, cx| {
1048 let buffer = MultiBuffer::build_simple(
1049 &"
1050 class Foo:
1051 # Hello!
1052
1053 def a():
1054 print(1)
1055
1056 def b():
1057 print(2)
1058
1059
1060 def c():
1061 print(3)
1062
1063
1064 "
1065 .unindent(),
1066 cx,
1067 );
1068 build_editor(buffer.clone(), window, cx)
1069 });
1070
1071 _ = editor.update(cx, |editor, window, cx| {
1072 editor.change_selections(None, window, cx, |s| {
1073 s.select_display_ranges([
1074 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1075 ]);
1076 });
1077 editor.fold(&Fold, window, cx);
1078 assert_eq!(
1079 editor.display_text(cx),
1080 "
1081 class Foo:
1082 # Hello!
1083
1084 def a():
1085 print(1)
1086
1087 def b():⋯
1088
1089
1090 def c():⋯
1091
1092
1093 "
1094 .unindent(),
1095 );
1096
1097 editor.fold(&Fold, window, cx);
1098 assert_eq!(
1099 editor.display_text(cx),
1100 "
1101 class Foo:⋯
1102
1103
1104 "
1105 .unindent(),
1106 );
1107
1108 editor.unfold_lines(&UnfoldLines, window, cx);
1109 assert_eq!(
1110 editor.display_text(cx),
1111 "
1112 class Foo:
1113 # Hello!
1114
1115 def a():
1116 print(1)
1117
1118 def b():⋯
1119
1120
1121 def c():⋯
1122
1123
1124 "
1125 .unindent(),
1126 );
1127
1128 editor.unfold_lines(&UnfoldLines, window, cx);
1129 assert_eq!(
1130 editor.display_text(cx),
1131 editor.buffer.read(cx).read(cx).text()
1132 );
1133 });
1134}
1135
1136#[gpui::test]
1137fn test_fold_at_level(cx: &mut TestAppContext) {
1138 init_test(cx, |_| {});
1139
1140 let editor = cx.add_window(|window, cx| {
1141 let buffer = MultiBuffer::build_simple(
1142 &"
1143 class Foo:
1144 # Hello!
1145
1146 def a():
1147 print(1)
1148
1149 def b():
1150 print(2)
1151
1152
1153 class Bar:
1154 # World!
1155
1156 def a():
1157 print(1)
1158
1159 def b():
1160 print(2)
1161
1162
1163 "
1164 .unindent(),
1165 cx,
1166 );
1167 build_editor(buffer.clone(), window, cx)
1168 });
1169
1170 _ = editor.update(cx, |editor, window, cx| {
1171 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1172 assert_eq!(
1173 editor.display_text(cx),
1174 "
1175 class Foo:
1176 # Hello!
1177
1178 def a():⋯
1179
1180 def b():⋯
1181
1182
1183 class Bar:
1184 # World!
1185
1186 def a():⋯
1187
1188 def b():⋯
1189
1190
1191 "
1192 .unindent(),
1193 );
1194
1195 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1196 assert_eq!(
1197 editor.display_text(cx),
1198 "
1199 class Foo:⋯
1200
1201
1202 class Bar:⋯
1203
1204
1205 "
1206 .unindent(),
1207 );
1208
1209 editor.unfold_all(&UnfoldAll, window, cx);
1210 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1211 assert_eq!(
1212 editor.display_text(cx),
1213 "
1214 class Foo:
1215 # Hello!
1216
1217 def a():
1218 print(1)
1219
1220 def b():
1221 print(2)
1222
1223
1224 class Bar:
1225 # World!
1226
1227 def a():
1228 print(1)
1229
1230 def b():
1231 print(2)
1232
1233
1234 "
1235 .unindent(),
1236 );
1237
1238 assert_eq!(
1239 editor.display_text(cx),
1240 editor.buffer.read(cx).read(cx).text()
1241 );
1242 });
1243}
1244
1245#[gpui::test]
1246fn test_move_cursor(cx: &mut TestAppContext) {
1247 init_test(cx, |_| {});
1248
1249 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1250 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1251
1252 buffer.update(cx, |buffer, cx| {
1253 buffer.edit(
1254 vec![
1255 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1256 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1257 ],
1258 None,
1259 cx,
1260 );
1261 });
1262 _ = editor.update(cx, |editor, window, cx| {
1263 assert_eq!(
1264 editor.selections.display_ranges(cx),
1265 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1266 );
1267
1268 editor.move_down(&MoveDown, window, cx);
1269 assert_eq!(
1270 editor.selections.display_ranges(cx),
1271 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1272 );
1273
1274 editor.move_right(&MoveRight, window, cx);
1275 assert_eq!(
1276 editor.selections.display_ranges(cx),
1277 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1278 );
1279
1280 editor.move_left(&MoveLeft, window, cx);
1281 assert_eq!(
1282 editor.selections.display_ranges(cx),
1283 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1284 );
1285
1286 editor.move_up(&MoveUp, window, cx);
1287 assert_eq!(
1288 editor.selections.display_ranges(cx),
1289 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1290 );
1291
1292 editor.move_to_end(&MoveToEnd, window, cx);
1293 assert_eq!(
1294 editor.selections.display_ranges(cx),
1295 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1296 );
1297
1298 editor.move_to_beginning(&MoveToBeginning, window, cx);
1299 assert_eq!(
1300 editor.selections.display_ranges(cx),
1301 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1302 );
1303
1304 editor.change_selections(None, window, cx, |s| {
1305 s.select_display_ranges([
1306 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1307 ]);
1308 });
1309 editor.select_to_beginning(&SelectToBeginning, window, cx);
1310 assert_eq!(
1311 editor.selections.display_ranges(cx),
1312 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1313 );
1314
1315 editor.select_to_end(&SelectToEnd, window, cx);
1316 assert_eq!(
1317 editor.selections.display_ranges(cx),
1318 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1319 );
1320 });
1321}
1322
1323#[gpui::test]
1324fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1325 init_test(cx, |_| {});
1326
1327 let editor = cx.add_window(|window, cx| {
1328 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1329 build_editor(buffer.clone(), window, cx)
1330 });
1331
1332 assert_eq!('🟥'.len_utf8(), 4);
1333 assert_eq!('α'.len_utf8(), 2);
1334
1335 _ = editor.update(cx, |editor, window, cx| {
1336 editor.fold_creases(
1337 vec![
1338 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1339 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1340 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1341 ],
1342 true,
1343 window,
1344 cx,
1345 );
1346 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1347
1348 editor.move_right(&MoveRight, window, cx);
1349 assert_eq!(
1350 editor.selections.display_ranges(cx),
1351 &[empty_range(0, "🟥".len())]
1352 );
1353 editor.move_right(&MoveRight, window, cx);
1354 assert_eq!(
1355 editor.selections.display_ranges(cx),
1356 &[empty_range(0, "🟥🟧".len())]
1357 );
1358 editor.move_right(&MoveRight, window, cx);
1359 assert_eq!(
1360 editor.selections.display_ranges(cx),
1361 &[empty_range(0, "🟥🟧⋯".len())]
1362 );
1363
1364 editor.move_down(&MoveDown, window, cx);
1365 assert_eq!(
1366 editor.selections.display_ranges(cx),
1367 &[empty_range(1, "ab⋯e".len())]
1368 );
1369 editor.move_left(&MoveLeft, window, cx);
1370 assert_eq!(
1371 editor.selections.display_ranges(cx),
1372 &[empty_range(1, "ab⋯".len())]
1373 );
1374 editor.move_left(&MoveLeft, window, cx);
1375 assert_eq!(
1376 editor.selections.display_ranges(cx),
1377 &[empty_range(1, "ab".len())]
1378 );
1379 editor.move_left(&MoveLeft, window, cx);
1380 assert_eq!(
1381 editor.selections.display_ranges(cx),
1382 &[empty_range(1, "a".len())]
1383 );
1384
1385 editor.move_down(&MoveDown, window, cx);
1386 assert_eq!(
1387 editor.selections.display_ranges(cx),
1388 &[empty_range(2, "α".len())]
1389 );
1390 editor.move_right(&MoveRight, window, cx);
1391 assert_eq!(
1392 editor.selections.display_ranges(cx),
1393 &[empty_range(2, "αβ".len())]
1394 );
1395 editor.move_right(&MoveRight, window, cx);
1396 assert_eq!(
1397 editor.selections.display_ranges(cx),
1398 &[empty_range(2, "αβ⋯".len())]
1399 );
1400 editor.move_right(&MoveRight, window, cx);
1401 assert_eq!(
1402 editor.selections.display_ranges(cx),
1403 &[empty_range(2, "αβ⋯ε".len())]
1404 );
1405
1406 editor.move_up(&MoveUp, window, cx);
1407 assert_eq!(
1408 editor.selections.display_ranges(cx),
1409 &[empty_range(1, "ab⋯e".len())]
1410 );
1411 editor.move_down(&MoveDown, window, cx);
1412 assert_eq!(
1413 editor.selections.display_ranges(cx),
1414 &[empty_range(2, "αβ⋯ε".len())]
1415 );
1416 editor.move_up(&MoveUp, window, cx);
1417 assert_eq!(
1418 editor.selections.display_ranges(cx),
1419 &[empty_range(1, "ab⋯e".len())]
1420 );
1421
1422 editor.move_up(&MoveUp, window, cx);
1423 assert_eq!(
1424 editor.selections.display_ranges(cx),
1425 &[empty_range(0, "🟥🟧".len())]
1426 );
1427 editor.move_left(&MoveLeft, window, cx);
1428 assert_eq!(
1429 editor.selections.display_ranges(cx),
1430 &[empty_range(0, "🟥".len())]
1431 );
1432 editor.move_left(&MoveLeft, window, cx);
1433 assert_eq!(
1434 editor.selections.display_ranges(cx),
1435 &[empty_range(0, "".len())]
1436 );
1437 });
1438}
1439
1440#[gpui::test]
1441fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1442 init_test(cx, |_| {});
1443
1444 let editor = cx.add_window(|window, cx| {
1445 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1446 build_editor(buffer.clone(), window, cx)
1447 });
1448 _ = editor.update(cx, |editor, window, cx| {
1449 editor.change_selections(None, window, cx, |s| {
1450 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1451 });
1452
1453 // moving above start of document should move selection to start of document,
1454 // but the next move down should still be at the original goal_x
1455 editor.move_up(&MoveUp, window, cx);
1456 assert_eq!(
1457 editor.selections.display_ranges(cx),
1458 &[empty_range(0, "".len())]
1459 );
1460
1461 editor.move_down(&MoveDown, window, cx);
1462 assert_eq!(
1463 editor.selections.display_ranges(cx),
1464 &[empty_range(1, "abcd".len())]
1465 );
1466
1467 editor.move_down(&MoveDown, window, cx);
1468 assert_eq!(
1469 editor.selections.display_ranges(cx),
1470 &[empty_range(2, "αβγ".len())]
1471 );
1472
1473 editor.move_down(&MoveDown, window, cx);
1474 assert_eq!(
1475 editor.selections.display_ranges(cx),
1476 &[empty_range(3, "abcd".len())]
1477 );
1478
1479 editor.move_down(&MoveDown, window, cx);
1480 assert_eq!(
1481 editor.selections.display_ranges(cx),
1482 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1483 );
1484
1485 // moving past end of document should not change goal_x
1486 editor.move_down(&MoveDown, window, cx);
1487 assert_eq!(
1488 editor.selections.display_ranges(cx),
1489 &[empty_range(5, "".len())]
1490 );
1491
1492 editor.move_down(&MoveDown, window, cx);
1493 assert_eq!(
1494 editor.selections.display_ranges(cx),
1495 &[empty_range(5, "".len())]
1496 );
1497
1498 editor.move_up(&MoveUp, window, cx);
1499 assert_eq!(
1500 editor.selections.display_ranges(cx),
1501 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1502 );
1503
1504 editor.move_up(&MoveUp, window, cx);
1505 assert_eq!(
1506 editor.selections.display_ranges(cx),
1507 &[empty_range(3, "abcd".len())]
1508 );
1509
1510 editor.move_up(&MoveUp, window, cx);
1511 assert_eq!(
1512 editor.selections.display_ranges(cx),
1513 &[empty_range(2, "αβγ".len())]
1514 );
1515 });
1516}
1517
1518#[gpui::test]
1519fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1520 init_test(cx, |_| {});
1521 let move_to_beg = MoveToBeginningOfLine {
1522 stop_at_soft_wraps: true,
1523 stop_at_indent: true,
1524 };
1525
1526 let delete_to_beg = DeleteToBeginningOfLine {
1527 stop_at_indent: false,
1528 };
1529
1530 let move_to_end = MoveToEndOfLine {
1531 stop_at_soft_wraps: true,
1532 };
1533
1534 let editor = cx.add_window(|window, cx| {
1535 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1536 build_editor(buffer, window, cx)
1537 });
1538 _ = editor.update(cx, |editor, window, cx| {
1539 editor.change_selections(None, window, cx, |s| {
1540 s.select_display_ranges([
1541 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1542 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1543 ]);
1544 });
1545 });
1546
1547 _ = editor.update(cx, |editor, window, cx| {
1548 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1549 assert_eq!(
1550 editor.selections.display_ranges(cx),
1551 &[
1552 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1553 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1554 ]
1555 );
1556 });
1557
1558 _ = editor.update(cx, |editor, window, cx| {
1559 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1560 assert_eq!(
1561 editor.selections.display_ranges(cx),
1562 &[
1563 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1564 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1565 ]
1566 );
1567 });
1568
1569 _ = editor.update(cx, |editor, window, cx| {
1570 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1571 assert_eq!(
1572 editor.selections.display_ranges(cx),
1573 &[
1574 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1575 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1576 ]
1577 );
1578 });
1579
1580 _ = editor.update(cx, |editor, window, cx| {
1581 editor.move_to_end_of_line(&move_to_end, window, cx);
1582 assert_eq!(
1583 editor.selections.display_ranges(cx),
1584 &[
1585 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1586 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1587 ]
1588 );
1589 });
1590
1591 // Moving to the end of line again is a no-op.
1592 _ = editor.update(cx, |editor, window, cx| {
1593 editor.move_to_end_of_line(&move_to_end, window, cx);
1594 assert_eq!(
1595 editor.selections.display_ranges(cx),
1596 &[
1597 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1598 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1599 ]
1600 );
1601 });
1602
1603 _ = editor.update(cx, |editor, window, cx| {
1604 editor.move_left(&MoveLeft, window, cx);
1605 editor.select_to_beginning_of_line(
1606 &SelectToBeginningOfLine {
1607 stop_at_soft_wraps: true,
1608 stop_at_indent: true,
1609 },
1610 window,
1611 cx,
1612 );
1613 assert_eq!(
1614 editor.selections.display_ranges(cx),
1615 &[
1616 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1617 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1618 ]
1619 );
1620 });
1621
1622 _ = editor.update(cx, |editor, window, cx| {
1623 editor.select_to_beginning_of_line(
1624 &SelectToBeginningOfLine {
1625 stop_at_soft_wraps: true,
1626 stop_at_indent: true,
1627 },
1628 window,
1629 cx,
1630 );
1631 assert_eq!(
1632 editor.selections.display_ranges(cx),
1633 &[
1634 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1635 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1636 ]
1637 );
1638 });
1639
1640 _ = editor.update(cx, |editor, window, cx| {
1641 editor.select_to_beginning_of_line(
1642 &SelectToBeginningOfLine {
1643 stop_at_soft_wraps: true,
1644 stop_at_indent: true,
1645 },
1646 window,
1647 cx,
1648 );
1649 assert_eq!(
1650 editor.selections.display_ranges(cx),
1651 &[
1652 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1653 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1654 ]
1655 );
1656 });
1657
1658 _ = editor.update(cx, |editor, window, cx| {
1659 editor.select_to_end_of_line(
1660 &SelectToEndOfLine {
1661 stop_at_soft_wraps: true,
1662 },
1663 window,
1664 cx,
1665 );
1666 assert_eq!(
1667 editor.selections.display_ranges(cx),
1668 &[
1669 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1670 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1671 ]
1672 );
1673 });
1674
1675 _ = editor.update(cx, |editor, window, cx| {
1676 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1677 assert_eq!(editor.display_text(cx), "ab\n de");
1678 assert_eq!(
1679 editor.selections.display_ranges(cx),
1680 &[
1681 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1682 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1683 ]
1684 );
1685 });
1686
1687 _ = editor.update(cx, |editor, window, cx| {
1688 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1689 assert_eq!(editor.display_text(cx), "\n");
1690 assert_eq!(
1691 editor.selections.display_ranges(cx),
1692 &[
1693 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1694 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1695 ]
1696 );
1697 });
1698}
1699
1700#[gpui::test]
1701fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1702 init_test(cx, |_| {});
1703 let move_to_beg = MoveToBeginningOfLine {
1704 stop_at_soft_wraps: false,
1705 stop_at_indent: false,
1706 };
1707
1708 let move_to_end = MoveToEndOfLine {
1709 stop_at_soft_wraps: false,
1710 };
1711
1712 let editor = cx.add_window(|window, cx| {
1713 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1714 build_editor(buffer, window, cx)
1715 });
1716
1717 _ = editor.update(cx, |editor, window, cx| {
1718 editor.set_wrap_width(Some(140.0.into()), cx);
1719
1720 // We expect the following lines after wrapping
1721 // ```
1722 // thequickbrownfox
1723 // jumpedoverthelazydo
1724 // gs
1725 // ```
1726 // The final `gs` was soft-wrapped onto a new line.
1727 assert_eq!(
1728 "thequickbrownfox\njumpedoverthelaz\nydogs",
1729 editor.display_text(cx),
1730 );
1731
1732 // First, let's assert behavior on the first line, that was not soft-wrapped.
1733 // Start the cursor at the `k` on the first line
1734 editor.change_selections(None, window, cx, |s| {
1735 s.select_display_ranges([
1736 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1737 ]);
1738 });
1739
1740 // Moving to the beginning of the line should put us at the beginning of the line.
1741 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1742 assert_eq!(
1743 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1744 editor.selections.display_ranges(cx)
1745 );
1746
1747 // Moving to the end of the line should put us at the end of the line.
1748 editor.move_to_end_of_line(&move_to_end, window, cx);
1749 assert_eq!(
1750 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1751 editor.selections.display_ranges(cx)
1752 );
1753
1754 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1755 // Start the cursor at the last line (`y` that was wrapped to a new line)
1756 editor.change_selections(None, window, cx, |s| {
1757 s.select_display_ranges([
1758 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1759 ]);
1760 });
1761
1762 // Moving to the beginning of the line should put us at the start of the second line of
1763 // display text, i.e., the `j`.
1764 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1765 assert_eq!(
1766 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1767 editor.selections.display_ranges(cx)
1768 );
1769
1770 // Moving to the beginning of the line again should be a no-op.
1771 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1772 assert_eq!(
1773 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1774 editor.selections.display_ranges(cx)
1775 );
1776
1777 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1778 // next display line.
1779 editor.move_to_end_of_line(&move_to_end, window, cx);
1780 assert_eq!(
1781 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1782 editor.selections.display_ranges(cx)
1783 );
1784
1785 // Moving to the end of the line again should be a no-op.
1786 editor.move_to_end_of_line(&move_to_end, window, cx);
1787 assert_eq!(
1788 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1789 editor.selections.display_ranges(cx)
1790 );
1791 });
1792}
1793
1794#[gpui::test]
1795fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1796 init_test(cx, |_| {});
1797
1798 let move_to_beg = MoveToBeginningOfLine {
1799 stop_at_soft_wraps: true,
1800 stop_at_indent: true,
1801 };
1802
1803 let select_to_beg = SelectToBeginningOfLine {
1804 stop_at_soft_wraps: true,
1805 stop_at_indent: true,
1806 };
1807
1808 let delete_to_beg = DeleteToBeginningOfLine {
1809 stop_at_indent: true,
1810 };
1811
1812 let move_to_end = MoveToEndOfLine {
1813 stop_at_soft_wraps: false,
1814 };
1815
1816 let editor = cx.add_window(|window, cx| {
1817 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1818 build_editor(buffer, window, cx)
1819 });
1820
1821 _ = editor.update(cx, |editor, window, cx| {
1822 editor.change_selections(None, window, cx, |s| {
1823 s.select_display_ranges([
1824 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1825 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1826 ]);
1827 });
1828
1829 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1830 // and the second cursor at the first non-whitespace character in the line.
1831 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1832 assert_eq!(
1833 editor.selections.display_ranges(cx),
1834 &[
1835 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1836 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1837 ]
1838 );
1839
1840 // Moving to the beginning of the line again should be a no-op for the first cursor,
1841 // and should move the second cursor to the beginning of the line.
1842 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1843 assert_eq!(
1844 editor.selections.display_ranges(cx),
1845 &[
1846 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1847 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1848 ]
1849 );
1850
1851 // Moving to the beginning of the line again should still be a no-op for the first cursor,
1852 // and should move the second cursor back to the first non-whitespace character in the line.
1853 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1854 assert_eq!(
1855 editor.selections.display_ranges(cx),
1856 &[
1857 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1858 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1859 ]
1860 );
1861
1862 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
1863 // and to the first non-whitespace character in the line for the second cursor.
1864 editor.move_to_end_of_line(&move_to_end, window, cx);
1865 editor.move_left(&MoveLeft, window, cx);
1866 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1867 assert_eq!(
1868 editor.selections.display_ranges(cx),
1869 &[
1870 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1871 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1872 ]
1873 );
1874
1875 // Selecting to the beginning of the line again should be a no-op for the first cursor,
1876 // and should select to the beginning of the line for the second cursor.
1877 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1878 assert_eq!(
1879 editor.selections.display_ranges(cx),
1880 &[
1881 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1882 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1883 ]
1884 );
1885
1886 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
1887 // and should delete to the first non-whitespace character in the line for the second cursor.
1888 editor.move_to_end_of_line(&move_to_end, window, cx);
1889 editor.move_left(&MoveLeft, window, cx);
1890 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1891 assert_eq!(editor.text(cx), "c\n f");
1892 });
1893}
1894
1895#[gpui::test]
1896fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1897 init_test(cx, |_| {});
1898
1899 let editor = cx.add_window(|window, cx| {
1900 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1901 build_editor(buffer, window, cx)
1902 });
1903 _ = editor.update(cx, |editor, window, cx| {
1904 editor.change_selections(None, window, cx, |s| {
1905 s.select_display_ranges([
1906 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
1907 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
1908 ])
1909 });
1910 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1911 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1912
1913 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1914 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
1915
1916 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1917 assert_selection_ranges("use ˇstd::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1918
1919 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1920 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1921
1922 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1923 assert_selection_ranges("ˇuse std::str::{foo, ˇbar}\n\n {baz.qux()}", editor, cx);
1924
1925 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1926 assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1927
1928 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1929 assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
1930
1931 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1932 assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1933
1934 editor.move_right(&MoveRight, window, cx);
1935 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1936 assert_selection_ranges(
1937 "use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}",
1938 editor,
1939 cx,
1940 );
1941
1942 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1943 assert_selection_ranges(
1944 "use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
1945 editor,
1946 cx,
1947 );
1948
1949 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
1950 assert_selection_ranges(
1951 "use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
1952 editor,
1953 cx,
1954 );
1955 });
1956}
1957
1958#[gpui::test]
1959fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
1960 init_test(cx, |_| {});
1961
1962 let editor = cx.add_window(|window, cx| {
1963 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
1964 build_editor(buffer, window, cx)
1965 });
1966
1967 _ = editor.update(cx, |editor, window, cx| {
1968 editor.set_wrap_width(Some(140.0.into()), cx);
1969 assert_eq!(
1970 editor.display_text(cx),
1971 "use one::{\n two::three::\n four::five\n};"
1972 );
1973
1974 editor.change_selections(None, window, cx, |s| {
1975 s.select_display_ranges([
1976 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
1977 ]);
1978 });
1979
1980 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1981 assert_eq!(
1982 editor.selections.display_ranges(cx),
1983 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
1984 );
1985
1986 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1987 assert_eq!(
1988 editor.selections.display_ranges(cx),
1989 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
1990 );
1991
1992 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1993 assert_eq!(
1994 editor.selections.display_ranges(cx),
1995 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
1996 );
1997
1998 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1999 assert_eq!(
2000 editor.selections.display_ranges(cx),
2001 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2002 );
2003
2004 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2005 assert_eq!(
2006 editor.selections.display_ranges(cx),
2007 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2008 );
2009
2010 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2011 assert_eq!(
2012 editor.selections.display_ranges(cx),
2013 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2014 );
2015 });
2016}
2017
2018#[gpui::test]
2019async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2020 init_test(cx, |_| {});
2021 let mut cx = EditorTestContext::new(cx).await;
2022
2023 let line_height = cx.editor(|editor, window, _| {
2024 editor
2025 .style()
2026 .unwrap()
2027 .text
2028 .line_height_in_pixels(window.rem_size())
2029 });
2030 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2031
2032 cx.set_state(
2033 &r#"ˇone
2034 two
2035
2036 three
2037 fourˇ
2038 five
2039
2040 six"#
2041 .unindent(),
2042 );
2043
2044 cx.update_editor(|editor, window, cx| {
2045 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2046 });
2047 cx.assert_editor_state(
2048 &r#"one
2049 two
2050 ˇ
2051 three
2052 four
2053 five
2054 ˇ
2055 six"#
2056 .unindent(),
2057 );
2058
2059 cx.update_editor(|editor, window, cx| {
2060 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2061 });
2062 cx.assert_editor_state(
2063 &r#"one
2064 two
2065
2066 three
2067 four
2068 five
2069 ˇ
2070 sixˇ"#
2071 .unindent(),
2072 );
2073
2074 cx.update_editor(|editor, window, cx| {
2075 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2076 });
2077 cx.assert_editor_state(
2078 &r#"one
2079 two
2080
2081 three
2082 four
2083 five
2084
2085 sixˇ"#
2086 .unindent(),
2087 );
2088
2089 cx.update_editor(|editor, window, cx| {
2090 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2091 });
2092 cx.assert_editor_state(
2093 &r#"one
2094 two
2095
2096 three
2097 four
2098 five
2099 ˇ
2100 six"#
2101 .unindent(),
2102 );
2103
2104 cx.update_editor(|editor, window, cx| {
2105 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2106 });
2107 cx.assert_editor_state(
2108 &r#"one
2109 two
2110 ˇ
2111 three
2112 four
2113 five
2114
2115 six"#
2116 .unindent(),
2117 );
2118
2119 cx.update_editor(|editor, window, cx| {
2120 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2121 });
2122 cx.assert_editor_state(
2123 &r#"ˇone
2124 two
2125
2126 three
2127 four
2128 five
2129
2130 six"#
2131 .unindent(),
2132 );
2133}
2134
2135#[gpui::test]
2136async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2137 init_test(cx, |_| {});
2138 let mut cx = EditorTestContext::new(cx).await;
2139 let line_height = cx.editor(|editor, window, _| {
2140 editor
2141 .style()
2142 .unwrap()
2143 .text
2144 .line_height_in_pixels(window.rem_size())
2145 });
2146 let window = cx.window;
2147 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2148
2149 cx.set_state(
2150 r#"ˇone
2151 two
2152 three
2153 four
2154 five
2155 six
2156 seven
2157 eight
2158 nine
2159 ten
2160 "#,
2161 );
2162
2163 cx.update_editor(|editor, window, cx| {
2164 assert_eq!(
2165 editor.snapshot(window, cx).scroll_position(),
2166 gpui::Point::new(0., 0.)
2167 );
2168 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2169 assert_eq!(
2170 editor.snapshot(window, cx).scroll_position(),
2171 gpui::Point::new(0., 3.)
2172 );
2173 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2174 assert_eq!(
2175 editor.snapshot(window, cx).scroll_position(),
2176 gpui::Point::new(0., 6.)
2177 );
2178 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2179 assert_eq!(
2180 editor.snapshot(window, cx).scroll_position(),
2181 gpui::Point::new(0., 3.)
2182 );
2183
2184 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2185 assert_eq!(
2186 editor.snapshot(window, cx).scroll_position(),
2187 gpui::Point::new(0., 1.)
2188 );
2189 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2190 assert_eq!(
2191 editor.snapshot(window, cx).scroll_position(),
2192 gpui::Point::new(0., 3.)
2193 );
2194 });
2195}
2196
2197#[gpui::test]
2198async fn test_autoscroll(cx: &mut TestAppContext) {
2199 init_test(cx, |_| {});
2200 let mut cx = EditorTestContext::new(cx).await;
2201
2202 let line_height = cx.update_editor(|editor, window, cx| {
2203 editor.set_vertical_scroll_margin(2, cx);
2204 editor
2205 .style()
2206 .unwrap()
2207 .text
2208 .line_height_in_pixels(window.rem_size())
2209 });
2210 let window = cx.window;
2211 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2212
2213 cx.set_state(
2214 r#"ˇone
2215 two
2216 three
2217 four
2218 five
2219 six
2220 seven
2221 eight
2222 nine
2223 ten
2224 "#,
2225 );
2226 cx.update_editor(|editor, window, cx| {
2227 assert_eq!(
2228 editor.snapshot(window, cx).scroll_position(),
2229 gpui::Point::new(0., 0.0)
2230 );
2231 });
2232
2233 // Add a cursor below the visible area. Since both cursors cannot fit
2234 // on screen, the editor autoscrolls to reveal the newest cursor, and
2235 // allows the vertical scroll margin below that cursor.
2236 cx.update_editor(|editor, window, cx| {
2237 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2238 selections.select_ranges([
2239 Point::new(0, 0)..Point::new(0, 0),
2240 Point::new(6, 0)..Point::new(6, 0),
2241 ]);
2242 })
2243 });
2244 cx.update_editor(|editor, window, cx| {
2245 assert_eq!(
2246 editor.snapshot(window, cx).scroll_position(),
2247 gpui::Point::new(0., 3.0)
2248 );
2249 });
2250
2251 // Move down. The editor cursor scrolls down to track the newest cursor.
2252 cx.update_editor(|editor, window, cx| {
2253 editor.move_down(&Default::default(), window, cx);
2254 });
2255 cx.update_editor(|editor, window, cx| {
2256 assert_eq!(
2257 editor.snapshot(window, cx).scroll_position(),
2258 gpui::Point::new(0., 4.0)
2259 );
2260 });
2261
2262 // Add a cursor above the visible area. Since both cursors fit on screen,
2263 // the editor scrolls to show both.
2264 cx.update_editor(|editor, window, cx| {
2265 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2266 selections.select_ranges([
2267 Point::new(1, 0)..Point::new(1, 0),
2268 Point::new(6, 0)..Point::new(6, 0),
2269 ]);
2270 })
2271 });
2272 cx.update_editor(|editor, window, cx| {
2273 assert_eq!(
2274 editor.snapshot(window, cx).scroll_position(),
2275 gpui::Point::new(0., 1.0)
2276 );
2277 });
2278}
2279
2280#[gpui::test]
2281async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2282 init_test(cx, |_| {});
2283 let mut cx = EditorTestContext::new(cx).await;
2284
2285 let line_height = cx.editor(|editor, window, _cx| {
2286 editor
2287 .style()
2288 .unwrap()
2289 .text
2290 .line_height_in_pixels(window.rem_size())
2291 });
2292 let window = cx.window;
2293 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2294 cx.set_state(
2295 &r#"
2296 ˇone
2297 two
2298 threeˇ
2299 four
2300 five
2301 six
2302 seven
2303 eight
2304 nine
2305 ten
2306 "#
2307 .unindent(),
2308 );
2309
2310 cx.update_editor(|editor, window, cx| {
2311 editor.move_page_down(&MovePageDown::default(), window, cx)
2312 });
2313 cx.assert_editor_state(
2314 &r#"
2315 one
2316 two
2317 three
2318 ˇfour
2319 five
2320 sixˇ
2321 seven
2322 eight
2323 nine
2324 ten
2325 "#
2326 .unindent(),
2327 );
2328
2329 cx.update_editor(|editor, window, cx| {
2330 editor.move_page_down(&MovePageDown::default(), window, cx)
2331 });
2332 cx.assert_editor_state(
2333 &r#"
2334 one
2335 two
2336 three
2337 four
2338 five
2339 six
2340 ˇseven
2341 eight
2342 nineˇ
2343 ten
2344 "#
2345 .unindent(),
2346 );
2347
2348 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2349 cx.assert_editor_state(
2350 &r#"
2351 one
2352 two
2353 three
2354 ˇfour
2355 five
2356 sixˇ
2357 seven
2358 eight
2359 nine
2360 ten
2361 "#
2362 .unindent(),
2363 );
2364
2365 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2366 cx.assert_editor_state(
2367 &r#"
2368 ˇone
2369 two
2370 threeˇ
2371 four
2372 five
2373 six
2374 seven
2375 eight
2376 nine
2377 ten
2378 "#
2379 .unindent(),
2380 );
2381
2382 // Test select collapsing
2383 cx.update_editor(|editor, window, cx| {
2384 editor.move_page_down(&MovePageDown::default(), window, cx);
2385 editor.move_page_down(&MovePageDown::default(), window, cx);
2386 editor.move_page_down(&MovePageDown::default(), window, cx);
2387 });
2388 cx.assert_editor_state(
2389 &r#"
2390 one
2391 two
2392 three
2393 four
2394 five
2395 six
2396 seven
2397 eight
2398 nine
2399 ˇten
2400 ˇ"#
2401 .unindent(),
2402 );
2403}
2404
2405#[gpui::test]
2406async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2407 init_test(cx, |_| {});
2408 let mut cx = EditorTestContext::new(cx).await;
2409 cx.set_state("one «two threeˇ» four");
2410 cx.update_editor(|editor, window, cx| {
2411 editor.delete_to_beginning_of_line(
2412 &DeleteToBeginningOfLine {
2413 stop_at_indent: false,
2414 },
2415 window,
2416 cx,
2417 );
2418 assert_eq!(editor.text(cx), " four");
2419 });
2420}
2421
2422#[gpui::test]
2423fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2424 init_test(cx, |_| {});
2425
2426 let editor = cx.add_window(|window, cx| {
2427 let buffer = MultiBuffer::build_simple("one two three four", cx);
2428 build_editor(buffer.clone(), window, cx)
2429 });
2430
2431 _ = editor.update(cx, |editor, window, cx| {
2432 editor.change_selections(None, window, cx, |s| {
2433 s.select_display_ranges([
2434 // an empty selection - the preceding word fragment is deleted
2435 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2436 // characters selected - they are deleted
2437 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 12),
2438 ])
2439 });
2440 editor.delete_to_previous_word_start(
2441 &DeleteToPreviousWordStart {
2442 ignore_newlines: false,
2443 },
2444 window,
2445 cx,
2446 );
2447 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e two te four");
2448 });
2449
2450 _ = editor.update(cx, |editor, window, cx| {
2451 editor.change_selections(None, window, cx, |s| {
2452 s.select_display_ranges([
2453 // an empty selection - the following word fragment is deleted
2454 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
2455 // characters selected - they are deleted
2456 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 10),
2457 ])
2458 });
2459 editor.delete_to_next_word_end(
2460 &DeleteToNextWordEnd {
2461 ignore_newlines: false,
2462 },
2463 window,
2464 cx,
2465 );
2466 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e t te our");
2467 });
2468}
2469
2470#[gpui::test]
2471fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2472 init_test(cx, |_| {});
2473
2474 let editor = cx.add_window(|window, cx| {
2475 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2476 build_editor(buffer.clone(), window, cx)
2477 });
2478 let del_to_prev_word_start = DeleteToPreviousWordStart {
2479 ignore_newlines: false,
2480 };
2481 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2482 ignore_newlines: true,
2483 };
2484
2485 _ = editor.update(cx, |editor, window, cx| {
2486 editor.change_selections(None, window, cx, |s| {
2487 s.select_display_ranges([
2488 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2489 ])
2490 });
2491 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2492 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2493 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2494 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2495 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2496 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2497 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2498 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
2499 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2500 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
2501 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2502 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2503 });
2504}
2505
2506#[gpui::test]
2507fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2508 init_test(cx, |_| {});
2509
2510 let editor = cx.add_window(|window, cx| {
2511 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2512 build_editor(buffer.clone(), window, cx)
2513 });
2514 let del_to_next_word_end = DeleteToNextWordEnd {
2515 ignore_newlines: false,
2516 };
2517 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2518 ignore_newlines: true,
2519 };
2520
2521 _ = editor.update(cx, |editor, window, cx| {
2522 editor.change_selections(None, window, cx, |s| {
2523 s.select_display_ranges([
2524 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2525 ])
2526 });
2527 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2528 assert_eq!(
2529 editor.buffer.read(cx).read(cx).text(),
2530 "one\n two\nthree\n four"
2531 );
2532 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2533 assert_eq!(
2534 editor.buffer.read(cx).read(cx).text(),
2535 "\n two\nthree\n four"
2536 );
2537 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2538 assert_eq!(
2539 editor.buffer.read(cx).read(cx).text(),
2540 "two\nthree\n four"
2541 );
2542 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2543 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2544 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2545 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
2546 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2547 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2548 });
2549}
2550
2551#[gpui::test]
2552fn test_newline(cx: &mut TestAppContext) {
2553 init_test(cx, |_| {});
2554
2555 let editor = cx.add_window(|window, cx| {
2556 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2557 build_editor(buffer.clone(), window, cx)
2558 });
2559
2560 _ = editor.update(cx, |editor, window, cx| {
2561 editor.change_selections(None, window, cx, |s| {
2562 s.select_display_ranges([
2563 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2564 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2565 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2566 ])
2567 });
2568
2569 editor.newline(&Newline, window, cx);
2570 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2571 });
2572}
2573
2574#[gpui::test]
2575fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2576 init_test(cx, |_| {});
2577
2578 let editor = cx.add_window(|window, cx| {
2579 let buffer = MultiBuffer::build_simple(
2580 "
2581 a
2582 b(
2583 X
2584 )
2585 c(
2586 X
2587 )
2588 "
2589 .unindent()
2590 .as_str(),
2591 cx,
2592 );
2593 let mut editor = build_editor(buffer.clone(), window, cx);
2594 editor.change_selections(None, window, cx, |s| {
2595 s.select_ranges([
2596 Point::new(2, 4)..Point::new(2, 5),
2597 Point::new(5, 4)..Point::new(5, 5),
2598 ])
2599 });
2600 editor
2601 });
2602
2603 _ = editor.update(cx, |editor, window, cx| {
2604 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2605 editor.buffer.update(cx, |buffer, cx| {
2606 buffer.edit(
2607 [
2608 (Point::new(1, 2)..Point::new(3, 0), ""),
2609 (Point::new(4, 2)..Point::new(6, 0), ""),
2610 ],
2611 None,
2612 cx,
2613 );
2614 assert_eq!(
2615 buffer.read(cx).text(),
2616 "
2617 a
2618 b()
2619 c()
2620 "
2621 .unindent()
2622 );
2623 });
2624 assert_eq!(
2625 editor.selections.ranges(cx),
2626 &[
2627 Point::new(1, 2)..Point::new(1, 2),
2628 Point::new(2, 2)..Point::new(2, 2),
2629 ],
2630 );
2631
2632 editor.newline(&Newline, window, cx);
2633 assert_eq!(
2634 editor.text(cx),
2635 "
2636 a
2637 b(
2638 )
2639 c(
2640 )
2641 "
2642 .unindent()
2643 );
2644
2645 // The selections are moved after the inserted newlines
2646 assert_eq!(
2647 editor.selections.ranges(cx),
2648 &[
2649 Point::new(2, 0)..Point::new(2, 0),
2650 Point::new(4, 0)..Point::new(4, 0),
2651 ],
2652 );
2653 });
2654}
2655
2656#[gpui::test]
2657async fn test_newline_above(cx: &mut TestAppContext) {
2658 init_test(cx, |settings| {
2659 settings.defaults.tab_size = NonZeroU32::new(4)
2660 });
2661
2662 let language = Arc::new(
2663 Language::new(
2664 LanguageConfig::default(),
2665 Some(tree_sitter_rust::LANGUAGE.into()),
2666 )
2667 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2668 .unwrap(),
2669 );
2670
2671 let mut cx = EditorTestContext::new(cx).await;
2672 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2673 cx.set_state(indoc! {"
2674 const a: ˇA = (
2675 (ˇ
2676 «const_functionˇ»(ˇ),
2677 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2678 )ˇ
2679 ˇ);ˇ
2680 "});
2681
2682 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
2683 cx.assert_editor_state(indoc! {"
2684 ˇ
2685 const a: A = (
2686 ˇ
2687 (
2688 ˇ
2689 ˇ
2690 const_function(),
2691 ˇ
2692 ˇ
2693 ˇ
2694 ˇ
2695 something_else,
2696 ˇ
2697 )
2698 ˇ
2699 ˇ
2700 );
2701 "});
2702}
2703
2704#[gpui::test]
2705async fn test_newline_below(cx: &mut TestAppContext) {
2706 init_test(cx, |settings| {
2707 settings.defaults.tab_size = NonZeroU32::new(4)
2708 });
2709
2710 let language = Arc::new(
2711 Language::new(
2712 LanguageConfig::default(),
2713 Some(tree_sitter_rust::LANGUAGE.into()),
2714 )
2715 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2716 .unwrap(),
2717 );
2718
2719 let mut cx = EditorTestContext::new(cx).await;
2720 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2721 cx.set_state(indoc! {"
2722 const a: ˇA = (
2723 (ˇ
2724 «const_functionˇ»(ˇ),
2725 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2726 )ˇ
2727 ˇ);ˇ
2728 "});
2729
2730 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
2731 cx.assert_editor_state(indoc! {"
2732 const a: A = (
2733 ˇ
2734 (
2735 ˇ
2736 const_function(),
2737 ˇ
2738 ˇ
2739 something_else,
2740 ˇ
2741 ˇ
2742 ˇ
2743 ˇ
2744 )
2745 ˇ
2746 );
2747 ˇ
2748 ˇ
2749 "});
2750}
2751
2752#[gpui::test]
2753async fn test_newline_comments(cx: &mut TestAppContext) {
2754 init_test(cx, |settings| {
2755 settings.defaults.tab_size = NonZeroU32::new(4)
2756 });
2757
2758 let language = Arc::new(Language::new(
2759 LanguageConfig {
2760 line_comments: vec!["// ".into()],
2761 ..LanguageConfig::default()
2762 },
2763 None,
2764 ));
2765 {
2766 let mut cx = EditorTestContext::new(cx).await;
2767 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2768 cx.set_state(indoc! {"
2769 // Fooˇ
2770 "});
2771
2772 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2773 cx.assert_editor_state(indoc! {"
2774 // Foo
2775 // ˇ
2776 "});
2777 // Ensure that we add comment prefix when existing line contains space
2778 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2779 cx.assert_editor_state(
2780 indoc! {"
2781 // Foo
2782 //s
2783 // ˇ
2784 "}
2785 .replace("s", " ") // s is used as space placeholder to prevent format on save
2786 .as_str(),
2787 );
2788 // Ensure that we add comment prefix when existing line does not contain space
2789 cx.set_state(indoc! {"
2790 // Foo
2791 //ˇ
2792 "});
2793 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2794 cx.assert_editor_state(indoc! {"
2795 // Foo
2796 //
2797 // ˇ
2798 "});
2799 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
2800 cx.set_state(indoc! {"
2801 ˇ// Foo
2802 "});
2803 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2804 cx.assert_editor_state(indoc! {"
2805
2806 ˇ// Foo
2807 "});
2808 }
2809 // Ensure that comment continuations can be disabled.
2810 update_test_language_settings(cx, |settings| {
2811 settings.defaults.extend_comment_on_newline = Some(false);
2812 });
2813 let mut cx = EditorTestContext::new(cx).await;
2814 cx.set_state(indoc! {"
2815 // Fooˇ
2816 "});
2817 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2818 cx.assert_editor_state(indoc! {"
2819 // Foo
2820 ˇ
2821 "});
2822}
2823
2824#[gpui::test]
2825async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
2826 init_test(cx, |settings| {
2827 settings.defaults.tab_size = NonZeroU32::new(4)
2828 });
2829
2830 let language = Arc::new(Language::new(
2831 LanguageConfig {
2832 line_comments: vec!["// ".into(), "/// ".into()],
2833 ..LanguageConfig::default()
2834 },
2835 None,
2836 ));
2837 {
2838 let mut cx = EditorTestContext::new(cx).await;
2839 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2840 cx.set_state(indoc! {"
2841 //ˇ
2842 "});
2843 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2844 cx.assert_editor_state(indoc! {"
2845 //
2846 // ˇ
2847 "});
2848
2849 cx.set_state(indoc! {"
2850 ///ˇ
2851 "});
2852 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2853 cx.assert_editor_state(indoc! {"
2854 ///
2855 /// ˇ
2856 "});
2857 }
2858}
2859
2860#[gpui::test]
2861async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
2862 init_test(cx, |settings| {
2863 settings.defaults.tab_size = NonZeroU32::new(4)
2864 });
2865
2866 let language = Arc::new(
2867 Language::new(
2868 LanguageConfig {
2869 documentation: Some(language::DocumentationConfig {
2870 start: "/**".into(),
2871 end: "*/".into(),
2872 prefix: "* ".into(),
2873 tab_size: NonZeroU32::new(1).unwrap(),
2874 }),
2875
2876 ..LanguageConfig::default()
2877 },
2878 Some(tree_sitter_rust::LANGUAGE.into()),
2879 )
2880 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
2881 .unwrap(),
2882 );
2883
2884 {
2885 let mut cx = EditorTestContext::new(cx).await;
2886 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2887 cx.set_state(indoc! {"
2888 /**ˇ
2889 "});
2890
2891 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2892 cx.assert_editor_state(indoc! {"
2893 /**
2894 * ˇ
2895 "});
2896 // Ensure that if cursor is before the comment start,
2897 // we do not actually insert a comment prefix.
2898 cx.set_state(indoc! {"
2899 ˇ/**
2900 "});
2901 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2902 cx.assert_editor_state(indoc! {"
2903
2904 ˇ/**
2905 "});
2906 // Ensure that if cursor is between it doesn't add comment prefix.
2907 cx.set_state(indoc! {"
2908 /*ˇ*
2909 "});
2910 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2911 cx.assert_editor_state(indoc! {"
2912 /*
2913 ˇ*
2914 "});
2915 // Ensure that if suffix exists on same line after cursor it adds new line.
2916 cx.set_state(indoc! {"
2917 /**ˇ*/
2918 "});
2919 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2920 cx.assert_editor_state(indoc! {"
2921 /**
2922 * ˇ
2923 */
2924 "});
2925 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2926 cx.set_state(indoc! {"
2927 /**ˇ */
2928 "});
2929 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2930 cx.assert_editor_state(indoc! {"
2931 /**
2932 * ˇ
2933 */
2934 "});
2935 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2936 cx.set_state(indoc! {"
2937 /** ˇ*/
2938 "});
2939 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2940 cx.assert_editor_state(
2941 indoc! {"
2942 /**s
2943 * ˇ
2944 */
2945 "}
2946 .replace("s", " ") // s is used as space placeholder to prevent format on save
2947 .as_str(),
2948 );
2949 // Ensure that delimiter space is preserved when newline on already
2950 // spaced delimiter.
2951 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2952 cx.assert_editor_state(
2953 indoc! {"
2954 /**s
2955 *s
2956 * ˇ
2957 */
2958 "}
2959 .replace("s", " ") // s is used as space placeholder to prevent format on save
2960 .as_str(),
2961 );
2962 // Ensure that delimiter space is preserved when space is not
2963 // on existing delimiter.
2964 cx.set_state(indoc! {"
2965 /**
2966 *ˇ
2967 */
2968 "});
2969 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2970 cx.assert_editor_state(indoc! {"
2971 /**
2972 *
2973 * ˇ
2974 */
2975 "});
2976 // Ensure that if suffix exists on same line after cursor it
2977 // doesn't add extra new line if prefix is not on same line.
2978 cx.set_state(indoc! {"
2979 /**
2980 ˇ*/
2981 "});
2982 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2983 cx.assert_editor_state(indoc! {"
2984 /**
2985
2986 ˇ*/
2987 "});
2988 // Ensure that it detects suffix after existing prefix.
2989 cx.set_state(indoc! {"
2990 /**ˇ/
2991 "});
2992 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2993 cx.assert_editor_state(indoc! {"
2994 /**
2995 ˇ/
2996 "});
2997 // Ensure that if suffix exists on same line before
2998 // cursor it does not add comment prefix.
2999 cx.set_state(indoc! {"
3000 /** */ˇ
3001 "});
3002 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3003 cx.assert_editor_state(indoc! {"
3004 /** */
3005 ˇ
3006 "});
3007 // Ensure that if suffix exists on same line before
3008 // cursor it does not add comment prefix.
3009 cx.set_state(indoc! {"
3010 /**
3011 *
3012 */ˇ
3013 "});
3014 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3015 cx.assert_editor_state(indoc! {"
3016 /**
3017 *
3018 */
3019 ˇ
3020 "});
3021
3022 // Ensure that inline comment followed by code
3023 // doesn't add comment prefix on newline
3024 cx.set_state(indoc! {"
3025 /** */ textˇ
3026 "});
3027 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3028 cx.assert_editor_state(indoc! {"
3029 /** */ text
3030 ˇ
3031 "});
3032
3033 // Ensure that text after comment end tag
3034 // doesn't add comment prefix on newline
3035 cx.set_state(indoc! {"
3036 /**
3037 *
3038 */ˇtext
3039 "});
3040 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3041 cx.assert_editor_state(indoc! {"
3042 /**
3043 *
3044 */
3045 ˇtext
3046 "});
3047
3048 // Ensure if not comment block it doesn't
3049 // add comment prefix on newline
3050 cx.set_state(indoc! {"
3051 * textˇ
3052 "});
3053 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3054 cx.assert_editor_state(indoc! {"
3055 * text
3056 ˇ
3057 "});
3058 }
3059 // Ensure that comment continuations can be disabled.
3060 update_test_language_settings(cx, |settings| {
3061 settings.defaults.extend_comment_on_newline = Some(false);
3062 });
3063 let mut cx = EditorTestContext::new(cx).await;
3064 cx.set_state(indoc! {"
3065 /**ˇ
3066 "});
3067 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3068 cx.assert_editor_state(indoc! {"
3069 /**
3070 ˇ
3071 "});
3072}
3073
3074#[gpui::test]
3075fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3076 init_test(cx, |_| {});
3077
3078 let editor = cx.add_window(|window, cx| {
3079 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3080 let mut editor = build_editor(buffer.clone(), window, cx);
3081 editor.change_selections(None, window, cx, |s| {
3082 s.select_ranges([3..4, 11..12, 19..20])
3083 });
3084 editor
3085 });
3086
3087 _ = editor.update(cx, |editor, window, cx| {
3088 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3089 editor.buffer.update(cx, |buffer, cx| {
3090 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3091 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3092 });
3093 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
3094
3095 editor.insert("Z", window, cx);
3096 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3097
3098 // The selections are moved after the inserted characters
3099 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
3100 });
3101}
3102
3103#[gpui::test]
3104async fn test_tab(cx: &mut TestAppContext) {
3105 init_test(cx, |settings| {
3106 settings.defaults.tab_size = NonZeroU32::new(3)
3107 });
3108
3109 let mut cx = EditorTestContext::new(cx).await;
3110 cx.set_state(indoc! {"
3111 ˇabˇc
3112 ˇ🏀ˇ🏀ˇefg
3113 dˇ
3114 "});
3115 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3116 cx.assert_editor_state(indoc! {"
3117 ˇab ˇc
3118 ˇ🏀 ˇ🏀 ˇefg
3119 d ˇ
3120 "});
3121
3122 cx.set_state(indoc! {"
3123 a
3124 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3125 "});
3126 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3127 cx.assert_editor_state(indoc! {"
3128 a
3129 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3130 "});
3131}
3132
3133#[gpui::test]
3134async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3135 init_test(cx, |_| {});
3136
3137 let mut cx = EditorTestContext::new(cx).await;
3138 let language = Arc::new(
3139 Language::new(
3140 LanguageConfig::default(),
3141 Some(tree_sitter_rust::LANGUAGE.into()),
3142 )
3143 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3144 .unwrap(),
3145 );
3146 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3147
3148 // test when all cursors are not at suggested indent
3149 // then simply move to their suggested indent location
3150 cx.set_state(indoc! {"
3151 const a: B = (
3152 c(
3153 ˇ
3154 ˇ )
3155 );
3156 "});
3157 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3158 cx.assert_editor_state(indoc! {"
3159 const a: B = (
3160 c(
3161 ˇ
3162 ˇ)
3163 );
3164 "});
3165
3166 // test cursor already at suggested indent not moving when
3167 // other cursors are yet to reach their suggested indents
3168 cx.set_state(indoc! {"
3169 ˇ
3170 const a: B = (
3171 c(
3172 d(
3173 ˇ
3174 )
3175 ˇ
3176 ˇ )
3177 );
3178 "});
3179 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3180 cx.assert_editor_state(indoc! {"
3181 ˇ
3182 const a: B = (
3183 c(
3184 d(
3185 ˇ
3186 )
3187 ˇ
3188 ˇ)
3189 );
3190 "});
3191 // test when all cursors are at suggested indent then tab is inserted
3192 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3193 cx.assert_editor_state(indoc! {"
3194 ˇ
3195 const a: B = (
3196 c(
3197 d(
3198 ˇ
3199 )
3200 ˇ
3201 ˇ)
3202 );
3203 "});
3204
3205 // test when current indent is less than suggested indent,
3206 // we adjust line to match suggested indent and move cursor to it
3207 //
3208 // when no other cursor is at word boundary, all of them should move
3209 cx.set_state(indoc! {"
3210 const a: B = (
3211 c(
3212 d(
3213 ˇ
3214 ˇ )
3215 ˇ )
3216 );
3217 "});
3218 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3219 cx.assert_editor_state(indoc! {"
3220 const a: B = (
3221 c(
3222 d(
3223 ˇ
3224 ˇ)
3225 ˇ)
3226 );
3227 "});
3228
3229 // test when current indent is less than suggested indent,
3230 // we adjust line to match suggested indent and move cursor to it
3231 //
3232 // when some other cursor is at word boundary, it should not move
3233 cx.set_state(indoc! {"
3234 const a: B = (
3235 c(
3236 d(
3237 ˇ
3238 ˇ )
3239 ˇ)
3240 );
3241 "});
3242 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3243 cx.assert_editor_state(indoc! {"
3244 const a: B = (
3245 c(
3246 d(
3247 ˇ
3248 ˇ)
3249 ˇ)
3250 );
3251 "});
3252
3253 // test when current indent is more than suggested indent,
3254 // we just move cursor to current indent instead of suggested indent
3255 //
3256 // when no other cursor is at word boundary, all of them should move
3257 cx.set_state(indoc! {"
3258 const a: B = (
3259 c(
3260 d(
3261 ˇ
3262 ˇ )
3263 ˇ )
3264 );
3265 "});
3266 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3267 cx.assert_editor_state(indoc! {"
3268 const a: B = (
3269 c(
3270 d(
3271 ˇ
3272 ˇ)
3273 ˇ)
3274 );
3275 "});
3276 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3277 cx.assert_editor_state(indoc! {"
3278 const a: B = (
3279 c(
3280 d(
3281 ˇ
3282 ˇ)
3283 ˇ)
3284 );
3285 "});
3286
3287 // test when current indent is more than suggested indent,
3288 // we just move cursor to current indent instead of suggested indent
3289 //
3290 // when some other cursor is at word boundary, it doesn't move
3291 cx.set_state(indoc! {"
3292 const a: B = (
3293 c(
3294 d(
3295 ˇ
3296 ˇ )
3297 ˇ)
3298 );
3299 "});
3300 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3301 cx.assert_editor_state(indoc! {"
3302 const a: B = (
3303 c(
3304 d(
3305 ˇ
3306 ˇ)
3307 ˇ)
3308 );
3309 "});
3310
3311 // handle auto-indent when there are multiple cursors on the same line
3312 cx.set_state(indoc! {"
3313 const a: B = (
3314 c(
3315 ˇ ˇ
3316 ˇ )
3317 );
3318 "});
3319 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3320 cx.assert_editor_state(indoc! {"
3321 const a: B = (
3322 c(
3323 ˇ
3324 ˇ)
3325 );
3326 "});
3327}
3328
3329#[gpui::test]
3330async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3331 init_test(cx, |settings| {
3332 settings.defaults.tab_size = NonZeroU32::new(3)
3333 });
3334
3335 let mut cx = EditorTestContext::new(cx).await;
3336 cx.set_state(indoc! {"
3337 ˇ
3338 \t ˇ
3339 \t ˇ
3340 \t ˇ
3341 \t \t\t \t \t\t \t\t \t \t ˇ
3342 "});
3343
3344 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3345 cx.assert_editor_state(indoc! {"
3346 ˇ
3347 \t ˇ
3348 \t ˇ
3349 \t ˇ
3350 \t \t\t \t \t\t \t\t \t \t ˇ
3351 "});
3352}
3353
3354#[gpui::test]
3355async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3356 init_test(cx, |settings| {
3357 settings.defaults.tab_size = NonZeroU32::new(4)
3358 });
3359
3360 let language = Arc::new(
3361 Language::new(
3362 LanguageConfig::default(),
3363 Some(tree_sitter_rust::LANGUAGE.into()),
3364 )
3365 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3366 .unwrap(),
3367 );
3368
3369 let mut cx = EditorTestContext::new(cx).await;
3370 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3371 cx.set_state(indoc! {"
3372 fn a() {
3373 if b {
3374 \t ˇc
3375 }
3376 }
3377 "});
3378
3379 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3380 cx.assert_editor_state(indoc! {"
3381 fn a() {
3382 if b {
3383 ˇc
3384 }
3385 }
3386 "});
3387}
3388
3389#[gpui::test]
3390async fn test_indent_outdent(cx: &mut TestAppContext) {
3391 init_test(cx, |settings| {
3392 settings.defaults.tab_size = NonZeroU32::new(4);
3393 });
3394
3395 let mut cx = EditorTestContext::new(cx).await;
3396
3397 cx.set_state(indoc! {"
3398 «oneˇ» «twoˇ»
3399 three
3400 four
3401 "});
3402 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3403 cx.assert_editor_state(indoc! {"
3404 «oneˇ» «twoˇ»
3405 three
3406 four
3407 "});
3408
3409 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3410 cx.assert_editor_state(indoc! {"
3411 «oneˇ» «twoˇ»
3412 three
3413 four
3414 "});
3415
3416 // select across line ending
3417 cx.set_state(indoc! {"
3418 one two
3419 t«hree
3420 ˇ» four
3421 "});
3422 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3423 cx.assert_editor_state(indoc! {"
3424 one two
3425 t«hree
3426 ˇ» four
3427 "});
3428
3429 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3430 cx.assert_editor_state(indoc! {"
3431 one two
3432 t«hree
3433 ˇ» four
3434 "});
3435
3436 // Ensure that indenting/outdenting works when the cursor is at column 0.
3437 cx.set_state(indoc! {"
3438 one two
3439 ˇthree
3440 four
3441 "});
3442 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3443 cx.assert_editor_state(indoc! {"
3444 one two
3445 ˇthree
3446 four
3447 "});
3448
3449 cx.set_state(indoc! {"
3450 one two
3451 ˇ three
3452 four
3453 "});
3454 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3455 cx.assert_editor_state(indoc! {"
3456 one two
3457 ˇthree
3458 four
3459 "});
3460}
3461
3462#[gpui::test]
3463async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3464 init_test(cx, |settings| {
3465 settings.defaults.hard_tabs = Some(true);
3466 });
3467
3468 let mut cx = EditorTestContext::new(cx).await;
3469
3470 // select two ranges on one line
3471 cx.set_state(indoc! {"
3472 «oneˇ» «twoˇ»
3473 three
3474 four
3475 "});
3476 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3477 cx.assert_editor_state(indoc! {"
3478 \t«oneˇ» «twoˇ»
3479 three
3480 four
3481 "});
3482 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3483 cx.assert_editor_state(indoc! {"
3484 \t\t«oneˇ» «twoˇ»
3485 three
3486 four
3487 "});
3488 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3489 cx.assert_editor_state(indoc! {"
3490 \t«oneˇ» «twoˇ»
3491 three
3492 four
3493 "});
3494 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3495 cx.assert_editor_state(indoc! {"
3496 «oneˇ» «twoˇ»
3497 three
3498 four
3499 "});
3500
3501 // select across a line ending
3502 cx.set_state(indoc! {"
3503 one two
3504 t«hree
3505 ˇ»four
3506 "});
3507 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3508 cx.assert_editor_state(indoc! {"
3509 one two
3510 \tt«hree
3511 ˇ»four
3512 "});
3513 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3514 cx.assert_editor_state(indoc! {"
3515 one two
3516 \t\tt«hree
3517 ˇ»four
3518 "});
3519 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3520 cx.assert_editor_state(indoc! {"
3521 one two
3522 \tt«hree
3523 ˇ»four
3524 "});
3525 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3526 cx.assert_editor_state(indoc! {"
3527 one two
3528 t«hree
3529 ˇ»four
3530 "});
3531
3532 // Ensure that indenting/outdenting works when the cursor is at column 0.
3533 cx.set_state(indoc! {"
3534 one two
3535 ˇthree
3536 four
3537 "});
3538 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3539 cx.assert_editor_state(indoc! {"
3540 one two
3541 ˇthree
3542 four
3543 "});
3544 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3545 cx.assert_editor_state(indoc! {"
3546 one two
3547 \tˇthree
3548 four
3549 "});
3550 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3551 cx.assert_editor_state(indoc! {"
3552 one two
3553 ˇthree
3554 four
3555 "});
3556}
3557
3558#[gpui::test]
3559fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3560 init_test(cx, |settings| {
3561 settings.languages.extend([
3562 (
3563 "TOML".into(),
3564 LanguageSettingsContent {
3565 tab_size: NonZeroU32::new(2),
3566 ..Default::default()
3567 },
3568 ),
3569 (
3570 "Rust".into(),
3571 LanguageSettingsContent {
3572 tab_size: NonZeroU32::new(4),
3573 ..Default::default()
3574 },
3575 ),
3576 ]);
3577 });
3578
3579 let toml_language = Arc::new(Language::new(
3580 LanguageConfig {
3581 name: "TOML".into(),
3582 ..Default::default()
3583 },
3584 None,
3585 ));
3586 let rust_language = Arc::new(Language::new(
3587 LanguageConfig {
3588 name: "Rust".into(),
3589 ..Default::default()
3590 },
3591 None,
3592 ));
3593
3594 let toml_buffer =
3595 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3596 let rust_buffer =
3597 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3598 let multibuffer = cx.new(|cx| {
3599 let mut multibuffer = MultiBuffer::new(ReadWrite);
3600 multibuffer.push_excerpts(
3601 toml_buffer.clone(),
3602 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
3603 cx,
3604 );
3605 multibuffer.push_excerpts(
3606 rust_buffer.clone(),
3607 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
3608 cx,
3609 );
3610 multibuffer
3611 });
3612
3613 cx.add_window(|window, cx| {
3614 let mut editor = build_editor(multibuffer, window, cx);
3615
3616 assert_eq!(
3617 editor.text(cx),
3618 indoc! {"
3619 a = 1
3620 b = 2
3621
3622 const c: usize = 3;
3623 "}
3624 );
3625
3626 select_ranges(
3627 &mut editor,
3628 indoc! {"
3629 «aˇ» = 1
3630 b = 2
3631
3632 «const c:ˇ» usize = 3;
3633 "},
3634 window,
3635 cx,
3636 );
3637
3638 editor.tab(&Tab, window, cx);
3639 assert_text_with_selections(
3640 &mut editor,
3641 indoc! {"
3642 «aˇ» = 1
3643 b = 2
3644
3645 «const c:ˇ» usize = 3;
3646 "},
3647 cx,
3648 );
3649 editor.backtab(&Backtab, window, cx);
3650 assert_text_with_selections(
3651 &mut editor,
3652 indoc! {"
3653 «aˇ» = 1
3654 b = 2
3655
3656 «const c:ˇ» usize = 3;
3657 "},
3658 cx,
3659 );
3660
3661 editor
3662 });
3663}
3664
3665#[gpui::test]
3666async fn test_backspace(cx: &mut TestAppContext) {
3667 init_test(cx, |_| {});
3668
3669 let mut cx = EditorTestContext::new(cx).await;
3670
3671 // Basic backspace
3672 cx.set_state(indoc! {"
3673 onˇe two three
3674 fou«rˇ» five six
3675 seven «ˇeight nine
3676 »ten
3677 "});
3678 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3679 cx.assert_editor_state(indoc! {"
3680 oˇe two three
3681 fouˇ five six
3682 seven ˇten
3683 "});
3684
3685 // Test backspace inside and around indents
3686 cx.set_state(indoc! {"
3687 zero
3688 ˇone
3689 ˇtwo
3690 ˇ ˇ ˇ three
3691 ˇ ˇ four
3692 "});
3693 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3694 cx.assert_editor_state(indoc! {"
3695 zero
3696 ˇone
3697 ˇtwo
3698 ˇ threeˇ four
3699 "});
3700}
3701
3702#[gpui::test]
3703async fn test_delete(cx: &mut TestAppContext) {
3704 init_test(cx, |_| {});
3705
3706 let mut cx = EditorTestContext::new(cx).await;
3707 cx.set_state(indoc! {"
3708 onˇe two three
3709 fou«rˇ» five six
3710 seven «ˇeight nine
3711 »ten
3712 "});
3713 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3714 cx.assert_editor_state(indoc! {"
3715 onˇ two three
3716 fouˇ five six
3717 seven ˇten
3718 "});
3719}
3720
3721#[gpui::test]
3722fn test_delete_line(cx: &mut TestAppContext) {
3723 init_test(cx, |_| {});
3724
3725 let editor = cx.add_window(|window, cx| {
3726 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3727 build_editor(buffer, window, cx)
3728 });
3729 _ = editor.update(cx, |editor, window, cx| {
3730 editor.change_selections(None, window, cx, |s| {
3731 s.select_display_ranges([
3732 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3733 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3734 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3735 ])
3736 });
3737 editor.delete_line(&DeleteLine, window, cx);
3738 assert_eq!(editor.display_text(cx), "ghi");
3739 assert_eq!(
3740 editor.selections.display_ranges(cx),
3741 vec![
3742 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3743 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3744 ]
3745 );
3746 });
3747
3748 let editor = cx.add_window(|window, cx| {
3749 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3750 build_editor(buffer, window, cx)
3751 });
3752 _ = editor.update(cx, |editor, window, cx| {
3753 editor.change_selections(None, window, cx, |s| {
3754 s.select_display_ranges([
3755 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3756 ])
3757 });
3758 editor.delete_line(&DeleteLine, window, cx);
3759 assert_eq!(editor.display_text(cx), "ghi\n");
3760 assert_eq!(
3761 editor.selections.display_ranges(cx),
3762 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3763 );
3764 });
3765}
3766
3767#[gpui::test]
3768fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3769 init_test(cx, |_| {});
3770
3771 cx.add_window(|window, cx| {
3772 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3773 let mut editor = build_editor(buffer.clone(), window, cx);
3774 let buffer = buffer.read(cx).as_singleton().unwrap();
3775
3776 assert_eq!(
3777 editor.selections.ranges::<Point>(cx),
3778 &[Point::new(0, 0)..Point::new(0, 0)]
3779 );
3780
3781 // When on single line, replace newline at end by space
3782 editor.join_lines(&JoinLines, window, cx);
3783 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3784 assert_eq!(
3785 editor.selections.ranges::<Point>(cx),
3786 &[Point::new(0, 3)..Point::new(0, 3)]
3787 );
3788
3789 // When multiple lines are selected, remove newlines that are spanned by the selection
3790 editor.change_selections(None, window, cx, |s| {
3791 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3792 });
3793 editor.join_lines(&JoinLines, window, cx);
3794 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3795 assert_eq!(
3796 editor.selections.ranges::<Point>(cx),
3797 &[Point::new(0, 11)..Point::new(0, 11)]
3798 );
3799
3800 // Undo should be transactional
3801 editor.undo(&Undo, window, cx);
3802 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3803 assert_eq!(
3804 editor.selections.ranges::<Point>(cx),
3805 &[Point::new(0, 5)..Point::new(2, 2)]
3806 );
3807
3808 // When joining an empty line don't insert a space
3809 editor.change_selections(None, window, cx, |s| {
3810 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3811 });
3812 editor.join_lines(&JoinLines, window, cx);
3813 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3814 assert_eq!(
3815 editor.selections.ranges::<Point>(cx),
3816 [Point::new(2, 3)..Point::new(2, 3)]
3817 );
3818
3819 // We can remove trailing newlines
3820 editor.join_lines(&JoinLines, window, cx);
3821 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3822 assert_eq!(
3823 editor.selections.ranges::<Point>(cx),
3824 [Point::new(2, 3)..Point::new(2, 3)]
3825 );
3826
3827 // We don't blow up on the last line
3828 editor.join_lines(&JoinLines, window, cx);
3829 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3830 assert_eq!(
3831 editor.selections.ranges::<Point>(cx),
3832 [Point::new(2, 3)..Point::new(2, 3)]
3833 );
3834
3835 // reset to test indentation
3836 editor.buffer.update(cx, |buffer, cx| {
3837 buffer.edit(
3838 [
3839 (Point::new(1, 0)..Point::new(1, 2), " "),
3840 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3841 ],
3842 None,
3843 cx,
3844 )
3845 });
3846
3847 // We remove any leading spaces
3848 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3849 editor.change_selections(None, window, cx, |s| {
3850 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3851 });
3852 editor.join_lines(&JoinLines, window, cx);
3853 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3854
3855 // We don't insert a space for a line containing only spaces
3856 editor.join_lines(&JoinLines, window, cx);
3857 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3858
3859 // We ignore any leading tabs
3860 editor.join_lines(&JoinLines, window, cx);
3861 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3862
3863 editor
3864 });
3865}
3866
3867#[gpui::test]
3868fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3869 init_test(cx, |_| {});
3870
3871 cx.add_window(|window, cx| {
3872 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3873 let mut editor = build_editor(buffer.clone(), window, cx);
3874 let buffer = buffer.read(cx).as_singleton().unwrap();
3875
3876 editor.change_selections(None, window, cx, |s| {
3877 s.select_ranges([
3878 Point::new(0, 2)..Point::new(1, 1),
3879 Point::new(1, 2)..Point::new(1, 2),
3880 Point::new(3, 1)..Point::new(3, 2),
3881 ])
3882 });
3883
3884 editor.join_lines(&JoinLines, window, cx);
3885 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
3886
3887 assert_eq!(
3888 editor.selections.ranges::<Point>(cx),
3889 [
3890 Point::new(0, 7)..Point::new(0, 7),
3891 Point::new(1, 3)..Point::new(1, 3)
3892 ]
3893 );
3894 editor
3895 });
3896}
3897
3898#[gpui::test]
3899async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
3900 init_test(cx, |_| {});
3901
3902 let mut cx = EditorTestContext::new(cx).await;
3903
3904 let diff_base = r#"
3905 Line 0
3906 Line 1
3907 Line 2
3908 Line 3
3909 "#
3910 .unindent();
3911
3912 cx.set_state(
3913 &r#"
3914 ˇLine 0
3915 Line 1
3916 Line 2
3917 Line 3
3918 "#
3919 .unindent(),
3920 );
3921
3922 cx.set_head_text(&diff_base);
3923 executor.run_until_parked();
3924
3925 // Join lines
3926 cx.update_editor(|editor, window, cx| {
3927 editor.join_lines(&JoinLines, window, cx);
3928 });
3929 executor.run_until_parked();
3930
3931 cx.assert_editor_state(
3932 &r#"
3933 Line 0ˇ Line 1
3934 Line 2
3935 Line 3
3936 "#
3937 .unindent(),
3938 );
3939 // Join again
3940 cx.update_editor(|editor, window, cx| {
3941 editor.join_lines(&JoinLines, window, cx);
3942 });
3943 executor.run_until_parked();
3944
3945 cx.assert_editor_state(
3946 &r#"
3947 Line 0 Line 1ˇ Line 2
3948 Line 3
3949 "#
3950 .unindent(),
3951 );
3952}
3953
3954#[gpui::test]
3955async fn test_custom_newlines_cause_no_false_positive_diffs(
3956 executor: BackgroundExecutor,
3957 cx: &mut TestAppContext,
3958) {
3959 init_test(cx, |_| {});
3960 let mut cx = EditorTestContext::new(cx).await;
3961 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
3962 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
3963 executor.run_until_parked();
3964
3965 cx.update_editor(|editor, window, cx| {
3966 let snapshot = editor.snapshot(window, cx);
3967 assert_eq!(
3968 snapshot
3969 .buffer_snapshot
3970 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
3971 .collect::<Vec<_>>(),
3972 Vec::new(),
3973 "Should not have any diffs for files with custom newlines"
3974 );
3975 });
3976}
3977
3978#[gpui::test]
3979async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
3980 init_test(cx, |_| {});
3981
3982 let mut cx = EditorTestContext::new(cx).await;
3983
3984 // Test sort_lines_case_insensitive()
3985 cx.set_state(indoc! {"
3986 «z
3987 y
3988 x
3989 Z
3990 Y
3991 Xˇ»
3992 "});
3993 cx.update_editor(|e, window, cx| {
3994 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
3995 });
3996 cx.assert_editor_state(indoc! {"
3997 «x
3998 X
3999 y
4000 Y
4001 z
4002 Zˇ»
4003 "});
4004
4005 // Test reverse_lines()
4006 cx.set_state(indoc! {"
4007 «5
4008 4
4009 3
4010 2
4011 1ˇ»
4012 "});
4013 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4014 cx.assert_editor_state(indoc! {"
4015 «1
4016 2
4017 3
4018 4
4019 5ˇ»
4020 "});
4021
4022 // Skip testing shuffle_line()
4023
4024 // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive()
4025 // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines)
4026
4027 // Don't manipulate when cursor is on single line, but expand the selection
4028 cx.set_state(indoc! {"
4029 ddˇdd
4030 ccc
4031 bb
4032 a
4033 "});
4034 cx.update_editor(|e, window, cx| {
4035 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4036 });
4037 cx.assert_editor_state(indoc! {"
4038 «ddddˇ»
4039 ccc
4040 bb
4041 a
4042 "});
4043
4044 // Basic manipulate case
4045 // Start selection moves to column 0
4046 // End of selection shrinks to fit shorter line
4047 cx.set_state(indoc! {"
4048 dd«d
4049 ccc
4050 bb
4051 aaaaaˇ»
4052 "});
4053 cx.update_editor(|e, window, cx| {
4054 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4055 });
4056 cx.assert_editor_state(indoc! {"
4057 «aaaaa
4058 bb
4059 ccc
4060 dddˇ»
4061 "});
4062
4063 // Manipulate case with newlines
4064 cx.set_state(indoc! {"
4065 dd«d
4066 ccc
4067
4068 bb
4069 aaaaa
4070
4071 ˇ»
4072 "});
4073 cx.update_editor(|e, window, cx| {
4074 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4075 });
4076 cx.assert_editor_state(indoc! {"
4077 «
4078
4079 aaaaa
4080 bb
4081 ccc
4082 dddˇ»
4083
4084 "});
4085
4086 // Adding new line
4087 cx.set_state(indoc! {"
4088 aa«a
4089 bbˇ»b
4090 "});
4091 cx.update_editor(|e, window, cx| {
4092 e.manipulate_lines(window, cx, |lines| lines.push("added_line"))
4093 });
4094 cx.assert_editor_state(indoc! {"
4095 «aaa
4096 bbb
4097 added_lineˇ»
4098 "});
4099
4100 // Removing line
4101 cx.set_state(indoc! {"
4102 aa«a
4103 bbbˇ»
4104 "});
4105 cx.update_editor(|e, window, cx| {
4106 e.manipulate_lines(window, cx, |lines| {
4107 lines.pop();
4108 })
4109 });
4110 cx.assert_editor_state(indoc! {"
4111 «aaaˇ»
4112 "});
4113
4114 // Removing all lines
4115 cx.set_state(indoc! {"
4116 aa«a
4117 bbbˇ»
4118 "});
4119 cx.update_editor(|e, window, cx| {
4120 e.manipulate_lines(window, cx, |lines| {
4121 lines.drain(..);
4122 })
4123 });
4124 cx.assert_editor_state(indoc! {"
4125 ˇ
4126 "});
4127}
4128
4129#[gpui::test]
4130async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4131 init_test(cx, |_| {});
4132
4133 let mut cx = EditorTestContext::new(cx).await;
4134
4135 // Consider continuous selection as single selection
4136 cx.set_state(indoc! {"
4137 Aaa«aa
4138 cˇ»c«c
4139 bb
4140 aaaˇ»aa
4141 "});
4142 cx.update_editor(|e, window, cx| {
4143 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4144 });
4145 cx.assert_editor_state(indoc! {"
4146 «Aaaaa
4147 ccc
4148 bb
4149 aaaaaˇ»
4150 "});
4151
4152 cx.set_state(indoc! {"
4153 Aaa«aa
4154 cˇ»c«c
4155 bb
4156 aaaˇ»aa
4157 "});
4158 cx.update_editor(|e, window, cx| {
4159 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4160 });
4161 cx.assert_editor_state(indoc! {"
4162 «Aaaaa
4163 ccc
4164 bbˇ»
4165 "});
4166
4167 // Consider non continuous selection as distinct dedup operations
4168 cx.set_state(indoc! {"
4169 «aaaaa
4170 bb
4171 aaaaa
4172 aaaaaˇ»
4173
4174 aaa«aaˇ»
4175 "});
4176 cx.update_editor(|e, window, cx| {
4177 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4178 });
4179 cx.assert_editor_state(indoc! {"
4180 «aaaaa
4181 bbˇ»
4182
4183 «aaaaaˇ»
4184 "});
4185}
4186
4187#[gpui::test]
4188async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4189 init_test(cx, |_| {});
4190
4191 let mut cx = EditorTestContext::new(cx).await;
4192
4193 cx.set_state(indoc! {"
4194 «Aaa
4195 aAa
4196 Aaaˇ»
4197 "});
4198 cx.update_editor(|e, window, cx| {
4199 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4200 });
4201 cx.assert_editor_state(indoc! {"
4202 «Aaa
4203 aAaˇ»
4204 "});
4205
4206 cx.set_state(indoc! {"
4207 «Aaa
4208 aAa
4209 aaAˇ»
4210 "});
4211 cx.update_editor(|e, window, cx| {
4212 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4213 });
4214 cx.assert_editor_state(indoc! {"
4215 «Aaaˇ»
4216 "});
4217}
4218
4219#[gpui::test]
4220async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
4221 init_test(cx, |_| {});
4222
4223 let mut cx = EditorTestContext::new(cx).await;
4224
4225 // Manipulate with multiple selections on a single line
4226 cx.set_state(indoc! {"
4227 dd«dd
4228 cˇ»c«c
4229 bb
4230 aaaˇ»aa
4231 "});
4232 cx.update_editor(|e, window, cx| {
4233 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4234 });
4235 cx.assert_editor_state(indoc! {"
4236 «aaaaa
4237 bb
4238 ccc
4239 ddddˇ»
4240 "});
4241
4242 // Manipulate with multiple disjoin selections
4243 cx.set_state(indoc! {"
4244 5«
4245 4
4246 3
4247 2
4248 1ˇ»
4249
4250 dd«dd
4251 ccc
4252 bb
4253 aaaˇ»aa
4254 "});
4255 cx.update_editor(|e, window, cx| {
4256 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4257 });
4258 cx.assert_editor_state(indoc! {"
4259 «1
4260 2
4261 3
4262 4
4263 5ˇ»
4264
4265 «aaaaa
4266 bb
4267 ccc
4268 ddddˇ»
4269 "});
4270
4271 // Adding lines on each selection
4272 cx.set_state(indoc! {"
4273 2«
4274 1ˇ»
4275
4276 bb«bb
4277 aaaˇ»aa
4278 "});
4279 cx.update_editor(|e, window, cx| {
4280 e.manipulate_lines(window, cx, |lines| lines.push("added line"))
4281 });
4282 cx.assert_editor_state(indoc! {"
4283 «2
4284 1
4285 added lineˇ»
4286
4287 «bbbb
4288 aaaaa
4289 added lineˇ»
4290 "});
4291
4292 // Removing lines on each selection
4293 cx.set_state(indoc! {"
4294 2«
4295 1ˇ»
4296
4297 bb«bb
4298 aaaˇ»aa
4299 "});
4300 cx.update_editor(|e, window, cx| {
4301 e.manipulate_lines(window, cx, |lines| {
4302 lines.pop();
4303 })
4304 });
4305 cx.assert_editor_state(indoc! {"
4306 «2ˇ»
4307
4308 «bbbbˇ»
4309 "});
4310}
4311
4312#[gpui::test]
4313async fn test_toggle_case(cx: &mut TestAppContext) {
4314 init_test(cx, |_| {});
4315
4316 let mut cx = EditorTestContext::new(cx).await;
4317
4318 // If all lower case -> upper case
4319 cx.set_state(indoc! {"
4320 «hello worldˇ»
4321 "});
4322 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4323 cx.assert_editor_state(indoc! {"
4324 «HELLO WORLDˇ»
4325 "});
4326
4327 // If all upper case -> lower case
4328 cx.set_state(indoc! {"
4329 «HELLO WORLDˇ»
4330 "});
4331 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4332 cx.assert_editor_state(indoc! {"
4333 «hello worldˇ»
4334 "});
4335
4336 // If any upper case characters are identified -> lower case
4337 // This matches JetBrains IDEs
4338 cx.set_state(indoc! {"
4339 «hEllo worldˇ»
4340 "});
4341 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4342 cx.assert_editor_state(indoc! {"
4343 «hello worldˇ»
4344 "});
4345}
4346
4347#[gpui::test]
4348async fn test_manipulate_text(cx: &mut TestAppContext) {
4349 init_test(cx, |_| {});
4350
4351 let mut cx = EditorTestContext::new(cx).await;
4352
4353 // Test convert_to_upper_case()
4354 cx.set_state(indoc! {"
4355 «hello worldˇ»
4356 "});
4357 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4358 cx.assert_editor_state(indoc! {"
4359 «HELLO WORLDˇ»
4360 "});
4361
4362 // Test convert_to_lower_case()
4363 cx.set_state(indoc! {"
4364 «HELLO WORLDˇ»
4365 "});
4366 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
4367 cx.assert_editor_state(indoc! {"
4368 «hello worldˇ»
4369 "});
4370
4371 // Test multiple line, single selection case
4372 cx.set_state(indoc! {"
4373 «The quick brown
4374 fox jumps over
4375 the lazy dogˇ»
4376 "});
4377 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
4378 cx.assert_editor_state(indoc! {"
4379 «The Quick Brown
4380 Fox Jumps Over
4381 The Lazy Dogˇ»
4382 "});
4383
4384 // Test multiple line, single selection case
4385 cx.set_state(indoc! {"
4386 «The quick brown
4387 fox jumps over
4388 the lazy dogˇ»
4389 "});
4390 cx.update_editor(|e, window, cx| {
4391 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
4392 });
4393 cx.assert_editor_state(indoc! {"
4394 «TheQuickBrown
4395 FoxJumpsOver
4396 TheLazyDogˇ»
4397 "});
4398
4399 // From here on out, test more complex cases of manipulate_text()
4400
4401 // Test no selection case - should affect words cursors are in
4402 // Cursor at beginning, middle, and end of word
4403 cx.set_state(indoc! {"
4404 ˇhello big beauˇtiful worldˇ
4405 "});
4406 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4407 cx.assert_editor_state(indoc! {"
4408 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
4409 "});
4410
4411 // Test multiple selections on a single line and across multiple lines
4412 cx.set_state(indoc! {"
4413 «Theˇ» quick «brown
4414 foxˇ» jumps «overˇ»
4415 the «lazyˇ» dog
4416 "});
4417 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4418 cx.assert_editor_state(indoc! {"
4419 «THEˇ» quick «BROWN
4420 FOXˇ» jumps «OVERˇ»
4421 the «LAZYˇ» dog
4422 "});
4423
4424 // Test case where text length grows
4425 cx.set_state(indoc! {"
4426 «tschüߡ»
4427 "});
4428 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4429 cx.assert_editor_state(indoc! {"
4430 «TSCHÜSSˇ»
4431 "});
4432
4433 // Test to make sure we don't crash when text shrinks
4434 cx.set_state(indoc! {"
4435 aaa_bbbˇ
4436 "});
4437 cx.update_editor(|e, window, cx| {
4438 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4439 });
4440 cx.assert_editor_state(indoc! {"
4441 «aaaBbbˇ»
4442 "});
4443
4444 // Test to make sure we all aware of the fact that each word can grow and shrink
4445 // Final selections should be aware of this fact
4446 cx.set_state(indoc! {"
4447 aaa_bˇbb bbˇb_ccc ˇccc_ddd
4448 "});
4449 cx.update_editor(|e, window, cx| {
4450 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4451 });
4452 cx.assert_editor_state(indoc! {"
4453 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
4454 "});
4455
4456 cx.set_state(indoc! {"
4457 «hElLo, WoRld!ˇ»
4458 "});
4459 cx.update_editor(|e, window, cx| {
4460 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
4461 });
4462 cx.assert_editor_state(indoc! {"
4463 «HeLlO, wOrLD!ˇ»
4464 "});
4465}
4466
4467#[gpui::test]
4468fn test_duplicate_line(cx: &mut TestAppContext) {
4469 init_test(cx, |_| {});
4470
4471 let editor = cx.add_window(|window, cx| {
4472 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4473 build_editor(buffer, window, cx)
4474 });
4475 _ = editor.update(cx, |editor, window, cx| {
4476 editor.change_selections(None, window, cx, |s| {
4477 s.select_display_ranges([
4478 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4479 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4480 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4481 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4482 ])
4483 });
4484 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4485 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4486 assert_eq!(
4487 editor.selections.display_ranges(cx),
4488 vec![
4489 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4490 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
4491 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4492 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4493 ]
4494 );
4495 });
4496
4497 let editor = cx.add_window(|window, cx| {
4498 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4499 build_editor(buffer, window, cx)
4500 });
4501 _ = editor.update(cx, |editor, window, cx| {
4502 editor.change_selections(None, window, cx, |s| {
4503 s.select_display_ranges([
4504 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4505 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4506 ])
4507 });
4508 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4509 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4510 assert_eq!(
4511 editor.selections.display_ranges(cx),
4512 vec![
4513 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
4514 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
4515 ]
4516 );
4517 });
4518
4519 // With `move_upwards` the selections stay in place, except for
4520 // the lines inserted above them
4521 let editor = cx.add_window(|window, cx| {
4522 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4523 build_editor(buffer, window, cx)
4524 });
4525 _ = editor.update(cx, |editor, window, cx| {
4526 editor.change_selections(None, window, cx, |s| {
4527 s.select_display_ranges([
4528 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4529 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4530 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4531 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4532 ])
4533 });
4534 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4535 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4536 assert_eq!(
4537 editor.selections.display_ranges(cx),
4538 vec![
4539 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4540 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4541 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4542 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4543 ]
4544 );
4545 });
4546
4547 let editor = cx.add_window(|window, cx| {
4548 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4549 build_editor(buffer, window, cx)
4550 });
4551 _ = editor.update(cx, |editor, window, cx| {
4552 editor.change_selections(None, window, cx, |s| {
4553 s.select_display_ranges([
4554 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4555 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4556 ])
4557 });
4558 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4559 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4560 assert_eq!(
4561 editor.selections.display_ranges(cx),
4562 vec![
4563 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4564 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4565 ]
4566 );
4567 });
4568
4569 let editor = cx.add_window(|window, cx| {
4570 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4571 build_editor(buffer, window, cx)
4572 });
4573 _ = editor.update(cx, |editor, window, cx| {
4574 editor.change_selections(None, window, cx, |s| {
4575 s.select_display_ranges([
4576 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4577 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4578 ])
4579 });
4580 editor.duplicate_selection(&DuplicateSelection, window, cx);
4581 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4582 assert_eq!(
4583 editor.selections.display_ranges(cx),
4584 vec![
4585 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4586 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4587 ]
4588 );
4589 });
4590}
4591
4592#[gpui::test]
4593fn test_move_line_up_down(cx: &mut TestAppContext) {
4594 init_test(cx, |_| {});
4595
4596 let editor = cx.add_window(|window, cx| {
4597 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4598 build_editor(buffer, window, cx)
4599 });
4600 _ = editor.update(cx, |editor, window, cx| {
4601 editor.fold_creases(
4602 vec![
4603 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4604 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4605 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4606 ],
4607 true,
4608 window,
4609 cx,
4610 );
4611 editor.change_selections(None, window, cx, |s| {
4612 s.select_display_ranges([
4613 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4614 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4615 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4616 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
4617 ])
4618 });
4619 assert_eq!(
4620 editor.display_text(cx),
4621 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
4622 );
4623
4624 editor.move_line_up(&MoveLineUp, window, cx);
4625 assert_eq!(
4626 editor.display_text(cx),
4627 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
4628 );
4629 assert_eq!(
4630 editor.selections.display_ranges(cx),
4631 vec![
4632 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4633 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4634 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4635 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4636 ]
4637 );
4638 });
4639
4640 _ = editor.update(cx, |editor, window, cx| {
4641 editor.move_line_down(&MoveLineDown, window, cx);
4642 assert_eq!(
4643 editor.display_text(cx),
4644 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
4645 );
4646 assert_eq!(
4647 editor.selections.display_ranges(cx),
4648 vec![
4649 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4650 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4651 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4652 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4653 ]
4654 );
4655 });
4656
4657 _ = editor.update(cx, |editor, window, cx| {
4658 editor.move_line_down(&MoveLineDown, window, cx);
4659 assert_eq!(
4660 editor.display_text(cx),
4661 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
4662 );
4663 assert_eq!(
4664 editor.selections.display_ranges(cx),
4665 vec![
4666 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4667 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4668 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4669 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4670 ]
4671 );
4672 });
4673
4674 _ = editor.update(cx, |editor, window, cx| {
4675 editor.move_line_up(&MoveLineUp, window, cx);
4676 assert_eq!(
4677 editor.display_text(cx),
4678 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
4679 );
4680 assert_eq!(
4681 editor.selections.display_ranges(cx),
4682 vec![
4683 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4684 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4685 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4686 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4687 ]
4688 );
4689 });
4690}
4691
4692#[gpui::test]
4693fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
4694 init_test(cx, |_| {});
4695
4696 let editor = cx.add_window(|window, cx| {
4697 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4698 build_editor(buffer, window, cx)
4699 });
4700 _ = editor.update(cx, |editor, window, cx| {
4701 let snapshot = editor.buffer.read(cx).snapshot(cx);
4702 editor.insert_blocks(
4703 [BlockProperties {
4704 style: BlockStyle::Fixed,
4705 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
4706 height: Some(1),
4707 render: Arc::new(|_| div().into_any()),
4708 priority: 0,
4709 render_in_minimap: true,
4710 }],
4711 Some(Autoscroll::fit()),
4712 cx,
4713 );
4714 editor.change_selections(None, window, cx, |s| {
4715 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
4716 });
4717 editor.move_line_down(&MoveLineDown, window, cx);
4718 });
4719}
4720
4721#[gpui::test]
4722async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
4723 init_test(cx, |_| {});
4724
4725 let mut cx = EditorTestContext::new(cx).await;
4726 cx.set_state(
4727 &"
4728 ˇzero
4729 one
4730 two
4731 three
4732 four
4733 five
4734 "
4735 .unindent(),
4736 );
4737
4738 // Create a four-line block that replaces three lines of text.
4739 cx.update_editor(|editor, window, cx| {
4740 let snapshot = editor.snapshot(window, cx);
4741 let snapshot = &snapshot.buffer_snapshot;
4742 let placement = BlockPlacement::Replace(
4743 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
4744 );
4745 editor.insert_blocks(
4746 [BlockProperties {
4747 placement,
4748 height: Some(4),
4749 style: BlockStyle::Sticky,
4750 render: Arc::new(|_| gpui::div().into_any_element()),
4751 priority: 0,
4752 render_in_minimap: true,
4753 }],
4754 None,
4755 cx,
4756 );
4757 });
4758
4759 // Move down so that the cursor touches the block.
4760 cx.update_editor(|editor, window, cx| {
4761 editor.move_down(&Default::default(), window, cx);
4762 });
4763 cx.assert_editor_state(
4764 &"
4765 zero
4766 «one
4767 two
4768 threeˇ»
4769 four
4770 five
4771 "
4772 .unindent(),
4773 );
4774
4775 // Move down past the block.
4776 cx.update_editor(|editor, window, cx| {
4777 editor.move_down(&Default::default(), window, cx);
4778 });
4779 cx.assert_editor_state(
4780 &"
4781 zero
4782 one
4783 two
4784 three
4785 ˇfour
4786 five
4787 "
4788 .unindent(),
4789 );
4790}
4791
4792#[gpui::test]
4793fn test_transpose(cx: &mut TestAppContext) {
4794 init_test(cx, |_| {});
4795
4796 _ = cx.add_window(|window, cx| {
4797 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
4798 editor.set_style(EditorStyle::default(), window, cx);
4799 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
4800 editor.transpose(&Default::default(), window, cx);
4801 assert_eq!(editor.text(cx), "bac");
4802 assert_eq!(editor.selections.ranges(cx), [2..2]);
4803
4804 editor.transpose(&Default::default(), window, cx);
4805 assert_eq!(editor.text(cx), "bca");
4806 assert_eq!(editor.selections.ranges(cx), [3..3]);
4807
4808 editor.transpose(&Default::default(), window, cx);
4809 assert_eq!(editor.text(cx), "bac");
4810 assert_eq!(editor.selections.ranges(cx), [3..3]);
4811
4812 editor
4813 });
4814
4815 _ = cx.add_window(|window, cx| {
4816 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4817 editor.set_style(EditorStyle::default(), window, cx);
4818 editor.change_selections(None, window, cx, |s| s.select_ranges([3..3]));
4819 editor.transpose(&Default::default(), window, cx);
4820 assert_eq!(editor.text(cx), "acb\nde");
4821 assert_eq!(editor.selections.ranges(cx), [3..3]);
4822
4823 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4824 editor.transpose(&Default::default(), window, cx);
4825 assert_eq!(editor.text(cx), "acbd\ne");
4826 assert_eq!(editor.selections.ranges(cx), [5..5]);
4827
4828 editor.transpose(&Default::default(), window, cx);
4829 assert_eq!(editor.text(cx), "acbde\n");
4830 assert_eq!(editor.selections.ranges(cx), [6..6]);
4831
4832 editor.transpose(&Default::default(), window, cx);
4833 assert_eq!(editor.text(cx), "acbd\ne");
4834 assert_eq!(editor.selections.ranges(cx), [6..6]);
4835
4836 editor
4837 });
4838
4839 _ = cx.add_window(|window, cx| {
4840 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4841 editor.set_style(EditorStyle::default(), window, cx);
4842 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1, 2..2, 4..4]));
4843 editor.transpose(&Default::default(), window, cx);
4844 assert_eq!(editor.text(cx), "bacd\ne");
4845 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
4846
4847 editor.transpose(&Default::default(), window, cx);
4848 assert_eq!(editor.text(cx), "bcade\n");
4849 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
4850
4851 editor.transpose(&Default::default(), window, cx);
4852 assert_eq!(editor.text(cx), "bcda\ne");
4853 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4854
4855 editor.transpose(&Default::default(), window, cx);
4856 assert_eq!(editor.text(cx), "bcade\n");
4857 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4858
4859 editor.transpose(&Default::default(), window, cx);
4860 assert_eq!(editor.text(cx), "bcaed\n");
4861 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
4862
4863 editor
4864 });
4865
4866 _ = cx.add_window(|window, cx| {
4867 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
4868 editor.set_style(EditorStyle::default(), window, cx);
4869 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4870 editor.transpose(&Default::default(), window, cx);
4871 assert_eq!(editor.text(cx), "🏀🍐✋");
4872 assert_eq!(editor.selections.ranges(cx), [8..8]);
4873
4874 editor.transpose(&Default::default(), window, cx);
4875 assert_eq!(editor.text(cx), "🏀✋🍐");
4876 assert_eq!(editor.selections.ranges(cx), [11..11]);
4877
4878 editor.transpose(&Default::default(), window, cx);
4879 assert_eq!(editor.text(cx), "🏀🍐✋");
4880 assert_eq!(editor.selections.ranges(cx), [11..11]);
4881
4882 editor
4883 });
4884}
4885
4886#[gpui::test]
4887async fn test_rewrap(cx: &mut TestAppContext) {
4888 init_test(cx, |settings| {
4889 settings.languages.extend([
4890 (
4891 "Markdown".into(),
4892 LanguageSettingsContent {
4893 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4894 ..Default::default()
4895 },
4896 ),
4897 (
4898 "Plain Text".into(),
4899 LanguageSettingsContent {
4900 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4901 ..Default::default()
4902 },
4903 ),
4904 ])
4905 });
4906
4907 let mut cx = EditorTestContext::new(cx).await;
4908
4909 let language_with_c_comments = Arc::new(Language::new(
4910 LanguageConfig {
4911 line_comments: vec!["// ".into()],
4912 ..LanguageConfig::default()
4913 },
4914 None,
4915 ));
4916 let language_with_pound_comments = Arc::new(Language::new(
4917 LanguageConfig {
4918 line_comments: vec!["# ".into()],
4919 ..LanguageConfig::default()
4920 },
4921 None,
4922 ));
4923 let markdown_language = Arc::new(Language::new(
4924 LanguageConfig {
4925 name: "Markdown".into(),
4926 ..LanguageConfig::default()
4927 },
4928 None,
4929 ));
4930 let language_with_doc_comments = Arc::new(Language::new(
4931 LanguageConfig {
4932 line_comments: vec!["// ".into(), "/// ".into()],
4933 ..LanguageConfig::default()
4934 },
4935 Some(tree_sitter_rust::LANGUAGE.into()),
4936 ));
4937
4938 let plaintext_language = Arc::new(Language::new(
4939 LanguageConfig {
4940 name: "Plain Text".into(),
4941 ..LanguageConfig::default()
4942 },
4943 None,
4944 ));
4945
4946 assert_rewrap(
4947 indoc! {"
4948 // ˇ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.
4949 "},
4950 indoc! {"
4951 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4952 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4953 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4954 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4955 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4956 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4957 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4958 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4959 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4960 // porttitor id. Aliquam id accumsan eros.
4961 "},
4962 language_with_c_comments.clone(),
4963 &mut cx,
4964 );
4965
4966 // Test that rewrapping works inside of a selection
4967 assert_rewrap(
4968 indoc! {"
4969 «// 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.ˇ»
4970 "},
4971 indoc! {"
4972 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4973 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4974 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4975 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4976 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4977 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4978 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4979 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4980 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4981 // porttitor id. Aliquam id accumsan eros.ˇ»
4982 "},
4983 language_with_c_comments.clone(),
4984 &mut cx,
4985 );
4986
4987 // Test that cursors that expand to the same region are collapsed.
4988 assert_rewrap(
4989 indoc! {"
4990 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4991 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4992 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4993 // ˇ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.
4994 "},
4995 indoc! {"
4996 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4997 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4998 // auctor, eu lacinia sapien scelerisque. ˇVivamus sit amet neque et quam
4999 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
5000 // Pellentesque odio lectus, iaculis ac volutpat et, ˇblandit quis urna. Sed
5001 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
5002 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
5003 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
5004 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
5005 // porttitor id. Aliquam id accumsan eros.
5006 "},
5007 language_with_c_comments.clone(),
5008 &mut cx,
5009 );
5010
5011 // Test that non-contiguous selections are treated separately.
5012 assert_rewrap(
5013 indoc! {"
5014 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
5015 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
5016 //
5017 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
5018 // ˇ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.
5019 "},
5020 indoc! {"
5021 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
5022 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
5023 // auctor, eu lacinia sapien scelerisque.
5024 //
5025 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas
5026 // tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
5027 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec
5028 // molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque
5029 // nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas
5030 // porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id
5031 // vulputate turpis porttitor id. Aliquam id accumsan eros.
5032 "},
5033 language_with_c_comments.clone(),
5034 &mut cx,
5035 );
5036
5037 // Test that different comment prefixes are supported.
5038 assert_rewrap(
5039 indoc! {"
5040 # ˇ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.
5041 "},
5042 indoc! {"
5043 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
5044 # purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
5045 # eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
5046 # hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
5047 # lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit
5048 # amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet
5049 # in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur
5050 # adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis.
5051 # Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id
5052 # accumsan eros.
5053 "},
5054 language_with_pound_comments.clone(),
5055 &mut cx,
5056 );
5057
5058 // Test that rewrapping is ignored outside of comments in most languages.
5059 assert_rewrap(
5060 indoc! {"
5061 /// Adds two numbers.
5062 /// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
5063 fn add(a: u32, b: u32) -> u32 {
5064 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ˇ
5065 }
5066 "},
5067 indoc! {"
5068 /// Adds two numbers. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
5069 /// Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
5070 fn add(a: u32, b: u32) -> u32 {
5071 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ˇ
5072 }
5073 "},
5074 language_with_doc_comments.clone(),
5075 &mut cx,
5076 );
5077
5078 // Test that rewrapping works in Markdown and Plain Text languages.
5079 assert_rewrap(
5080 indoc! {"
5081 # Hello
5082
5083 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.
5084 "},
5085 indoc! {"
5086 # Hello
5087
5088 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
5089 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
5090 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
5091 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
5092 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
5093 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
5094 Integer sit amet scelerisque nisi.
5095 "},
5096 markdown_language,
5097 &mut cx,
5098 );
5099
5100 assert_rewrap(
5101 indoc! {"
5102 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.
5103 "},
5104 indoc! {"
5105 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
5106 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
5107 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
5108 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
5109 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
5110 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
5111 Integer sit amet scelerisque nisi.
5112 "},
5113 plaintext_language.clone(),
5114 &mut cx,
5115 );
5116
5117 // Test rewrapping unaligned comments in a selection.
5118 assert_rewrap(
5119 indoc! {"
5120 fn foo() {
5121 if true {
5122 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
5123 // Praesent semper egestas tellus id dignissim.ˇ»
5124 do_something();
5125 } else {
5126 //
5127 }
5128 }
5129 "},
5130 indoc! {"
5131 fn foo() {
5132 if true {
5133 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
5134 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
5135 // egestas tellus id dignissim.ˇ»
5136 do_something();
5137 } else {
5138 //
5139 }
5140 }
5141 "},
5142 language_with_doc_comments.clone(),
5143 &mut cx,
5144 );
5145
5146 assert_rewrap(
5147 indoc! {"
5148 fn foo() {
5149 if true {
5150 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
5151 // Praesent semper egestas tellus id dignissim.»
5152 do_something();
5153 } else {
5154 //
5155 }
5156
5157 }
5158 "},
5159 indoc! {"
5160 fn foo() {
5161 if true {
5162 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
5163 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
5164 // egestas tellus id dignissim.»
5165 do_something();
5166 } else {
5167 //
5168 }
5169
5170 }
5171 "},
5172 language_with_doc_comments.clone(),
5173 &mut cx,
5174 );
5175
5176 assert_rewrap(
5177 indoc! {"
5178 «ˇ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
5179
5180 two»
5181
5182 three
5183
5184 «ˇ\t
5185
5186 four four four four four four four four four four four four four four four four four four four four»
5187
5188 «ˇfive five five five five five five five five five five five five five five five five five five five
5189 \t»
5190 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
5191 "},
5192 indoc! {"
5193 «ˇone one one one one one one one one one one one one one one one one one one one
5194 one one one one one
5195
5196 two»
5197
5198 three
5199
5200 «ˇ\t
5201
5202 four four four four four four four four four four four four four four four four
5203 four four four four»
5204
5205 «ˇfive five five five five five five five five five five five five five five five
5206 five five five five
5207 \t»
5208 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
5209 "},
5210 plaintext_language.clone(),
5211 &mut cx,
5212 );
5213
5214 assert_rewrap(
5215 indoc! {"
5216 //ˇ 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
5217 //ˇ
5218 //ˇ 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
5219 //ˇ short short short
5220 int main(void) {
5221 return 17;
5222 }
5223 "},
5224 indoc! {"
5225 //ˇ long long long long long long long long long long long long long long long
5226 // long long long long long long long long long long long long long
5227 //ˇ
5228 //ˇ long long long long long long long long long long long long long long long
5229 //ˇ long long long long long long long long long long long long long short short
5230 // short
5231 int main(void) {
5232 return 17;
5233 }
5234 "},
5235 language_with_c_comments,
5236 &mut cx,
5237 );
5238
5239 #[track_caller]
5240 fn assert_rewrap(
5241 unwrapped_text: &str,
5242 wrapped_text: &str,
5243 language: Arc<Language>,
5244 cx: &mut EditorTestContext,
5245 ) {
5246 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
5247 cx.set_state(unwrapped_text);
5248 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
5249 cx.assert_editor_state(wrapped_text);
5250 }
5251}
5252
5253#[gpui::test]
5254async fn test_hard_wrap(cx: &mut TestAppContext) {
5255 init_test(cx, |_| {});
5256 let mut cx = EditorTestContext::new(cx).await;
5257
5258 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
5259 cx.update_editor(|editor, _, cx| {
5260 editor.set_hard_wrap(Some(14), cx);
5261 });
5262
5263 cx.set_state(indoc!(
5264 "
5265 one two three ˇ
5266 "
5267 ));
5268 cx.simulate_input("four");
5269 cx.run_until_parked();
5270
5271 cx.assert_editor_state(indoc!(
5272 "
5273 one two three
5274 fourˇ
5275 "
5276 ));
5277
5278 cx.update_editor(|editor, window, cx| {
5279 editor.newline(&Default::default(), window, cx);
5280 });
5281 cx.run_until_parked();
5282 cx.assert_editor_state(indoc!(
5283 "
5284 one two three
5285 four
5286 ˇ
5287 "
5288 ));
5289
5290 cx.simulate_input("five");
5291 cx.run_until_parked();
5292 cx.assert_editor_state(indoc!(
5293 "
5294 one two three
5295 four
5296 fiveˇ
5297 "
5298 ));
5299
5300 cx.update_editor(|editor, window, cx| {
5301 editor.newline(&Default::default(), window, cx);
5302 });
5303 cx.run_until_parked();
5304 cx.simulate_input("# ");
5305 cx.run_until_parked();
5306 cx.assert_editor_state(indoc!(
5307 "
5308 one two three
5309 four
5310 five
5311 # ˇ
5312 "
5313 ));
5314
5315 cx.update_editor(|editor, window, cx| {
5316 editor.newline(&Default::default(), window, cx);
5317 });
5318 cx.run_until_parked();
5319 cx.assert_editor_state(indoc!(
5320 "
5321 one two three
5322 four
5323 five
5324 #\x20
5325 #ˇ
5326 "
5327 ));
5328
5329 cx.simulate_input(" 6");
5330 cx.run_until_parked();
5331 cx.assert_editor_state(indoc!(
5332 "
5333 one two three
5334 four
5335 five
5336 #
5337 # 6ˇ
5338 "
5339 ));
5340}
5341
5342#[gpui::test]
5343async fn test_clipboard(cx: &mut TestAppContext) {
5344 init_test(cx, |_| {});
5345
5346 let mut cx = EditorTestContext::new(cx).await;
5347
5348 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
5349 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5350 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
5351
5352 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
5353 cx.set_state("two ˇfour ˇsix ˇ");
5354 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5355 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
5356
5357 // Paste again but with only two cursors. Since the number of cursors doesn't
5358 // match the number of slices in the clipboard, the entire clipboard text
5359 // is pasted at each cursor.
5360 cx.set_state("ˇtwo one✅ four three six five ˇ");
5361 cx.update_editor(|e, window, cx| {
5362 e.handle_input("( ", window, cx);
5363 e.paste(&Paste, window, cx);
5364 e.handle_input(") ", window, cx);
5365 });
5366 cx.assert_editor_state(
5367 &([
5368 "( one✅ ",
5369 "three ",
5370 "five ) ˇtwo one✅ four three six five ( one✅ ",
5371 "three ",
5372 "five ) ˇ",
5373 ]
5374 .join("\n")),
5375 );
5376
5377 // Cut with three selections, one of which is full-line.
5378 cx.set_state(indoc! {"
5379 1«2ˇ»3
5380 4ˇ567
5381 «8ˇ»9"});
5382 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5383 cx.assert_editor_state(indoc! {"
5384 1ˇ3
5385 ˇ9"});
5386
5387 // Paste with three selections, noticing how the copied selection that was full-line
5388 // gets inserted before the second cursor.
5389 cx.set_state(indoc! {"
5390 1ˇ3
5391 9ˇ
5392 «oˇ»ne"});
5393 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5394 cx.assert_editor_state(indoc! {"
5395 12ˇ3
5396 4567
5397 9ˇ
5398 8ˇne"});
5399
5400 // Copy with a single cursor only, which writes the whole line into the clipboard.
5401 cx.set_state(indoc! {"
5402 The quick brown
5403 fox juˇmps over
5404 the lazy dog"});
5405 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5406 assert_eq!(
5407 cx.read_from_clipboard()
5408 .and_then(|item| item.text().as_deref().map(str::to_string)),
5409 Some("fox jumps over\n".to_string())
5410 );
5411
5412 // Paste with three selections, noticing how the copied full-line selection is inserted
5413 // before the empty selections but replaces the selection that is non-empty.
5414 cx.set_state(indoc! {"
5415 Tˇhe quick brown
5416 «foˇ»x jumps over
5417 tˇhe lazy dog"});
5418 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5419 cx.assert_editor_state(indoc! {"
5420 fox jumps over
5421 Tˇhe quick brown
5422 fox jumps over
5423 ˇx jumps over
5424 fox jumps over
5425 tˇhe lazy dog"});
5426}
5427
5428#[gpui::test]
5429async fn test_copy_trim(cx: &mut TestAppContext) {
5430 init_test(cx, |_| {});
5431
5432 let mut cx = EditorTestContext::new(cx).await;
5433 cx.set_state(
5434 r#" «for selection in selections.iter() {
5435 let mut start = selection.start;
5436 let mut end = selection.end;
5437 let is_entire_line = selection.is_empty();
5438 if is_entire_line {
5439 start = Point::new(start.row, 0);ˇ»
5440 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5441 }
5442 "#,
5443 );
5444 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5445 assert_eq!(
5446 cx.read_from_clipboard()
5447 .and_then(|item| item.text().as_deref().map(str::to_string)),
5448 Some(
5449 "for selection in selections.iter() {
5450 let mut start = selection.start;
5451 let mut end = selection.end;
5452 let is_entire_line = selection.is_empty();
5453 if is_entire_line {
5454 start = Point::new(start.row, 0);"
5455 .to_string()
5456 ),
5457 "Regular copying preserves all indentation selected",
5458 );
5459 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5460 assert_eq!(
5461 cx.read_from_clipboard()
5462 .and_then(|item| item.text().as_deref().map(str::to_string)),
5463 Some(
5464 "for selection in selections.iter() {
5465let mut start = selection.start;
5466let mut end = selection.end;
5467let is_entire_line = selection.is_empty();
5468if is_entire_line {
5469 start = Point::new(start.row, 0);"
5470 .to_string()
5471 ),
5472 "Copying with stripping should strip all leading whitespaces"
5473 );
5474
5475 cx.set_state(
5476 r#" « for selection in selections.iter() {
5477 let mut start = selection.start;
5478 let mut end = selection.end;
5479 let is_entire_line = selection.is_empty();
5480 if is_entire_line {
5481 start = Point::new(start.row, 0);ˇ»
5482 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5483 }
5484 "#,
5485 );
5486 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5487 assert_eq!(
5488 cx.read_from_clipboard()
5489 .and_then(|item| item.text().as_deref().map(str::to_string)),
5490 Some(
5491 " for selection in selections.iter() {
5492 let mut start = selection.start;
5493 let mut end = selection.end;
5494 let is_entire_line = selection.is_empty();
5495 if is_entire_line {
5496 start = Point::new(start.row, 0);"
5497 .to_string()
5498 ),
5499 "Regular copying preserves all indentation selected",
5500 );
5501 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5502 assert_eq!(
5503 cx.read_from_clipboard()
5504 .and_then(|item| item.text().as_deref().map(str::to_string)),
5505 Some(
5506 "for selection in selections.iter() {
5507let mut start = selection.start;
5508let mut end = selection.end;
5509let is_entire_line = selection.is_empty();
5510if is_entire_line {
5511 start = Point::new(start.row, 0);"
5512 .to_string()
5513 ),
5514 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
5515 );
5516
5517 cx.set_state(
5518 r#" «ˇ for selection in selections.iter() {
5519 let mut start = selection.start;
5520 let mut end = selection.end;
5521 let is_entire_line = selection.is_empty();
5522 if is_entire_line {
5523 start = Point::new(start.row, 0);»
5524 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5525 }
5526 "#,
5527 );
5528 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5529 assert_eq!(
5530 cx.read_from_clipboard()
5531 .and_then(|item| item.text().as_deref().map(str::to_string)),
5532 Some(
5533 " for selection in selections.iter() {
5534 let mut start = selection.start;
5535 let mut end = selection.end;
5536 let is_entire_line = selection.is_empty();
5537 if is_entire_line {
5538 start = Point::new(start.row, 0);"
5539 .to_string()
5540 ),
5541 "Regular copying for reverse selection works the same",
5542 );
5543 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5544 assert_eq!(
5545 cx.read_from_clipboard()
5546 .and_then(|item| item.text().as_deref().map(str::to_string)),
5547 Some(
5548 "for selection in selections.iter() {
5549let mut start = selection.start;
5550let mut end = selection.end;
5551let is_entire_line = selection.is_empty();
5552if is_entire_line {
5553 start = Point::new(start.row, 0);"
5554 .to_string()
5555 ),
5556 "Copying with stripping for reverse selection works the same"
5557 );
5558
5559 cx.set_state(
5560 r#" for selection «in selections.iter() {
5561 let mut start = selection.start;
5562 let mut end = selection.end;
5563 let is_entire_line = selection.is_empty();
5564 if is_entire_line {
5565 start = Point::new(start.row, 0);ˇ»
5566 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5567 }
5568 "#,
5569 );
5570 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5571 assert_eq!(
5572 cx.read_from_clipboard()
5573 .and_then(|item| item.text().as_deref().map(str::to_string)),
5574 Some(
5575 "in selections.iter() {
5576 let mut start = selection.start;
5577 let mut end = selection.end;
5578 let is_entire_line = selection.is_empty();
5579 if is_entire_line {
5580 start = Point::new(start.row, 0);"
5581 .to_string()
5582 ),
5583 "When selecting past the indent, the copying works as usual",
5584 );
5585 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5586 assert_eq!(
5587 cx.read_from_clipboard()
5588 .and_then(|item| item.text().as_deref().map(str::to_string)),
5589 Some(
5590 "in selections.iter() {
5591 let mut start = selection.start;
5592 let mut end = selection.end;
5593 let is_entire_line = selection.is_empty();
5594 if is_entire_line {
5595 start = Point::new(start.row, 0);"
5596 .to_string()
5597 ),
5598 "When selecting past the indent, nothing is trimmed"
5599 );
5600
5601 cx.set_state(
5602 r#" «for selection in selections.iter() {
5603 let mut start = selection.start;
5604
5605 let mut end = selection.end;
5606 let is_entire_line = selection.is_empty();
5607 if is_entire_line {
5608 start = Point::new(start.row, 0);
5609ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
5610 }
5611 "#,
5612 );
5613 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5614 assert_eq!(
5615 cx.read_from_clipboard()
5616 .and_then(|item| item.text().as_deref().map(str::to_string)),
5617 Some(
5618 "for selection in selections.iter() {
5619let mut start = selection.start;
5620
5621let mut end = selection.end;
5622let is_entire_line = selection.is_empty();
5623if is_entire_line {
5624 start = Point::new(start.row, 0);
5625"
5626 .to_string()
5627 ),
5628 "Copying with stripping should ignore empty lines"
5629 );
5630}
5631
5632#[gpui::test]
5633async fn test_paste_multiline(cx: &mut TestAppContext) {
5634 init_test(cx, |_| {});
5635
5636 let mut cx = EditorTestContext::new(cx).await;
5637 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5638
5639 // Cut an indented block, without the leading whitespace.
5640 cx.set_state(indoc! {"
5641 const a: B = (
5642 c(),
5643 «d(
5644 e,
5645 f
5646 )ˇ»
5647 );
5648 "});
5649 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5650 cx.assert_editor_state(indoc! {"
5651 const a: B = (
5652 c(),
5653 ˇ
5654 );
5655 "});
5656
5657 // Paste it at the same position.
5658 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5659 cx.assert_editor_state(indoc! {"
5660 const a: B = (
5661 c(),
5662 d(
5663 e,
5664 f
5665 )ˇ
5666 );
5667 "});
5668
5669 // Paste it at a line with a lower indent level.
5670 cx.set_state(indoc! {"
5671 ˇ
5672 const a: B = (
5673 c(),
5674 );
5675 "});
5676 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5677 cx.assert_editor_state(indoc! {"
5678 d(
5679 e,
5680 f
5681 )ˇ
5682 const a: B = (
5683 c(),
5684 );
5685 "});
5686
5687 // Cut an indented block, with the leading whitespace.
5688 cx.set_state(indoc! {"
5689 const a: B = (
5690 c(),
5691 « d(
5692 e,
5693 f
5694 )
5695 ˇ»);
5696 "});
5697 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5698 cx.assert_editor_state(indoc! {"
5699 const a: B = (
5700 c(),
5701 ˇ);
5702 "});
5703
5704 // Paste it at the same position.
5705 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5706 cx.assert_editor_state(indoc! {"
5707 const a: B = (
5708 c(),
5709 d(
5710 e,
5711 f
5712 )
5713 ˇ);
5714 "});
5715
5716 // Paste it at a line with a higher indent level.
5717 cx.set_state(indoc! {"
5718 const a: B = (
5719 c(),
5720 d(
5721 e,
5722 fˇ
5723 )
5724 );
5725 "});
5726 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5727 cx.assert_editor_state(indoc! {"
5728 const a: B = (
5729 c(),
5730 d(
5731 e,
5732 f d(
5733 e,
5734 f
5735 )
5736 ˇ
5737 )
5738 );
5739 "});
5740
5741 // Copy an indented block, starting mid-line
5742 cx.set_state(indoc! {"
5743 const a: B = (
5744 c(),
5745 somethin«g(
5746 e,
5747 f
5748 )ˇ»
5749 );
5750 "});
5751 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5752
5753 // Paste it on a line with a lower indent level
5754 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
5755 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5756 cx.assert_editor_state(indoc! {"
5757 const a: B = (
5758 c(),
5759 something(
5760 e,
5761 f
5762 )
5763 );
5764 g(
5765 e,
5766 f
5767 )ˇ"});
5768}
5769
5770#[gpui::test]
5771async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
5772 init_test(cx, |_| {});
5773
5774 cx.write_to_clipboard(ClipboardItem::new_string(
5775 " d(\n e\n );\n".into(),
5776 ));
5777
5778 let mut cx = EditorTestContext::new(cx).await;
5779 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5780
5781 cx.set_state(indoc! {"
5782 fn a() {
5783 b();
5784 if c() {
5785 ˇ
5786 }
5787 }
5788 "});
5789
5790 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5791 cx.assert_editor_state(indoc! {"
5792 fn a() {
5793 b();
5794 if c() {
5795 d(
5796 e
5797 );
5798 ˇ
5799 }
5800 }
5801 "});
5802
5803 cx.set_state(indoc! {"
5804 fn a() {
5805 b();
5806 ˇ
5807 }
5808 "});
5809
5810 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5811 cx.assert_editor_state(indoc! {"
5812 fn a() {
5813 b();
5814 d(
5815 e
5816 );
5817 ˇ
5818 }
5819 "});
5820}
5821
5822#[gpui::test]
5823fn test_select_all(cx: &mut TestAppContext) {
5824 init_test(cx, |_| {});
5825
5826 let editor = cx.add_window(|window, cx| {
5827 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
5828 build_editor(buffer, window, cx)
5829 });
5830 _ = editor.update(cx, |editor, window, cx| {
5831 editor.select_all(&SelectAll, window, cx);
5832 assert_eq!(
5833 editor.selections.display_ranges(cx),
5834 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
5835 );
5836 });
5837}
5838
5839#[gpui::test]
5840fn test_select_line(cx: &mut TestAppContext) {
5841 init_test(cx, |_| {});
5842
5843 let editor = cx.add_window(|window, cx| {
5844 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
5845 build_editor(buffer, window, cx)
5846 });
5847 _ = editor.update(cx, |editor, window, cx| {
5848 editor.change_selections(None, window, cx, |s| {
5849 s.select_display_ranges([
5850 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5851 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5852 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5853 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
5854 ])
5855 });
5856 editor.select_line(&SelectLine, window, cx);
5857 assert_eq!(
5858 editor.selections.display_ranges(cx),
5859 vec![
5860 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
5861 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
5862 ]
5863 );
5864 });
5865
5866 _ = editor.update(cx, |editor, window, cx| {
5867 editor.select_line(&SelectLine, window, cx);
5868 assert_eq!(
5869 editor.selections.display_ranges(cx),
5870 vec![
5871 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
5872 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
5873 ]
5874 );
5875 });
5876
5877 _ = editor.update(cx, |editor, window, cx| {
5878 editor.select_line(&SelectLine, window, cx);
5879 assert_eq!(
5880 editor.selections.display_ranges(cx),
5881 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
5882 );
5883 });
5884}
5885
5886#[gpui::test]
5887async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
5888 init_test(cx, |_| {});
5889 let mut cx = EditorTestContext::new(cx).await;
5890
5891 #[track_caller]
5892 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
5893 cx.set_state(initial_state);
5894 cx.update_editor(|e, window, cx| {
5895 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
5896 });
5897 cx.assert_editor_state(expected_state);
5898 }
5899
5900 // Selection starts and ends at the middle of lines, left-to-right
5901 test(
5902 &mut cx,
5903 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
5904 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5905 );
5906 // Same thing, right-to-left
5907 test(
5908 &mut cx,
5909 "aa\nb«b\ncc\ndd\neˇ»e\nff",
5910 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5911 );
5912
5913 // Whole buffer, left-to-right, last line *doesn't* end with newline
5914 test(
5915 &mut cx,
5916 "«ˇaa\nbb\ncc\ndd\nee\nff»",
5917 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5918 );
5919 // Same thing, right-to-left
5920 test(
5921 &mut cx,
5922 "«aa\nbb\ncc\ndd\nee\nffˇ»",
5923 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5924 );
5925
5926 // Whole buffer, left-to-right, last line ends with newline
5927 test(
5928 &mut cx,
5929 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
5930 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5931 );
5932 // Same thing, right-to-left
5933 test(
5934 &mut cx,
5935 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
5936 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5937 );
5938
5939 // Starts at the end of a line, ends at the start of another
5940 test(
5941 &mut cx,
5942 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
5943 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
5944 );
5945}
5946
5947#[gpui::test]
5948async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
5949 init_test(cx, |_| {});
5950
5951 let editor = cx.add_window(|window, cx| {
5952 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
5953 build_editor(buffer, window, cx)
5954 });
5955
5956 // setup
5957 _ = editor.update(cx, |editor, window, cx| {
5958 editor.fold_creases(
5959 vec![
5960 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5961 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5962 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5963 ],
5964 true,
5965 window,
5966 cx,
5967 );
5968 assert_eq!(
5969 editor.display_text(cx),
5970 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5971 );
5972 });
5973
5974 _ = editor.update(cx, |editor, window, cx| {
5975 editor.change_selections(None, window, cx, |s| {
5976 s.select_display_ranges([
5977 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5978 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5979 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5980 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
5981 ])
5982 });
5983 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5984 assert_eq!(
5985 editor.display_text(cx),
5986 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5987 );
5988 });
5989 EditorTestContext::for_editor(editor, cx)
5990 .await
5991 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
5992
5993 _ = editor.update(cx, |editor, window, cx| {
5994 editor.change_selections(None, window, cx, |s| {
5995 s.select_display_ranges([
5996 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
5997 ])
5998 });
5999 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
6000 assert_eq!(
6001 editor.display_text(cx),
6002 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
6003 );
6004 assert_eq!(
6005 editor.selections.display_ranges(cx),
6006 [
6007 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
6008 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
6009 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
6010 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
6011 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
6012 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
6013 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
6014 ]
6015 );
6016 });
6017 EditorTestContext::for_editor(editor, cx)
6018 .await
6019 .assert_editor_state(
6020 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
6021 );
6022}
6023
6024#[gpui::test]
6025async fn test_add_selection_above_below(cx: &mut TestAppContext) {
6026 init_test(cx, |_| {});
6027
6028 let mut cx = EditorTestContext::new(cx).await;
6029
6030 cx.set_state(indoc!(
6031 r#"abc
6032 defˇghi
6033
6034 jk
6035 nlmo
6036 "#
6037 ));
6038
6039 cx.update_editor(|editor, window, cx| {
6040 editor.add_selection_above(&Default::default(), window, cx);
6041 });
6042
6043 cx.assert_editor_state(indoc!(
6044 r#"abcˇ
6045 defˇghi
6046
6047 jk
6048 nlmo
6049 "#
6050 ));
6051
6052 cx.update_editor(|editor, window, cx| {
6053 editor.add_selection_above(&Default::default(), window, cx);
6054 });
6055
6056 cx.assert_editor_state(indoc!(
6057 r#"abcˇ
6058 defˇghi
6059
6060 jk
6061 nlmo
6062 "#
6063 ));
6064
6065 cx.update_editor(|editor, window, cx| {
6066 editor.add_selection_below(&Default::default(), window, cx);
6067 });
6068
6069 cx.assert_editor_state(indoc!(
6070 r#"abc
6071 defˇghi
6072
6073 jk
6074 nlmo
6075 "#
6076 ));
6077
6078 cx.update_editor(|editor, window, cx| {
6079 editor.undo_selection(&Default::default(), window, cx);
6080 });
6081
6082 cx.assert_editor_state(indoc!(
6083 r#"abcˇ
6084 defˇghi
6085
6086 jk
6087 nlmo
6088 "#
6089 ));
6090
6091 cx.update_editor(|editor, window, cx| {
6092 editor.redo_selection(&Default::default(), window, cx);
6093 });
6094
6095 cx.assert_editor_state(indoc!(
6096 r#"abc
6097 defˇghi
6098
6099 jk
6100 nlmo
6101 "#
6102 ));
6103
6104 cx.update_editor(|editor, window, cx| {
6105 editor.add_selection_below(&Default::default(), window, cx);
6106 });
6107
6108 cx.assert_editor_state(indoc!(
6109 r#"abc
6110 defˇghi
6111 ˇ
6112 jk
6113 nlmo
6114 "#
6115 ));
6116
6117 cx.update_editor(|editor, window, cx| {
6118 editor.add_selection_below(&Default::default(), window, cx);
6119 });
6120
6121 cx.assert_editor_state(indoc!(
6122 r#"abc
6123 defˇghi
6124 ˇ
6125 jkˇ
6126 nlmo
6127 "#
6128 ));
6129
6130 cx.update_editor(|editor, window, cx| {
6131 editor.add_selection_below(&Default::default(), window, cx);
6132 });
6133
6134 cx.assert_editor_state(indoc!(
6135 r#"abc
6136 defˇghi
6137 ˇ
6138 jkˇ
6139 nlmˇo
6140 "#
6141 ));
6142
6143 cx.update_editor(|editor, window, cx| {
6144 editor.add_selection_below(&Default::default(), window, cx);
6145 });
6146
6147 cx.assert_editor_state(indoc!(
6148 r#"abc
6149 defˇghi
6150 ˇ
6151 jkˇ
6152 nlmˇo
6153 ˇ"#
6154 ));
6155
6156 // change selections
6157 cx.set_state(indoc!(
6158 r#"abc
6159 def«ˇg»hi
6160
6161 jk
6162 nlmo
6163 "#
6164 ));
6165
6166 cx.update_editor(|editor, window, cx| {
6167 editor.add_selection_below(&Default::default(), window, cx);
6168 });
6169
6170 cx.assert_editor_state(indoc!(
6171 r#"abc
6172 def«ˇg»hi
6173
6174 jk
6175 nlm«ˇo»
6176 "#
6177 ));
6178
6179 cx.update_editor(|editor, window, cx| {
6180 editor.add_selection_below(&Default::default(), window, cx);
6181 });
6182
6183 cx.assert_editor_state(indoc!(
6184 r#"abc
6185 def«ˇg»hi
6186
6187 jk
6188 nlm«ˇo»
6189 "#
6190 ));
6191
6192 cx.update_editor(|editor, window, cx| {
6193 editor.add_selection_above(&Default::default(), window, cx);
6194 });
6195
6196 cx.assert_editor_state(indoc!(
6197 r#"abc
6198 def«ˇg»hi
6199
6200 jk
6201 nlmo
6202 "#
6203 ));
6204
6205 cx.update_editor(|editor, window, cx| {
6206 editor.add_selection_above(&Default::default(), window, cx);
6207 });
6208
6209 cx.assert_editor_state(indoc!(
6210 r#"abc
6211 def«ˇg»hi
6212
6213 jk
6214 nlmo
6215 "#
6216 ));
6217
6218 // Change selections again
6219 cx.set_state(indoc!(
6220 r#"a«bc
6221 defgˇ»hi
6222
6223 jk
6224 nlmo
6225 "#
6226 ));
6227
6228 cx.update_editor(|editor, window, cx| {
6229 editor.add_selection_below(&Default::default(), window, cx);
6230 });
6231
6232 cx.assert_editor_state(indoc!(
6233 r#"a«bcˇ»
6234 d«efgˇ»hi
6235
6236 j«kˇ»
6237 nlmo
6238 "#
6239 ));
6240
6241 cx.update_editor(|editor, window, cx| {
6242 editor.add_selection_below(&Default::default(), window, cx);
6243 });
6244 cx.assert_editor_state(indoc!(
6245 r#"a«bcˇ»
6246 d«efgˇ»hi
6247
6248 j«kˇ»
6249 n«lmoˇ»
6250 "#
6251 ));
6252 cx.update_editor(|editor, window, cx| {
6253 editor.add_selection_above(&Default::default(), window, cx);
6254 });
6255
6256 cx.assert_editor_state(indoc!(
6257 r#"a«bcˇ»
6258 d«efgˇ»hi
6259
6260 j«kˇ»
6261 nlmo
6262 "#
6263 ));
6264
6265 // Change selections again
6266 cx.set_state(indoc!(
6267 r#"abc
6268 d«ˇefghi
6269
6270 jk
6271 nlm»o
6272 "#
6273 ));
6274
6275 cx.update_editor(|editor, window, cx| {
6276 editor.add_selection_above(&Default::default(), window, cx);
6277 });
6278
6279 cx.assert_editor_state(indoc!(
6280 r#"a«ˇbc»
6281 d«ˇef»ghi
6282
6283 j«ˇk»
6284 n«ˇlm»o
6285 "#
6286 ));
6287
6288 cx.update_editor(|editor, window, cx| {
6289 editor.add_selection_below(&Default::default(), window, cx);
6290 });
6291
6292 cx.assert_editor_state(indoc!(
6293 r#"abc
6294 d«ˇef»ghi
6295
6296 j«ˇk»
6297 n«ˇlm»o
6298 "#
6299 ));
6300}
6301
6302#[gpui::test]
6303async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
6304 init_test(cx, |_| {});
6305 let mut cx = EditorTestContext::new(cx).await;
6306
6307 cx.set_state(indoc!(
6308 r#"line onˇe
6309 liˇne two
6310 line three
6311 line four"#
6312 ));
6313
6314 cx.update_editor(|editor, window, cx| {
6315 editor.add_selection_below(&Default::default(), window, cx);
6316 });
6317
6318 // test multiple cursors expand in the same direction
6319 cx.assert_editor_state(indoc!(
6320 r#"line onˇe
6321 liˇne twˇo
6322 liˇne three
6323 line four"#
6324 ));
6325
6326 cx.update_editor(|editor, window, cx| {
6327 editor.add_selection_below(&Default::default(), window, cx);
6328 });
6329
6330 cx.update_editor(|editor, window, cx| {
6331 editor.add_selection_below(&Default::default(), window, cx);
6332 });
6333
6334 // test multiple cursors expand below overflow
6335 cx.assert_editor_state(indoc!(
6336 r#"line onˇe
6337 liˇne twˇo
6338 liˇne thˇree
6339 liˇne foˇur"#
6340 ));
6341
6342 cx.update_editor(|editor, window, cx| {
6343 editor.add_selection_above(&Default::default(), window, cx);
6344 });
6345
6346 // test multiple cursors retrieves back correctly
6347 cx.assert_editor_state(indoc!(
6348 r#"line onˇe
6349 liˇne twˇo
6350 liˇne thˇree
6351 line four"#
6352 ));
6353
6354 cx.update_editor(|editor, window, cx| {
6355 editor.add_selection_above(&Default::default(), window, cx);
6356 });
6357
6358 cx.update_editor(|editor, window, cx| {
6359 editor.add_selection_above(&Default::default(), window, cx);
6360 });
6361
6362 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
6363 cx.assert_editor_state(indoc!(
6364 r#"liˇne onˇe
6365 liˇne two
6366 line three
6367 line four"#
6368 ));
6369
6370 cx.update_editor(|editor, window, cx| {
6371 editor.undo_selection(&Default::default(), window, cx);
6372 });
6373
6374 // test undo
6375 cx.assert_editor_state(indoc!(
6376 r#"line onˇe
6377 liˇne twˇo
6378 line three
6379 line four"#
6380 ));
6381
6382 cx.update_editor(|editor, window, cx| {
6383 editor.redo_selection(&Default::default(), window, cx);
6384 });
6385
6386 // test redo
6387 cx.assert_editor_state(indoc!(
6388 r#"liˇne onˇe
6389 liˇne two
6390 line three
6391 line four"#
6392 ));
6393
6394 cx.set_state(indoc!(
6395 r#"abcd
6396 ef«ghˇ»
6397 ijkl
6398 «mˇ»nop"#
6399 ));
6400
6401 cx.update_editor(|editor, window, cx| {
6402 editor.add_selection_above(&Default::default(), window, cx);
6403 });
6404
6405 // test multiple selections expand in the same direction
6406 cx.assert_editor_state(indoc!(
6407 r#"ab«cdˇ»
6408 ef«ghˇ»
6409 «iˇ»jkl
6410 «mˇ»nop"#
6411 ));
6412
6413 cx.update_editor(|editor, window, cx| {
6414 editor.add_selection_above(&Default::default(), window, cx);
6415 });
6416
6417 // test multiple selection upward overflow
6418 cx.assert_editor_state(indoc!(
6419 r#"ab«cdˇ»
6420 «eˇ»f«ghˇ»
6421 «iˇ»jkl
6422 «mˇ»nop"#
6423 ));
6424
6425 cx.update_editor(|editor, window, cx| {
6426 editor.add_selection_below(&Default::default(), window, cx);
6427 });
6428
6429 // test multiple selection retrieves back correctly
6430 cx.assert_editor_state(indoc!(
6431 r#"abcd
6432 ef«ghˇ»
6433 «iˇ»jkl
6434 «mˇ»nop"#
6435 ));
6436
6437 cx.update_editor(|editor, window, cx| {
6438 editor.add_selection_below(&Default::default(), window, cx);
6439 });
6440
6441 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
6442 cx.assert_editor_state(indoc!(
6443 r#"abcd
6444 ef«ghˇ»
6445 ij«klˇ»
6446 «mˇ»nop"#
6447 ));
6448
6449 cx.update_editor(|editor, window, cx| {
6450 editor.undo_selection(&Default::default(), window, cx);
6451 });
6452
6453 // test undo
6454 cx.assert_editor_state(indoc!(
6455 r#"abcd
6456 ef«ghˇ»
6457 «iˇ»jkl
6458 «mˇ»nop"#
6459 ));
6460
6461 cx.update_editor(|editor, window, cx| {
6462 editor.redo_selection(&Default::default(), window, cx);
6463 });
6464
6465 // test redo
6466 cx.assert_editor_state(indoc!(
6467 r#"abcd
6468 ef«ghˇ»
6469 ij«klˇ»
6470 «mˇ»nop"#
6471 ));
6472}
6473
6474#[gpui::test]
6475async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
6476 init_test(cx, |_| {});
6477 let mut cx = EditorTestContext::new(cx).await;
6478
6479 cx.set_state(indoc!(
6480 r#"line onˇe
6481 liˇne two
6482 line three
6483 line four"#
6484 ));
6485
6486 cx.update_editor(|editor, window, cx| {
6487 editor.add_selection_below(&Default::default(), window, cx);
6488 editor.add_selection_below(&Default::default(), window, cx);
6489 editor.add_selection_below(&Default::default(), window, cx);
6490 });
6491
6492 // initial state with two multi cursor groups
6493 cx.assert_editor_state(indoc!(
6494 r#"line onˇe
6495 liˇne twˇo
6496 liˇne thˇree
6497 liˇne foˇur"#
6498 ));
6499
6500 // add single cursor in middle - simulate opt click
6501 cx.update_editor(|editor, window, cx| {
6502 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
6503 editor.begin_selection(new_cursor_point, true, 1, window, cx);
6504 editor.end_selection(window, cx);
6505 });
6506
6507 cx.assert_editor_state(indoc!(
6508 r#"line onˇe
6509 liˇne twˇo
6510 liˇneˇ thˇree
6511 liˇne foˇur"#
6512 ));
6513
6514 cx.update_editor(|editor, window, cx| {
6515 editor.add_selection_above(&Default::default(), window, cx);
6516 });
6517
6518 // test new added selection expands above and existing selection shrinks
6519 cx.assert_editor_state(indoc!(
6520 r#"line onˇe
6521 liˇneˇ twˇo
6522 liˇneˇ thˇree
6523 line four"#
6524 ));
6525
6526 cx.update_editor(|editor, window, cx| {
6527 editor.add_selection_above(&Default::default(), window, cx);
6528 });
6529
6530 // test new added selection expands above and existing selection shrinks
6531 cx.assert_editor_state(indoc!(
6532 r#"lineˇ onˇe
6533 liˇneˇ twˇo
6534 lineˇ three
6535 line four"#
6536 ));
6537
6538 // intial state with two selection groups
6539 cx.set_state(indoc!(
6540 r#"abcd
6541 ef«ghˇ»
6542 ijkl
6543 «mˇ»nop"#
6544 ));
6545
6546 cx.update_editor(|editor, window, cx| {
6547 editor.add_selection_above(&Default::default(), window, cx);
6548 editor.add_selection_above(&Default::default(), window, cx);
6549 });
6550
6551 cx.assert_editor_state(indoc!(
6552 r#"ab«cdˇ»
6553 «eˇ»f«ghˇ»
6554 «iˇ»jkl
6555 «mˇ»nop"#
6556 ));
6557
6558 // add single selection in middle - simulate opt drag
6559 cx.update_editor(|editor, window, cx| {
6560 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
6561 editor.begin_selection(new_cursor_point, true, 1, window, cx);
6562 editor.update_selection(
6563 DisplayPoint::new(DisplayRow(2), 4),
6564 0,
6565 gpui::Point::<f32>::default(),
6566 window,
6567 cx,
6568 );
6569 editor.end_selection(window, cx);
6570 });
6571
6572 cx.assert_editor_state(indoc!(
6573 r#"ab«cdˇ»
6574 «eˇ»f«ghˇ»
6575 «iˇ»jk«lˇ»
6576 «mˇ»nop"#
6577 ));
6578
6579 cx.update_editor(|editor, window, cx| {
6580 editor.add_selection_below(&Default::default(), window, cx);
6581 });
6582
6583 // test new added selection expands below, others shrinks from above
6584 cx.assert_editor_state(indoc!(
6585 r#"abcd
6586 ef«ghˇ»
6587 «iˇ»jk«lˇ»
6588 «mˇ»no«pˇ»"#
6589 ));
6590}
6591
6592#[gpui::test]
6593async fn test_select_next(cx: &mut TestAppContext) {
6594 init_test(cx, |_| {});
6595
6596 let mut cx = EditorTestContext::new(cx).await;
6597 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6598
6599 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6600 .unwrap();
6601 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6602
6603 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6604 .unwrap();
6605 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
6606
6607 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6608 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6609
6610 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6611 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
6612
6613 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6614 .unwrap();
6615 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6616
6617 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6618 .unwrap();
6619 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6620
6621 // Test selection direction should be preserved
6622 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
6623
6624 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6625 .unwrap();
6626 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
6627}
6628
6629#[gpui::test]
6630async fn test_select_all_matches(cx: &mut TestAppContext) {
6631 init_test(cx, |_| {});
6632
6633 let mut cx = EditorTestContext::new(cx).await;
6634
6635 // Test caret-only selections
6636 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6637 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6638 .unwrap();
6639 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6640
6641 // Test left-to-right selections
6642 cx.set_state("abc\n«abcˇ»\nabc");
6643 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6644 .unwrap();
6645 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
6646
6647 // Test right-to-left selections
6648 cx.set_state("abc\n«ˇabc»\nabc");
6649 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6650 .unwrap();
6651 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
6652
6653 // Test selecting whitespace with caret selection
6654 cx.set_state("abc\nˇ abc\nabc");
6655 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6656 .unwrap();
6657 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
6658
6659 // Test selecting whitespace with left-to-right selection
6660 cx.set_state("abc\n«ˇ »abc\nabc");
6661 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6662 .unwrap();
6663 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
6664
6665 // Test no matches with right-to-left selection
6666 cx.set_state("abc\n« ˇ»abc\nabc");
6667 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6668 .unwrap();
6669 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
6670}
6671
6672#[gpui::test]
6673async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
6674 init_test(cx, |_| {});
6675
6676 let mut cx = EditorTestContext::new(cx).await;
6677
6678 let large_body_1 = "\nd".repeat(200);
6679 let large_body_2 = "\ne".repeat(200);
6680
6681 cx.set_state(&format!(
6682 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
6683 ));
6684 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
6685 let scroll_position = editor.scroll_position(cx);
6686 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
6687 scroll_position
6688 });
6689
6690 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6691 .unwrap();
6692 cx.assert_editor_state(&format!(
6693 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
6694 ));
6695 let scroll_position_after_selection =
6696 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
6697 assert_eq!(
6698 initial_scroll_position, scroll_position_after_selection,
6699 "Scroll position should not change after selecting all matches"
6700 );
6701}
6702
6703#[gpui::test]
6704async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
6705 init_test(cx, |_| {});
6706
6707 let mut cx = EditorLspTestContext::new_rust(
6708 lsp::ServerCapabilities {
6709 document_formatting_provider: Some(lsp::OneOf::Left(true)),
6710 ..Default::default()
6711 },
6712 cx,
6713 )
6714 .await;
6715
6716 cx.set_state(indoc! {"
6717 line 1
6718 line 2
6719 linˇe 3
6720 line 4
6721 line 5
6722 "});
6723
6724 // Make an edit
6725 cx.update_editor(|editor, window, cx| {
6726 editor.handle_input("X", window, cx);
6727 });
6728
6729 // Move cursor to a different position
6730 cx.update_editor(|editor, window, cx| {
6731 editor.change_selections(None, window, cx, |s| {
6732 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
6733 });
6734 });
6735
6736 cx.assert_editor_state(indoc! {"
6737 line 1
6738 line 2
6739 linXe 3
6740 line 4
6741 liˇne 5
6742 "});
6743
6744 cx.lsp
6745 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
6746 Ok(Some(vec![lsp::TextEdit::new(
6747 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
6748 "PREFIX ".to_string(),
6749 )]))
6750 });
6751
6752 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
6753 .unwrap()
6754 .await
6755 .unwrap();
6756
6757 cx.assert_editor_state(indoc! {"
6758 PREFIX line 1
6759 line 2
6760 linXe 3
6761 line 4
6762 liˇne 5
6763 "});
6764
6765 // Undo formatting
6766 cx.update_editor(|editor, window, cx| {
6767 editor.undo(&Default::default(), window, cx);
6768 });
6769
6770 // Verify cursor moved back to position after edit
6771 cx.assert_editor_state(indoc! {"
6772 line 1
6773 line 2
6774 linXˇe 3
6775 line 4
6776 line 5
6777 "});
6778}
6779
6780#[gpui::test]
6781async fn test_undo_inline_completion_scrolls_to_edit_pos(cx: &mut TestAppContext) {
6782 init_test(cx, |_| {});
6783
6784 let mut cx = EditorTestContext::new(cx).await;
6785
6786 let provider = cx.new(|_| FakeInlineCompletionProvider::default());
6787 cx.update_editor(|editor, window, cx| {
6788 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
6789 });
6790
6791 cx.set_state(indoc! {"
6792 line 1
6793 line 2
6794 linˇe 3
6795 line 4
6796 line 5
6797 line 6
6798 line 7
6799 line 8
6800 line 9
6801 line 10
6802 "});
6803
6804 let snapshot = cx.buffer_snapshot();
6805 let edit_position = snapshot.anchor_after(Point::new(2, 4));
6806
6807 cx.update(|_, cx| {
6808 provider.update(cx, |provider, _| {
6809 provider.set_inline_completion(Some(inline_completion::InlineCompletion {
6810 id: None,
6811 edits: vec![(edit_position..edit_position, "X".into())],
6812 edit_preview: None,
6813 }))
6814 })
6815 });
6816
6817 cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx));
6818 cx.update_editor(|editor, window, cx| {
6819 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
6820 });
6821
6822 cx.assert_editor_state(indoc! {"
6823 line 1
6824 line 2
6825 lineXˇ 3
6826 line 4
6827 line 5
6828 line 6
6829 line 7
6830 line 8
6831 line 9
6832 line 10
6833 "});
6834
6835 cx.update_editor(|editor, window, cx| {
6836 editor.change_selections(None, window, cx, |s| {
6837 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
6838 });
6839 });
6840
6841 cx.assert_editor_state(indoc! {"
6842 line 1
6843 line 2
6844 lineX 3
6845 line 4
6846 line 5
6847 line 6
6848 line 7
6849 line 8
6850 line 9
6851 liˇne 10
6852 "});
6853
6854 cx.update_editor(|editor, window, cx| {
6855 editor.undo(&Default::default(), window, cx);
6856 });
6857
6858 cx.assert_editor_state(indoc! {"
6859 line 1
6860 line 2
6861 lineˇ 3
6862 line 4
6863 line 5
6864 line 6
6865 line 7
6866 line 8
6867 line 9
6868 line 10
6869 "});
6870}
6871
6872#[gpui::test]
6873async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
6874 init_test(cx, |_| {});
6875
6876 let mut cx = EditorTestContext::new(cx).await;
6877 cx.set_state(
6878 r#"let foo = 2;
6879lˇet foo = 2;
6880let fooˇ = 2;
6881let foo = 2;
6882let foo = ˇ2;"#,
6883 );
6884
6885 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6886 .unwrap();
6887 cx.assert_editor_state(
6888 r#"let foo = 2;
6889«letˇ» foo = 2;
6890let «fooˇ» = 2;
6891let foo = 2;
6892let foo = «2ˇ»;"#,
6893 );
6894
6895 // noop for multiple selections with different contents
6896 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6897 .unwrap();
6898 cx.assert_editor_state(
6899 r#"let foo = 2;
6900«letˇ» foo = 2;
6901let «fooˇ» = 2;
6902let foo = 2;
6903let foo = «2ˇ»;"#,
6904 );
6905
6906 // Test last selection direction should be preserved
6907 cx.set_state(
6908 r#"let foo = 2;
6909let foo = 2;
6910let «fooˇ» = 2;
6911let «ˇfoo» = 2;
6912let foo = 2;"#,
6913 );
6914
6915 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6916 .unwrap();
6917 cx.assert_editor_state(
6918 r#"let foo = 2;
6919let foo = 2;
6920let «fooˇ» = 2;
6921let «ˇfoo» = 2;
6922let «ˇfoo» = 2;"#,
6923 );
6924}
6925
6926#[gpui::test]
6927async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
6928 init_test(cx, |_| {});
6929
6930 let mut cx =
6931 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
6932
6933 cx.assert_editor_state(indoc! {"
6934 ˇbbb
6935 ccc
6936
6937 bbb
6938 ccc
6939 "});
6940 cx.dispatch_action(SelectPrevious::default());
6941 cx.assert_editor_state(indoc! {"
6942 «bbbˇ»
6943 ccc
6944
6945 bbb
6946 ccc
6947 "});
6948 cx.dispatch_action(SelectPrevious::default());
6949 cx.assert_editor_state(indoc! {"
6950 «bbbˇ»
6951 ccc
6952
6953 «bbbˇ»
6954 ccc
6955 "});
6956}
6957
6958#[gpui::test]
6959async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
6960 init_test(cx, |_| {});
6961
6962 let mut cx = EditorTestContext::new(cx).await;
6963 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6964
6965 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6966 .unwrap();
6967 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6968
6969 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6970 .unwrap();
6971 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
6972
6973 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6974 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6975
6976 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6977 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
6978
6979 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6980 .unwrap();
6981 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
6982
6983 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6984 .unwrap();
6985 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6986}
6987
6988#[gpui::test]
6989async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
6990 init_test(cx, |_| {});
6991
6992 let mut cx = EditorTestContext::new(cx).await;
6993 cx.set_state("aˇ");
6994
6995 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6996 .unwrap();
6997 cx.assert_editor_state("«aˇ»");
6998 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6999 .unwrap();
7000 cx.assert_editor_state("«aˇ»");
7001}
7002
7003#[gpui::test]
7004async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
7005 init_test(cx, |_| {});
7006
7007 let mut cx = EditorTestContext::new(cx).await;
7008 cx.set_state(
7009 r#"let foo = 2;
7010lˇet foo = 2;
7011let fooˇ = 2;
7012let foo = 2;
7013let foo = ˇ2;"#,
7014 );
7015
7016 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7017 .unwrap();
7018 cx.assert_editor_state(
7019 r#"let foo = 2;
7020«letˇ» foo = 2;
7021let «fooˇ» = 2;
7022let foo = 2;
7023let foo = «2ˇ»;"#,
7024 );
7025
7026 // noop for multiple selections with different contents
7027 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7028 .unwrap();
7029 cx.assert_editor_state(
7030 r#"let foo = 2;
7031«letˇ» foo = 2;
7032let «fooˇ» = 2;
7033let foo = 2;
7034let foo = «2ˇ»;"#,
7035 );
7036}
7037
7038#[gpui::test]
7039async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
7040 init_test(cx, |_| {});
7041
7042 let mut cx = EditorTestContext::new(cx).await;
7043 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
7044
7045 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7046 .unwrap();
7047 // selection direction is preserved
7048 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
7049
7050 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7051 .unwrap();
7052 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
7053
7054 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7055 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
7056
7057 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7058 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
7059
7060 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7061 .unwrap();
7062 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
7063
7064 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7065 .unwrap();
7066 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
7067}
7068
7069#[gpui::test]
7070async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
7071 init_test(cx, |_| {});
7072
7073 let language = Arc::new(Language::new(
7074 LanguageConfig::default(),
7075 Some(tree_sitter_rust::LANGUAGE.into()),
7076 ));
7077
7078 let text = r#"
7079 use mod1::mod2::{mod3, mod4};
7080
7081 fn fn_1(param1: bool, param2: &str) {
7082 let var1 = "text";
7083 }
7084 "#
7085 .unindent();
7086
7087 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7088 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7089 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7090
7091 editor
7092 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7093 .await;
7094
7095 editor.update_in(cx, |editor, window, cx| {
7096 editor.change_selections(None, window, cx, |s| {
7097 s.select_display_ranges([
7098 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
7099 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
7100 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
7101 ]);
7102 });
7103 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7104 });
7105 editor.update(cx, |editor, cx| {
7106 assert_text_with_selections(
7107 editor,
7108 indoc! {r#"
7109 use mod1::mod2::{mod3, «mod4ˇ»};
7110
7111 fn fn_1«ˇ(param1: bool, param2: &str)» {
7112 let var1 = "«ˇtext»";
7113 }
7114 "#},
7115 cx,
7116 );
7117 });
7118
7119 editor.update_in(cx, |editor, window, cx| {
7120 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7121 });
7122 editor.update(cx, |editor, cx| {
7123 assert_text_with_selections(
7124 editor,
7125 indoc! {r#"
7126 use mod1::mod2::«{mod3, mod4}ˇ»;
7127
7128 «ˇfn fn_1(param1: bool, param2: &str) {
7129 let var1 = "text";
7130 }»
7131 "#},
7132 cx,
7133 );
7134 });
7135
7136 editor.update_in(cx, |editor, window, cx| {
7137 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7138 });
7139 assert_eq!(
7140 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
7141 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
7142 );
7143
7144 // Trying to expand the selected syntax node one more time has no effect.
7145 editor.update_in(cx, |editor, window, cx| {
7146 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7147 });
7148 assert_eq!(
7149 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
7150 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
7151 );
7152
7153 editor.update_in(cx, |editor, window, cx| {
7154 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7155 });
7156 editor.update(cx, |editor, cx| {
7157 assert_text_with_selections(
7158 editor,
7159 indoc! {r#"
7160 use mod1::mod2::«{mod3, mod4}ˇ»;
7161
7162 «ˇfn fn_1(param1: bool, param2: &str) {
7163 let var1 = "text";
7164 }»
7165 "#},
7166 cx,
7167 );
7168 });
7169
7170 editor.update_in(cx, |editor, window, cx| {
7171 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7172 });
7173 editor.update(cx, |editor, cx| {
7174 assert_text_with_selections(
7175 editor,
7176 indoc! {r#"
7177 use mod1::mod2::{mod3, «mod4ˇ»};
7178
7179 fn fn_1«ˇ(param1: bool, param2: &str)» {
7180 let var1 = "«ˇtext»";
7181 }
7182 "#},
7183 cx,
7184 );
7185 });
7186
7187 editor.update_in(cx, |editor, window, cx| {
7188 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7189 });
7190 editor.update(cx, |editor, cx| {
7191 assert_text_with_selections(
7192 editor,
7193 indoc! {r#"
7194 use mod1::mod2::{mod3, mo«ˇ»d4};
7195
7196 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
7197 let var1 = "te«ˇ»xt";
7198 }
7199 "#},
7200 cx,
7201 );
7202 });
7203
7204 // Trying to shrink the selected syntax node one more time has no effect.
7205 editor.update_in(cx, |editor, window, cx| {
7206 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7207 });
7208 editor.update_in(cx, |editor, _, cx| {
7209 assert_text_with_selections(
7210 editor,
7211 indoc! {r#"
7212 use mod1::mod2::{mod3, mo«ˇ»d4};
7213
7214 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
7215 let var1 = "te«ˇ»xt";
7216 }
7217 "#},
7218 cx,
7219 );
7220 });
7221
7222 // Ensure that we keep expanding the selection if the larger selection starts or ends within
7223 // a fold.
7224 editor.update_in(cx, |editor, window, cx| {
7225 editor.fold_creases(
7226 vec![
7227 Crease::simple(
7228 Point::new(0, 21)..Point::new(0, 24),
7229 FoldPlaceholder::test(),
7230 ),
7231 Crease::simple(
7232 Point::new(3, 20)..Point::new(3, 22),
7233 FoldPlaceholder::test(),
7234 ),
7235 ],
7236 true,
7237 window,
7238 cx,
7239 );
7240 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7241 });
7242 editor.update(cx, |editor, cx| {
7243 assert_text_with_selections(
7244 editor,
7245 indoc! {r#"
7246 use mod1::mod2::«{mod3, mod4}ˇ»;
7247
7248 fn fn_1«ˇ(param1: bool, param2: &str)» {
7249 let var1 = "«ˇtext»";
7250 }
7251 "#},
7252 cx,
7253 );
7254 });
7255}
7256
7257#[gpui::test]
7258async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
7259 init_test(cx, |_| {});
7260
7261 let language = Arc::new(Language::new(
7262 LanguageConfig::default(),
7263 Some(tree_sitter_rust::LANGUAGE.into()),
7264 ));
7265
7266 let text = "let a = 2;";
7267
7268 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7269 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7270 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7271
7272 editor
7273 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7274 .await;
7275
7276 // Test case 1: Cursor at end of word
7277 editor.update_in(cx, |editor, window, cx| {
7278 editor.change_selections(None, window, cx, |s| {
7279 s.select_display_ranges([
7280 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
7281 ]);
7282 });
7283 });
7284 editor.update(cx, |editor, cx| {
7285 assert_text_with_selections(editor, "let aˇ = 2;", cx);
7286 });
7287 editor.update_in(cx, |editor, window, cx| {
7288 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7289 });
7290 editor.update(cx, |editor, cx| {
7291 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
7292 });
7293 editor.update_in(cx, |editor, window, cx| {
7294 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7295 });
7296 editor.update(cx, |editor, cx| {
7297 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7298 });
7299
7300 // Test case 2: Cursor at end of statement
7301 editor.update_in(cx, |editor, window, cx| {
7302 editor.change_selections(None, window, cx, |s| {
7303 s.select_display_ranges([
7304 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
7305 ]);
7306 });
7307 });
7308 editor.update(cx, |editor, cx| {
7309 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
7310 });
7311 editor.update_in(cx, |editor, window, cx| {
7312 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7313 });
7314 editor.update(cx, |editor, cx| {
7315 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7316 });
7317}
7318
7319#[gpui::test]
7320async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
7321 init_test(cx, |_| {});
7322
7323 let language = Arc::new(Language::new(
7324 LanguageConfig::default(),
7325 Some(tree_sitter_rust::LANGUAGE.into()),
7326 ));
7327
7328 let text = r#"
7329 use mod1::mod2::{mod3, mod4};
7330
7331 fn fn_1(param1: bool, param2: &str) {
7332 let var1 = "hello world";
7333 }
7334 "#
7335 .unindent();
7336
7337 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7338 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7339 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7340
7341 editor
7342 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7343 .await;
7344
7345 // Test 1: Cursor on a letter of a string word
7346 editor.update_in(cx, |editor, window, cx| {
7347 editor.change_selections(None, window, cx, |s| {
7348 s.select_display_ranges([
7349 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
7350 ]);
7351 });
7352 });
7353 editor.update_in(cx, |editor, window, cx| {
7354 assert_text_with_selections(
7355 editor,
7356 indoc! {r#"
7357 use mod1::mod2::{mod3, mod4};
7358
7359 fn fn_1(param1: bool, param2: &str) {
7360 let var1 = "hˇello world";
7361 }
7362 "#},
7363 cx,
7364 );
7365 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7366 assert_text_with_selections(
7367 editor,
7368 indoc! {r#"
7369 use mod1::mod2::{mod3, mod4};
7370
7371 fn fn_1(param1: bool, param2: &str) {
7372 let var1 = "«ˇhello» world";
7373 }
7374 "#},
7375 cx,
7376 );
7377 });
7378
7379 // Test 2: Partial selection within a word
7380 editor.update_in(cx, |editor, window, cx| {
7381 editor.change_selections(None, window, cx, |s| {
7382 s.select_display_ranges([
7383 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
7384 ]);
7385 });
7386 });
7387 editor.update_in(cx, |editor, window, cx| {
7388 assert_text_with_selections(
7389 editor,
7390 indoc! {r#"
7391 use mod1::mod2::{mod3, mod4};
7392
7393 fn fn_1(param1: bool, param2: &str) {
7394 let var1 = "h«elˇ»lo world";
7395 }
7396 "#},
7397 cx,
7398 );
7399 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, 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 = "«ˇhello» world";
7407 }
7408 "#},
7409 cx,
7410 );
7411 });
7412
7413 // Test 3: Complete word already selected
7414 editor.update_in(cx, |editor, window, cx| {
7415 editor.change_selections(None, window, cx, |s| {
7416 s.select_display_ranges([
7417 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
7418 ]);
7419 });
7420 });
7421 editor.update_in(cx, |editor, window, cx| {
7422 assert_text_with_selections(
7423 editor,
7424 indoc! {r#"
7425 use mod1::mod2::{mod3, mod4};
7426
7427 fn fn_1(param1: bool, param2: &str) {
7428 let var1 = "«helloˇ» world";
7429 }
7430 "#},
7431 cx,
7432 );
7433 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7434 assert_text_with_selections(
7435 editor,
7436 indoc! {r#"
7437 use mod1::mod2::{mod3, mod4};
7438
7439 fn fn_1(param1: bool, param2: &str) {
7440 let var1 = "«hello worldˇ»";
7441 }
7442 "#},
7443 cx,
7444 );
7445 });
7446
7447 // Test 4: Selection spanning across words
7448 editor.update_in(cx, |editor, window, cx| {
7449 editor.change_selections(None, window, cx, |s| {
7450 s.select_display_ranges([
7451 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
7452 ]);
7453 });
7454 });
7455 editor.update_in(cx, |editor, window, cx| {
7456 assert_text_with_selections(
7457 editor,
7458 indoc! {r#"
7459 use mod1::mod2::{mod3, mod4};
7460
7461 fn fn_1(param1: bool, param2: &str) {
7462 let var1 = "hel«lo woˇ»rld";
7463 }
7464 "#},
7465 cx,
7466 );
7467 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7468 assert_text_with_selections(
7469 editor,
7470 indoc! {r#"
7471 use mod1::mod2::{mod3, mod4};
7472
7473 fn fn_1(param1: bool, param2: &str) {
7474 let var1 = "«ˇhello world»";
7475 }
7476 "#},
7477 cx,
7478 );
7479 });
7480
7481 // Test 5: Expansion beyond string
7482 editor.update_in(cx, |editor, window, cx| {
7483 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7484 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7485 assert_text_with_selections(
7486 editor,
7487 indoc! {r#"
7488 use mod1::mod2::{mod3, mod4};
7489
7490 fn fn_1(param1: bool, param2: &str) {
7491 «ˇlet var1 = "hello world";»
7492 }
7493 "#},
7494 cx,
7495 );
7496 });
7497}
7498
7499#[gpui::test]
7500async fn test_fold_function_bodies(cx: &mut TestAppContext) {
7501 init_test(cx, |_| {});
7502
7503 let base_text = r#"
7504 impl A {
7505 // this is an uncommitted comment
7506
7507 fn b() {
7508 c();
7509 }
7510
7511 // this is another uncommitted comment
7512
7513 fn d() {
7514 // e
7515 // f
7516 }
7517 }
7518
7519 fn g() {
7520 // h
7521 }
7522 "#
7523 .unindent();
7524
7525 let text = r#"
7526 ˇimpl A {
7527
7528 fn b() {
7529 c();
7530 }
7531
7532 fn d() {
7533 // e
7534 // f
7535 }
7536 }
7537
7538 fn g() {
7539 // h
7540 }
7541 "#
7542 .unindent();
7543
7544 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
7545 cx.set_state(&text);
7546 cx.set_head_text(&base_text);
7547 cx.update_editor(|editor, window, cx| {
7548 editor.expand_all_diff_hunks(&Default::default(), window, cx);
7549 });
7550
7551 cx.assert_state_with_diff(
7552 "
7553 ˇimpl A {
7554 - // this is an uncommitted comment
7555
7556 fn b() {
7557 c();
7558 }
7559
7560 - // this is another uncommitted comment
7561 -
7562 fn d() {
7563 // e
7564 // f
7565 }
7566 }
7567
7568 fn g() {
7569 // h
7570 }
7571 "
7572 .unindent(),
7573 );
7574
7575 let expected_display_text = "
7576 impl A {
7577 // this is an uncommitted comment
7578
7579 fn b() {
7580 ⋯
7581 }
7582
7583 // this is another uncommitted comment
7584
7585 fn d() {
7586 ⋯
7587 }
7588 }
7589
7590 fn g() {
7591 ⋯
7592 }
7593 "
7594 .unindent();
7595
7596 cx.update_editor(|editor, window, cx| {
7597 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
7598 assert_eq!(editor.display_text(cx), expected_display_text);
7599 });
7600}
7601
7602#[gpui::test]
7603async fn test_autoindent(cx: &mut TestAppContext) {
7604 init_test(cx, |_| {});
7605
7606 let language = Arc::new(
7607 Language::new(
7608 LanguageConfig {
7609 brackets: BracketPairConfig {
7610 pairs: vec![
7611 BracketPair {
7612 start: "{".to_string(),
7613 end: "}".to_string(),
7614 close: false,
7615 surround: false,
7616 newline: true,
7617 },
7618 BracketPair {
7619 start: "(".to_string(),
7620 end: ")".to_string(),
7621 close: false,
7622 surround: false,
7623 newline: true,
7624 },
7625 ],
7626 ..Default::default()
7627 },
7628 ..Default::default()
7629 },
7630 Some(tree_sitter_rust::LANGUAGE.into()),
7631 )
7632 .with_indents_query(
7633 r#"
7634 (_ "(" ")" @end) @indent
7635 (_ "{" "}" @end) @indent
7636 "#,
7637 )
7638 .unwrap(),
7639 );
7640
7641 let text = "fn a() {}";
7642
7643 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7644 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7645 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7646 editor
7647 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7648 .await;
7649
7650 editor.update_in(cx, |editor, window, cx| {
7651 editor.change_selections(None, window, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
7652 editor.newline(&Newline, window, cx);
7653 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
7654 assert_eq!(
7655 editor.selections.ranges(cx),
7656 &[
7657 Point::new(1, 4)..Point::new(1, 4),
7658 Point::new(3, 4)..Point::new(3, 4),
7659 Point::new(5, 0)..Point::new(5, 0)
7660 ]
7661 );
7662 });
7663}
7664
7665#[gpui::test]
7666async fn test_autoindent_selections(cx: &mut TestAppContext) {
7667 init_test(cx, |_| {});
7668
7669 {
7670 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
7671 cx.set_state(indoc! {"
7672 impl A {
7673
7674 fn b() {}
7675
7676 «fn c() {
7677
7678 }ˇ»
7679 }
7680 "});
7681
7682 cx.update_editor(|editor, window, cx| {
7683 editor.autoindent(&Default::default(), window, cx);
7684 });
7685
7686 cx.assert_editor_state(indoc! {"
7687 impl A {
7688
7689 fn b() {}
7690
7691 «fn c() {
7692
7693 }ˇ»
7694 }
7695 "});
7696 }
7697
7698 {
7699 let mut cx = EditorTestContext::new_multibuffer(
7700 cx,
7701 [indoc! { "
7702 impl A {
7703 «
7704 // a
7705 fn b(){}
7706 »
7707 «
7708 }
7709 fn c(){}
7710 »
7711 "}],
7712 );
7713
7714 let buffer = cx.update_editor(|editor, _, cx| {
7715 let buffer = editor.buffer().update(cx, |buffer, _| {
7716 buffer.all_buffers().iter().next().unwrap().clone()
7717 });
7718 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7719 buffer
7720 });
7721
7722 cx.run_until_parked();
7723 cx.update_editor(|editor, window, cx| {
7724 editor.select_all(&Default::default(), window, cx);
7725 editor.autoindent(&Default::default(), window, cx)
7726 });
7727 cx.run_until_parked();
7728
7729 cx.update(|_, cx| {
7730 assert_eq!(
7731 buffer.read(cx).text(),
7732 indoc! { "
7733 impl A {
7734
7735 // a
7736 fn b(){}
7737
7738
7739 }
7740 fn c(){}
7741
7742 " }
7743 )
7744 });
7745 }
7746}
7747
7748#[gpui::test]
7749async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
7750 init_test(cx, |_| {});
7751
7752 let mut cx = EditorTestContext::new(cx).await;
7753
7754 let language = Arc::new(Language::new(
7755 LanguageConfig {
7756 brackets: BracketPairConfig {
7757 pairs: vec![
7758 BracketPair {
7759 start: "{".to_string(),
7760 end: "}".to_string(),
7761 close: true,
7762 surround: true,
7763 newline: true,
7764 },
7765 BracketPair {
7766 start: "(".to_string(),
7767 end: ")".to_string(),
7768 close: true,
7769 surround: true,
7770 newline: true,
7771 },
7772 BracketPair {
7773 start: "/*".to_string(),
7774 end: " */".to_string(),
7775 close: true,
7776 surround: true,
7777 newline: true,
7778 },
7779 BracketPair {
7780 start: "[".to_string(),
7781 end: "]".to_string(),
7782 close: false,
7783 surround: false,
7784 newline: true,
7785 },
7786 BracketPair {
7787 start: "\"".to_string(),
7788 end: "\"".to_string(),
7789 close: true,
7790 surround: true,
7791 newline: false,
7792 },
7793 BracketPair {
7794 start: "<".to_string(),
7795 end: ">".to_string(),
7796 close: false,
7797 surround: true,
7798 newline: true,
7799 },
7800 ],
7801 ..Default::default()
7802 },
7803 autoclose_before: "})]".to_string(),
7804 ..Default::default()
7805 },
7806 Some(tree_sitter_rust::LANGUAGE.into()),
7807 ));
7808
7809 cx.language_registry().add(language.clone());
7810 cx.update_buffer(|buffer, cx| {
7811 buffer.set_language(Some(language), cx);
7812 });
7813
7814 cx.set_state(
7815 &r#"
7816 🏀ˇ
7817 εˇ
7818 ❤️ˇ
7819 "#
7820 .unindent(),
7821 );
7822
7823 // autoclose multiple nested brackets at multiple cursors
7824 cx.update_editor(|editor, window, cx| {
7825 editor.handle_input("{", window, cx);
7826 editor.handle_input("{", window, cx);
7827 editor.handle_input("{", window, cx);
7828 });
7829 cx.assert_editor_state(
7830 &"
7831 🏀{{{ˇ}}}
7832 ε{{{ˇ}}}
7833 ❤️{{{ˇ}}}
7834 "
7835 .unindent(),
7836 );
7837
7838 // insert a different closing bracket
7839 cx.update_editor(|editor, window, cx| {
7840 editor.handle_input(")", window, cx);
7841 });
7842 cx.assert_editor_state(
7843 &"
7844 🏀{{{)ˇ}}}
7845 ε{{{)ˇ}}}
7846 ❤️{{{)ˇ}}}
7847 "
7848 .unindent(),
7849 );
7850
7851 // skip over the auto-closed brackets when typing a closing bracket
7852 cx.update_editor(|editor, window, cx| {
7853 editor.move_right(&MoveRight, window, cx);
7854 editor.handle_input("}", window, cx);
7855 editor.handle_input("}", window, cx);
7856 editor.handle_input("}", window, cx);
7857 });
7858 cx.assert_editor_state(
7859 &"
7860 🏀{{{)}}}}ˇ
7861 ε{{{)}}}}ˇ
7862 ❤️{{{)}}}}ˇ
7863 "
7864 .unindent(),
7865 );
7866
7867 // autoclose multi-character pairs
7868 cx.set_state(
7869 &"
7870 ˇ
7871 ˇ
7872 "
7873 .unindent(),
7874 );
7875 cx.update_editor(|editor, window, cx| {
7876 editor.handle_input("/", window, cx);
7877 editor.handle_input("*", window, cx);
7878 });
7879 cx.assert_editor_state(
7880 &"
7881 /*ˇ */
7882 /*ˇ */
7883 "
7884 .unindent(),
7885 );
7886
7887 // one cursor autocloses a multi-character pair, one cursor
7888 // does not autoclose.
7889 cx.set_state(
7890 &"
7891 /ˇ
7892 ˇ
7893 "
7894 .unindent(),
7895 );
7896 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
7897 cx.assert_editor_state(
7898 &"
7899 /*ˇ */
7900 *ˇ
7901 "
7902 .unindent(),
7903 );
7904
7905 // Don't autoclose if the next character isn't whitespace and isn't
7906 // listed in the language's "autoclose_before" section.
7907 cx.set_state("ˇa b");
7908 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7909 cx.assert_editor_state("{ˇa b");
7910
7911 // Don't autoclose if `close` is false for the bracket pair
7912 cx.set_state("ˇ");
7913 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
7914 cx.assert_editor_state("[ˇ");
7915
7916 // Surround with brackets if text is selected
7917 cx.set_state("«aˇ» b");
7918 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7919 cx.assert_editor_state("{«aˇ»} b");
7920
7921 // Autoclose when not immediately after a word character
7922 cx.set_state("a ˇ");
7923 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7924 cx.assert_editor_state("a \"ˇ\"");
7925
7926 // Autoclose pair where the start and end characters are the same
7927 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7928 cx.assert_editor_state("a \"\"ˇ");
7929
7930 // Don't autoclose when immediately after a word character
7931 cx.set_state("aˇ");
7932 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7933 cx.assert_editor_state("a\"ˇ");
7934
7935 // Do autoclose when after a non-word character
7936 cx.set_state("{ˇ");
7937 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7938 cx.assert_editor_state("{\"ˇ\"");
7939
7940 // Non identical pairs autoclose regardless of preceding character
7941 cx.set_state("aˇ");
7942 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7943 cx.assert_editor_state("a{ˇ}");
7944
7945 // Don't autoclose pair if autoclose is disabled
7946 cx.set_state("ˇ");
7947 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
7948 cx.assert_editor_state("<ˇ");
7949
7950 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
7951 cx.set_state("«aˇ» b");
7952 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
7953 cx.assert_editor_state("<«aˇ»> b");
7954}
7955
7956#[gpui::test]
7957async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
7958 init_test(cx, |settings| {
7959 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
7960 });
7961
7962 let mut cx = EditorTestContext::new(cx).await;
7963
7964 let language = Arc::new(Language::new(
7965 LanguageConfig {
7966 brackets: BracketPairConfig {
7967 pairs: vec![
7968 BracketPair {
7969 start: "{".to_string(),
7970 end: "}".to_string(),
7971 close: true,
7972 surround: true,
7973 newline: true,
7974 },
7975 BracketPair {
7976 start: "(".to_string(),
7977 end: ")".to_string(),
7978 close: true,
7979 surround: true,
7980 newline: true,
7981 },
7982 BracketPair {
7983 start: "[".to_string(),
7984 end: "]".to_string(),
7985 close: false,
7986 surround: false,
7987 newline: true,
7988 },
7989 ],
7990 ..Default::default()
7991 },
7992 autoclose_before: "})]".to_string(),
7993 ..Default::default()
7994 },
7995 Some(tree_sitter_rust::LANGUAGE.into()),
7996 ));
7997
7998 cx.language_registry().add(language.clone());
7999 cx.update_buffer(|buffer, cx| {
8000 buffer.set_language(Some(language), cx);
8001 });
8002
8003 cx.set_state(
8004 &"
8005 ˇ
8006 ˇ
8007 ˇ
8008 "
8009 .unindent(),
8010 );
8011
8012 // ensure only matching closing brackets are skipped over
8013 cx.update_editor(|editor, window, cx| {
8014 editor.handle_input("}", window, cx);
8015 editor.move_left(&MoveLeft, window, cx);
8016 editor.handle_input(")", window, cx);
8017 editor.move_left(&MoveLeft, window, cx);
8018 });
8019 cx.assert_editor_state(
8020 &"
8021 ˇ)}
8022 ˇ)}
8023 ˇ)}
8024 "
8025 .unindent(),
8026 );
8027
8028 // skip-over closing brackets at multiple cursors
8029 cx.update_editor(|editor, window, cx| {
8030 editor.handle_input(")", window, cx);
8031 editor.handle_input("}", window, cx);
8032 });
8033 cx.assert_editor_state(
8034 &"
8035 )}ˇ
8036 )}ˇ
8037 )}ˇ
8038 "
8039 .unindent(),
8040 );
8041
8042 // ignore non-close brackets
8043 cx.update_editor(|editor, window, cx| {
8044 editor.handle_input("]", window, cx);
8045 editor.move_left(&MoveLeft, window, cx);
8046 editor.handle_input("]", window, cx);
8047 });
8048 cx.assert_editor_state(
8049 &"
8050 )}]ˇ]
8051 )}]ˇ]
8052 )}]ˇ]
8053 "
8054 .unindent(),
8055 );
8056}
8057
8058#[gpui::test]
8059async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
8060 init_test(cx, |_| {});
8061
8062 let mut cx = EditorTestContext::new(cx).await;
8063
8064 let html_language = Arc::new(
8065 Language::new(
8066 LanguageConfig {
8067 name: "HTML".into(),
8068 brackets: BracketPairConfig {
8069 pairs: vec![
8070 BracketPair {
8071 start: "<".into(),
8072 end: ">".into(),
8073 close: true,
8074 ..Default::default()
8075 },
8076 BracketPair {
8077 start: "{".into(),
8078 end: "}".into(),
8079 close: true,
8080 ..Default::default()
8081 },
8082 BracketPair {
8083 start: "(".into(),
8084 end: ")".into(),
8085 close: true,
8086 ..Default::default()
8087 },
8088 ],
8089 ..Default::default()
8090 },
8091 autoclose_before: "})]>".into(),
8092 ..Default::default()
8093 },
8094 Some(tree_sitter_html::LANGUAGE.into()),
8095 )
8096 .with_injection_query(
8097 r#"
8098 (script_element
8099 (raw_text) @injection.content
8100 (#set! injection.language "javascript"))
8101 "#,
8102 )
8103 .unwrap(),
8104 );
8105
8106 let javascript_language = Arc::new(Language::new(
8107 LanguageConfig {
8108 name: "JavaScript".into(),
8109 brackets: BracketPairConfig {
8110 pairs: vec![
8111 BracketPair {
8112 start: "/*".into(),
8113 end: " */".into(),
8114 close: true,
8115 ..Default::default()
8116 },
8117 BracketPair {
8118 start: "{".into(),
8119 end: "}".into(),
8120 close: true,
8121 ..Default::default()
8122 },
8123 BracketPair {
8124 start: "(".into(),
8125 end: ")".into(),
8126 close: true,
8127 ..Default::default()
8128 },
8129 ],
8130 ..Default::default()
8131 },
8132 autoclose_before: "})]>".into(),
8133 ..Default::default()
8134 },
8135 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
8136 ));
8137
8138 cx.language_registry().add(html_language.clone());
8139 cx.language_registry().add(javascript_language.clone());
8140
8141 cx.update_buffer(|buffer, cx| {
8142 buffer.set_language(Some(html_language), cx);
8143 });
8144
8145 cx.set_state(
8146 &r#"
8147 <body>ˇ
8148 <script>
8149 var x = 1;ˇ
8150 </script>
8151 </body>ˇ
8152 "#
8153 .unindent(),
8154 );
8155
8156 // Precondition: different languages are active at different locations.
8157 cx.update_editor(|editor, window, cx| {
8158 let snapshot = editor.snapshot(window, cx);
8159 let cursors = editor.selections.ranges::<usize>(cx);
8160 let languages = cursors
8161 .iter()
8162 .map(|c| snapshot.language_at(c.start).unwrap().name())
8163 .collect::<Vec<_>>();
8164 assert_eq!(
8165 languages,
8166 &["HTML".into(), "JavaScript".into(), "HTML".into()]
8167 );
8168 });
8169
8170 // Angle brackets autoclose in HTML, but not JavaScript.
8171 cx.update_editor(|editor, window, cx| {
8172 editor.handle_input("<", window, cx);
8173 editor.handle_input("a", window, cx);
8174 });
8175 cx.assert_editor_state(
8176 &r#"
8177 <body><aˇ>
8178 <script>
8179 var x = 1;<aˇ
8180 </script>
8181 </body><aˇ>
8182 "#
8183 .unindent(),
8184 );
8185
8186 // Curly braces and parens autoclose in both HTML and JavaScript.
8187 cx.update_editor(|editor, window, cx| {
8188 editor.handle_input(" b=", window, cx);
8189 editor.handle_input("{", window, cx);
8190 editor.handle_input("c", window, cx);
8191 editor.handle_input("(", window, cx);
8192 });
8193 cx.assert_editor_state(
8194 &r#"
8195 <body><a b={c(ˇ)}>
8196 <script>
8197 var x = 1;<a b={c(ˇ)}
8198 </script>
8199 </body><a b={c(ˇ)}>
8200 "#
8201 .unindent(),
8202 );
8203
8204 // Brackets that were already autoclosed are skipped.
8205 cx.update_editor(|editor, window, cx| {
8206 editor.handle_input(")", window, cx);
8207 editor.handle_input("d", window, cx);
8208 editor.handle_input("}", window, cx);
8209 });
8210 cx.assert_editor_state(
8211 &r#"
8212 <body><a b={c()d}ˇ>
8213 <script>
8214 var x = 1;<a b={c()d}ˇ
8215 </script>
8216 </body><a b={c()d}ˇ>
8217 "#
8218 .unindent(),
8219 );
8220 cx.update_editor(|editor, window, cx| {
8221 editor.handle_input(">", window, cx);
8222 });
8223 cx.assert_editor_state(
8224 &r#"
8225 <body><a b={c()d}>ˇ
8226 <script>
8227 var x = 1;<a b={c()d}>ˇ
8228 </script>
8229 </body><a b={c()d}>ˇ
8230 "#
8231 .unindent(),
8232 );
8233
8234 // Reset
8235 cx.set_state(
8236 &r#"
8237 <body>ˇ
8238 <script>
8239 var x = 1;ˇ
8240 </script>
8241 </body>ˇ
8242 "#
8243 .unindent(),
8244 );
8245
8246 cx.update_editor(|editor, window, cx| {
8247 editor.handle_input("<", window, cx);
8248 });
8249 cx.assert_editor_state(
8250 &r#"
8251 <body><ˇ>
8252 <script>
8253 var x = 1;<ˇ
8254 </script>
8255 </body><ˇ>
8256 "#
8257 .unindent(),
8258 );
8259
8260 // When backspacing, the closing angle brackets are removed.
8261 cx.update_editor(|editor, window, cx| {
8262 editor.backspace(&Backspace, window, cx);
8263 });
8264 cx.assert_editor_state(
8265 &r#"
8266 <body>ˇ
8267 <script>
8268 var x = 1;ˇ
8269 </script>
8270 </body>ˇ
8271 "#
8272 .unindent(),
8273 );
8274
8275 // Block comments autoclose in JavaScript, but not HTML.
8276 cx.update_editor(|editor, window, cx| {
8277 editor.handle_input("/", window, cx);
8278 editor.handle_input("*", window, cx);
8279 });
8280 cx.assert_editor_state(
8281 &r#"
8282 <body>/*ˇ
8283 <script>
8284 var x = 1;/*ˇ */
8285 </script>
8286 </body>/*ˇ
8287 "#
8288 .unindent(),
8289 );
8290}
8291
8292#[gpui::test]
8293async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
8294 init_test(cx, |_| {});
8295
8296 let mut cx = EditorTestContext::new(cx).await;
8297
8298 let rust_language = Arc::new(
8299 Language::new(
8300 LanguageConfig {
8301 name: "Rust".into(),
8302 brackets: serde_json::from_value(json!([
8303 { "start": "{", "end": "}", "close": true, "newline": true },
8304 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
8305 ]))
8306 .unwrap(),
8307 autoclose_before: "})]>".into(),
8308 ..Default::default()
8309 },
8310 Some(tree_sitter_rust::LANGUAGE.into()),
8311 )
8312 .with_override_query("(string_literal) @string")
8313 .unwrap(),
8314 );
8315
8316 cx.language_registry().add(rust_language.clone());
8317 cx.update_buffer(|buffer, cx| {
8318 buffer.set_language(Some(rust_language), cx);
8319 });
8320
8321 cx.set_state(
8322 &r#"
8323 let x = ˇ
8324 "#
8325 .unindent(),
8326 );
8327
8328 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
8329 cx.update_editor(|editor, window, cx| {
8330 editor.handle_input("\"", window, cx);
8331 });
8332 cx.assert_editor_state(
8333 &r#"
8334 let x = "ˇ"
8335 "#
8336 .unindent(),
8337 );
8338
8339 // Inserting another quotation mark. The cursor moves across the existing
8340 // automatically-inserted quotation mark.
8341 cx.update_editor(|editor, window, cx| {
8342 editor.handle_input("\"", window, cx);
8343 });
8344 cx.assert_editor_state(
8345 &r#"
8346 let x = ""ˇ
8347 "#
8348 .unindent(),
8349 );
8350
8351 // Reset
8352 cx.set_state(
8353 &r#"
8354 let x = ˇ
8355 "#
8356 .unindent(),
8357 );
8358
8359 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
8360 cx.update_editor(|editor, window, cx| {
8361 editor.handle_input("\"", window, cx);
8362 editor.handle_input(" ", window, cx);
8363 editor.move_left(&Default::default(), window, cx);
8364 editor.handle_input("\\", window, cx);
8365 editor.handle_input("\"", window, cx);
8366 });
8367 cx.assert_editor_state(
8368 &r#"
8369 let x = "\"ˇ "
8370 "#
8371 .unindent(),
8372 );
8373
8374 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
8375 // mark. Nothing is inserted.
8376 cx.update_editor(|editor, window, cx| {
8377 editor.move_right(&Default::default(), window, cx);
8378 editor.handle_input("\"", window, cx);
8379 });
8380 cx.assert_editor_state(
8381 &r#"
8382 let x = "\" "ˇ
8383 "#
8384 .unindent(),
8385 );
8386}
8387
8388#[gpui::test]
8389async fn test_surround_with_pair(cx: &mut TestAppContext) {
8390 init_test(cx, |_| {});
8391
8392 let language = Arc::new(Language::new(
8393 LanguageConfig {
8394 brackets: BracketPairConfig {
8395 pairs: vec![
8396 BracketPair {
8397 start: "{".to_string(),
8398 end: "}".to_string(),
8399 close: true,
8400 surround: true,
8401 newline: true,
8402 },
8403 BracketPair {
8404 start: "/* ".to_string(),
8405 end: "*/".to_string(),
8406 close: true,
8407 surround: true,
8408 ..Default::default()
8409 },
8410 ],
8411 ..Default::default()
8412 },
8413 ..Default::default()
8414 },
8415 Some(tree_sitter_rust::LANGUAGE.into()),
8416 ));
8417
8418 let text = r#"
8419 a
8420 b
8421 c
8422 "#
8423 .unindent();
8424
8425 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8426 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8427 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8428 editor
8429 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8430 .await;
8431
8432 editor.update_in(cx, |editor, window, cx| {
8433 editor.change_selections(None, window, cx, |s| {
8434 s.select_display_ranges([
8435 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8436 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8437 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
8438 ])
8439 });
8440
8441 editor.handle_input("{", window, cx);
8442 editor.handle_input("{", window, cx);
8443 editor.handle_input("{", window, cx);
8444 assert_eq!(
8445 editor.text(cx),
8446 "
8447 {{{a}}}
8448 {{{b}}}
8449 {{{c}}}
8450 "
8451 .unindent()
8452 );
8453 assert_eq!(
8454 editor.selections.display_ranges(cx),
8455 [
8456 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
8457 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
8458 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
8459 ]
8460 );
8461
8462 editor.undo(&Undo, window, cx);
8463 editor.undo(&Undo, window, cx);
8464 editor.undo(&Undo, window, cx);
8465 assert_eq!(
8466 editor.text(cx),
8467 "
8468 a
8469 b
8470 c
8471 "
8472 .unindent()
8473 );
8474 assert_eq!(
8475 editor.selections.display_ranges(cx),
8476 [
8477 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8478 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8479 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8480 ]
8481 );
8482
8483 // Ensure inserting the first character of a multi-byte bracket pair
8484 // doesn't surround the selections with the bracket.
8485 editor.handle_input("/", window, cx);
8486 assert_eq!(
8487 editor.text(cx),
8488 "
8489 /
8490 /
8491 /
8492 "
8493 .unindent()
8494 );
8495 assert_eq!(
8496 editor.selections.display_ranges(cx),
8497 [
8498 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8499 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8500 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8501 ]
8502 );
8503
8504 editor.undo(&Undo, window, cx);
8505 assert_eq!(
8506 editor.text(cx),
8507 "
8508 a
8509 b
8510 c
8511 "
8512 .unindent()
8513 );
8514 assert_eq!(
8515 editor.selections.display_ranges(cx),
8516 [
8517 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8518 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8519 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8520 ]
8521 );
8522
8523 // Ensure inserting the last character of a multi-byte bracket pair
8524 // doesn't surround the selections with the bracket.
8525 editor.handle_input("*", window, cx);
8526 assert_eq!(
8527 editor.text(cx),
8528 "
8529 *
8530 *
8531 *
8532 "
8533 .unindent()
8534 );
8535 assert_eq!(
8536 editor.selections.display_ranges(cx),
8537 [
8538 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8539 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8540 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8541 ]
8542 );
8543 });
8544}
8545
8546#[gpui::test]
8547async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
8548 init_test(cx, |_| {});
8549
8550 let language = Arc::new(Language::new(
8551 LanguageConfig {
8552 brackets: BracketPairConfig {
8553 pairs: vec![BracketPair {
8554 start: "{".to_string(),
8555 end: "}".to_string(),
8556 close: true,
8557 surround: true,
8558 newline: true,
8559 }],
8560 ..Default::default()
8561 },
8562 autoclose_before: "}".to_string(),
8563 ..Default::default()
8564 },
8565 Some(tree_sitter_rust::LANGUAGE.into()),
8566 ));
8567
8568 let text = r#"
8569 a
8570 b
8571 c
8572 "#
8573 .unindent();
8574
8575 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8576 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8577 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8578 editor
8579 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8580 .await;
8581
8582 editor.update_in(cx, |editor, window, cx| {
8583 editor.change_selections(None, window, cx, |s| {
8584 s.select_ranges([
8585 Point::new(0, 1)..Point::new(0, 1),
8586 Point::new(1, 1)..Point::new(1, 1),
8587 Point::new(2, 1)..Point::new(2, 1),
8588 ])
8589 });
8590
8591 editor.handle_input("{", window, cx);
8592 editor.handle_input("{", window, cx);
8593 editor.handle_input("_", window, cx);
8594 assert_eq!(
8595 editor.text(cx),
8596 "
8597 a{{_}}
8598 b{{_}}
8599 c{{_}}
8600 "
8601 .unindent()
8602 );
8603 assert_eq!(
8604 editor.selections.ranges::<Point>(cx),
8605 [
8606 Point::new(0, 4)..Point::new(0, 4),
8607 Point::new(1, 4)..Point::new(1, 4),
8608 Point::new(2, 4)..Point::new(2, 4)
8609 ]
8610 );
8611
8612 editor.backspace(&Default::default(), window, cx);
8613 editor.backspace(&Default::default(), window, cx);
8614 assert_eq!(
8615 editor.text(cx),
8616 "
8617 a{}
8618 b{}
8619 c{}
8620 "
8621 .unindent()
8622 );
8623 assert_eq!(
8624 editor.selections.ranges::<Point>(cx),
8625 [
8626 Point::new(0, 2)..Point::new(0, 2),
8627 Point::new(1, 2)..Point::new(1, 2),
8628 Point::new(2, 2)..Point::new(2, 2)
8629 ]
8630 );
8631
8632 editor.delete_to_previous_word_start(&Default::default(), window, cx);
8633 assert_eq!(
8634 editor.text(cx),
8635 "
8636 a
8637 b
8638 c
8639 "
8640 .unindent()
8641 );
8642 assert_eq!(
8643 editor.selections.ranges::<Point>(cx),
8644 [
8645 Point::new(0, 1)..Point::new(0, 1),
8646 Point::new(1, 1)..Point::new(1, 1),
8647 Point::new(2, 1)..Point::new(2, 1)
8648 ]
8649 );
8650 });
8651}
8652
8653#[gpui::test]
8654async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
8655 init_test(cx, |settings| {
8656 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
8657 });
8658
8659 let mut cx = EditorTestContext::new(cx).await;
8660
8661 let language = Arc::new(Language::new(
8662 LanguageConfig {
8663 brackets: BracketPairConfig {
8664 pairs: vec![
8665 BracketPair {
8666 start: "{".to_string(),
8667 end: "}".to_string(),
8668 close: true,
8669 surround: true,
8670 newline: true,
8671 },
8672 BracketPair {
8673 start: "(".to_string(),
8674 end: ")".to_string(),
8675 close: true,
8676 surround: true,
8677 newline: true,
8678 },
8679 BracketPair {
8680 start: "[".to_string(),
8681 end: "]".to_string(),
8682 close: false,
8683 surround: true,
8684 newline: true,
8685 },
8686 ],
8687 ..Default::default()
8688 },
8689 autoclose_before: "})]".to_string(),
8690 ..Default::default()
8691 },
8692 Some(tree_sitter_rust::LANGUAGE.into()),
8693 ));
8694
8695 cx.language_registry().add(language.clone());
8696 cx.update_buffer(|buffer, cx| {
8697 buffer.set_language(Some(language), cx);
8698 });
8699
8700 cx.set_state(
8701 &"
8702 {(ˇ)}
8703 [[ˇ]]
8704 {(ˇ)}
8705 "
8706 .unindent(),
8707 );
8708
8709 cx.update_editor(|editor, window, cx| {
8710 editor.backspace(&Default::default(), window, cx);
8711 editor.backspace(&Default::default(), window, cx);
8712 });
8713
8714 cx.assert_editor_state(
8715 &"
8716 ˇ
8717 ˇ]]
8718 ˇ
8719 "
8720 .unindent(),
8721 );
8722
8723 cx.update_editor(|editor, window, cx| {
8724 editor.handle_input("{", window, cx);
8725 editor.handle_input("{", window, cx);
8726 editor.move_right(&MoveRight, window, cx);
8727 editor.move_right(&MoveRight, window, cx);
8728 editor.move_left(&MoveLeft, window, cx);
8729 editor.move_left(&MoveLeft, window, cx);
8730 editor.backspace(&Default::default(), window, cx);
8731 });
8732
8733 cx.assert_editor_state(
8734 &"
8735 {ˇ}
8736 {ˇ}]]
8737 {ˇ}
8738 "
8739 .unindent(),
8740 );
8741
8742 cx.update_editor(|editor, window, cx| {
8743 editor.backspace(&Default::default(), window, cx);
8744 });
8745
8746 cx.assert_editor_state(
8747 &"
8748 ˇ
8749 ˇ]]
8750 ˇ
8751 "
8752 .unindent(),
8753 );
8754}
8755
8756#[gpui::test]
8757async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
8758 init_test(cx, |_| {});
8759
8760 let language = Arc::new(Language::new(
8761 LanguageConfig::default(),
8762 Some(tree_sitter_rust::LANGUAGE.into()),
8763 ));
8764
8765 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
8766 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8767 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8768 editor
8769 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8770 .await;
8771
8772 editor.update_in(cx, |editor, window, cx| {
8773 editor.set_auto_replace_emoji_shortcode(true);
8774
8775 editor.handle_input("Hello ", window, cx);
8776 editor.handle_input(":wave", window, cx);
8777 assert_eq!(editor.text(cx), "Hello :wave".unindent());
8778
8779 editor.handle_input(":", window, cx);
8780 assert_eq!(editor.text(cx), "Hello 👋".unindent());
8781
8782 editor.handle_input(" :smile", window, cx);
8783 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
8784
8785 editor.handle_input(":", window, cx);
8786 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
8787
8788 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
8789 editor.handle_input(":wave", window, cx);
8790 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
8791
8792 editor.handle_input(":", window, cx);
8793 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
8794
8795 editor.handle_input(":1", window, cx);
8796 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
8797
8798 editor.handle_input(":", window, cx);
8799 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
8800
8801 // Ensure shortcode does not get replaced when it is part of a word
8802 editor.handle_input(" Test:wave", window, cx);
8803 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
8804
8805 editor.handle_input(":", window, cx);
8806 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
8807
8808 editor.set_auto_replace_emoji_shortcode(false);
8809
8810 // Ensure shortcode does not get replaced when auto replace is off
8811 editor.handle_input(" :wave", window, cx);
8812 assert_eq!(
8813 editor.text(cx),
8814 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
8815 );
8816
8817 editor.handle_input(":", window, cx);
8818 assert_eq!(
8819 editor.text(cx),
8820 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
8821 );
8822 });
8823}
8824
8825#[gpui::test]
8826async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
8827 init_test(cx, |_| {});
8828
8829 let (text, insertion_ranges) = marked_text_ranges(
8830 indoc! {"
8831 ˇ
8832 "},
8833 false,
8834 );
8835
8836 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
8837 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8838
8839 _ = editor.update_in(cx, |editor, window, cx| {
8840 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
8841
8842 editor
8843 .insert_snippet(&insertion_ranges, snippet, window, cx)
8844 .unwrap();
8845
8846 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
8847 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
8848 assert_eq!(editor.text(cx), expected_text);
8849 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
8850 }
8851
8852 assert(
8853 editor,
8854 cx,
8855 indoc! {"
8856 type «» =•
8857 "},
8858 );
8859
8860 assert!(editor.context_menu_visible(), "There should be a matches");
8861 });
8862}
8863
8864#[gpui::test]
8865async fn test_snippets(cx: &mut TestAppContext) {
8866 init_test(cx, |_| {});
8867
8868 let mut cx = EditorTestContext::new(cx).await;
8869
8870 cx.set_state(indoc! {"
8871 a.ˇ b
8872 a.ˇ b
8873 a.ˇ b
8874 "});
8875
8876 cx.update_editor(|editor, window, cx| {
8877 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
8878 let insertion_ranges = editor
8879 .selections
8880 .all(cx)
8881 .iter()
8882 .map(|s| s.range().clone())
8883 .collect::<Vec<_>>();
8884 editor
8885 .insert_snippet(&insertion_ranges, snippet, window, cx)
8886 .unwrap();
8887 });
8888
8889 cx.assert_editor_state(indoc! {"
8890 a.f(«oneˇ», two, «threeˇ») b
8891 a.f(«oneˇ», two, «threeˇ») b
8892 a.f(«oneˇ», two, «threeˇ») b
8893 "});
8894
8895 // Can't move earlier than the first tab stop
8896 cx.update_editor(|editor, window, cx| {
8897 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
8898 });
8899 cx.assert_editor_state(indoc! {"
8900 a.f(«oneˇ», two, «threeˇ») b
8901 a.f(«oneˇ», two, «threeˇ») b
8902 a.f(«oneˇ», two, «threeˇ») b
8903 "});
8904
8905 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
8906 cx.assert_editor_state(indoc! {"
8907 a.f(one, «twoˇ», three) b
8908 a.f(one, «twoˇ», three) b
8909 a.f(one, «twoˇ», three) b
8910 "});
8911
8912 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
8913 cx.assert_editor_state(indoc! {"
8914 a.f(«oneˇ», two, «threeˇ») b
8915 a.f(«oneˇ», two, «threeˇ») b
8916 a.f(«oneˇ», two, «threeˇ») b
8917 "});
8918
8919 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
8920 cx.assert_editor_state(indoc! {"
8921 a.f(one, «twoˇ», three) b
8922 a.f(one, «twoˇ», three) b
8923 a.f(one, «twoˇ», three) b
8924 "});
8925 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
8926 cx.assert_editor_state(indoc! {"
8927 a.f(one, two, three)ˇ b
8928 a.f(one, two, three)ˇ b
8929 a.f(one, two, three)ˇ b
8930 "});
8931
8932 // As soon as the last tab stop is reached, snippet state is gone
8933 cx.update_editor(|editor, window, cx| {
8934 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
8935 });
8936 cx.assert_editor_state(indoc! {"
8937 a.f(one, two, three)ˇ b
8938 a.f(one, two, three)ˇ b
8939 a.f(one, two, three)ˇ b
8940 "});
8941}
8942
8943#[gpui::test]
8944async fn test_snippet_indentation(cx: &mut TestAppContext) {
8945 init_test(cx, |_| {});
8946
8947 let mut cx = EditorTestContext::new(cx).await;
8948
8949 cx.update_editor(|editor, window, cx| {
8950 let snippet = Snippet::parse(indoc! {"
8951 /*
8952 * Multiline comment with leading indentation
8953 *
8954 * $1
8955 */
8956 $0"})
8957 .unwrap();
8958 let insertion_ranges = editor
8959 .selections
8960 .all(cx)
8961 .iter()
8962 .map(|s| s.range().clone())
8963 .collect::<Vec<_>>();
8964 editor
8965 .insert_snippet(&insertion_ranges, snippet, window, cx)
8966 .unwrap();
8967 });
8968
8969 cx.assert_editor_state(indoc! {"
8970 /*
8971 * Multiline comment with leading indentation
8972 *
8973 * ˇ
8974 */
8975 "});
8976
8977 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
8978 cx.assert_editor_state(indoc! {"
8979 /*
8980 * Multiline comment with leading indentation
8981 *
8982 *•
8983 */
8984 ˇ"});
8985}
8986
8987#[gpui::test]
8988async fn test_document_format_during_save(cx: &mut TestAppContext) {
8989 init_test(cx, |_| {});
8990
8991 let fs = FakeFs::new(cx.executor());
8992 fs.insert_file(path!("/file.rs"), Default::default()).await;
8993
8994 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
8995
8996 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8997 language_registry.add(rust_lang());
8998 let mut fake_servers = language_registry.register_fake_lsp(
8999 "Rust",
9000 FakeLspAdapter {
9001 capabilities: lsp::ServerCapabilities {
9002 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9003 ..Default::default()
9004 },
9005 ..Default::default()
9006 },
9007 );
9008
9009 let buffer = project
9010 .update(cx, |project, cx| {
9011 project.open_local_buffer(path!("/file.rs"), cx)
9012 })
9013 .await
9014 .unwrap();
9015
9016 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9017 let (editor, cx) = cx.add_window_view(|window, cx| {
9018 build_editor_with_project(project.clone(), buffer, window, cx)
9019 });
9020 editor.update_in(cx, |editor, window, cx| {
9021 editor.set_text("one\ntwo\nthree\n", window, cx)
9022 });
9023 assert!(cx.read(|cx| editor.is_dirty(cx)));
9024
9025 cx.executor().start_waiting();
9026 let fake_server = fake_servers.next().await.unwrap();
9027
9028 {
9029 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9030 move |params, _| async move {
9031 assert_eq!(
9032 params.text_document.uri,
9033 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9034 );
9035 assert_eq!(params.options.tab_size, 4);
9036 Ok(Some(vec![lsp::TextEdit::new(
9037 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9038 ", ".to_string(),
9039 )]))
9040 },
9041 );
9042 let save = editor
9043 .update_in(cx, |editor, window, cx| {
9044 editor.save(
9045 SaveOptions {
9046 format: true,
9047 autosave: false,
9048 },
9049 project.clone(),
9050 window,
9051 cx,
9052 )
9053 })
9054 .unwrap();
9055 cx.executor().start_waiting();
9056 save.await;
9057
9058 assert_eq!(
9059 editor.update(cx, |editor, cx| editor.text(cx)),
9060 "one, two\nthree\n"
9061 );
9062 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9063 }
9064
9065 {
9066 editor.update_in(cx, |editor, window, cx| {
9067 editor.set_text("one\ntwo\nthree\n", window, cx)
9068 });
9069 assert!(cx.read(|cx| editor.is_dirty(cx)));
9070
9071 // Ensure we can still save even if formatting hangs.
9072 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9073 move |params, _| async move {
9074 assert_eq!(
9075 params.text_document.uri,
9076 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9077 );
9078 futures::future::pending::<()>().await;
9079 unreachable!()
9080 },
9081 );
9082 let save = editor
9083 .update_in(cx, |editor, window, cx| {
9084 editor.save(
9085 SaveOptions {
9086 format: true,
9087 autosave: false,
9088 },
9089 project.clone(),
9090 window,
9091 cx,
9092 )
9093 })
9094 .unwrap();
9095 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9096 cx.executor().start_waiting();
9097 save.await;
9098 assert_eq!(
9099 editor.update(cx, |editor, cx| editor.text(cx)),
9100 "one\ntwo\nthree\n"
9101 );
9102 }
9103
9104 // Set rust language override and assert overridden tabsize is sent to language server
9105 update_test_language_settings(cx, |settings| {
9106 settings.languages.insert(
9107 "Rust".into(),
9108 LanguageSettingsContent {
9109 tab_size: NonZeroU32::new(8),
9110 ..Default::default()
9111 },
9112 );
9113 });
9114
9115 {
9116 editor.update_in(cx, |editor, window, cx| {
9117 editor.set_text("somehting_new\n", window, cx)
9118 });
9119 assert!(cx.read(|cx| editor.is_dirty(cx)));
9120 let _formatting_request_signal = fake_server
9121 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
9122 assert_eq!(
9123 params.text_document.uri,
9124 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9125 );
9126 assert_eq!(params.options.tab_size, 8);
9127 Ok(Some(vec![]))
9128 });
9129 let save = editor
9130 .update_in(cx, |editor, window, cx| {
9131 editor.save(
9132 SaveOptions {
9133 format: true,
9134 autosave: false,
9135 },
9136 project.clone(),
9137 window,
9138 cx,
9139 )
9140 })
9141 .unwrap();
9142 cx.executor().start_waiting();
9143 save.await;
9144 }
9145}
9146
9147#[gpui::test]
9148async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
9149 init_test(cx, |_| {});
9150
9151 let cols = 4;
9152 let rows = 10;
9153 let sample_text_1 = sample_text(rows, cols, 'a');
9154 assert_eq!(
9155 sample_text_1,
9156 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
9157 );
9158 let sample_text_2 = sample_text(rows, cols, 'l');
9159 assert_eq!(
9160 sample_text_2,
9161 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
9162 );
9163 let sample_text_3 = sample_text(rows, cols, 'v');
9164 assert_eq!(
9165 sample_text_3,
9166 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
9167 );
9168
9169 let fs = FakeFs::new(cx.executor());
9170 fs.insert_tree(
9171 path!("/a"),
9172 json!({
9173 "main.rs": sample_text_1,
9174 "other.rs": sample_text_2,
9175 "lib.rs": sample_text_3,
9176 }),
9177 )
9178 .await;
9179
9180 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
9181 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9182 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
9183
9184 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9185 language_registry.add(rust_lang());
9186 let mut fake_servers = language_registry.register_fake_lsp(
9187 "Rust",
9188 FakeLspAdapter {
9189 capabilities: lsp::ServerCapabilities {
9190 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9191 ..Default::default()
9192 },
9193 ..Default::default()
9194 },
9195 );
9196
9197 let worktree = project.update(cx, |project, cx| {
9198 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
9199 assert_eq!(worktrees.len(), 1);
9200 worktrees.pop().unwrap()
9201 });
9202 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
9203
9204 let buffer_1 = project
9205 .update(cx, |project, cx| {
9206 project.open_buffer((worktree_id, "main.rs"), cx)
9207 })
9208 .await
9209 .unwrap();
9210 let buffer_2 = project
9211 .update(cx, |project, cx| {
9212 project.open_buffer((worktree_id, "other.rs"), cx)
9213 })
9214 .await
9215 .unwrap();
9216 let buffer_3 = project
9217 .update(cx, |project, cx| {
9218 project.open_buffer((worktree_id, "lib.rs"), cx)
9219 })
9220 .await
9221 .unwrap();
9222
9223 let multi_buffer = cx.new(|cx| {
9224 let mut multi_buffer = MultiBuffer::new(ReadWrite);
9225 multi_buffer.push_excerpts(
9226 buffer_1.clone(),
9227 [
9228 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9229 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9230 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9231 ],
9232 cx,
9233 );
9234 multi_buffer.push_excerpts(
9235 buffer_2.clone(),
9236 [
9237 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9238 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9239 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9240 ],
9241 cx,
9242 );
9243 multi_buffer.push_excerpts(
9244 buffer_3.clone(),
9245 [
9246 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9247 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9248 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9249 ],
9250 cx,
9251 );
9252 multi_buffer
9253 });
9254 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
9255 Editor::new(
9256 EditorMode::full(),
9257 multi_buffer,
9258 Some(project.clone()),
9259 window,
9260 cx,
9261 )
9262 });
9263
9264 multi_buffer_editor.update_in(cx, |editor, window, cx| {
9265 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
9266 s.select_ranges(Some(1..2))
9267 });
9268 editor.insert("|one|two|three|", window, cx);
9269 });
9270 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
9271 multi_buffer_editor.update_in(cx, |editor, window, cx| {
9272 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
9273 s.select_ranges(Some(60..70))
9274 });
9275 editor.insert("|four|five|six|", window, cx);
9276 });
9277 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
9278
9279 // First two buffers should be edited, but not the third one.
9280 assert_eq!(
9281 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
9282 "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}",
9283 );
9284 buffer_1.update(cx, |buffer, _| {
9285 assert!(buffer.is_dirty());
9286 assert_eq!(
9287 buffer.text(),
9288 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
9289 )
9290 });
9291 buffer_2.update(cx, |buffer, _| {
9292 assert!(buffer.is_dirty());
9293 assert_eq!(
9294 buffer.text(),
9295 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
9296 )
9297 });
9298 buffer_3.update(cx, |buffer, _| {
9299 assert!(!buffer.is_dirty());
9300 assert_eq!(buffer.text(), sample_text_3,)
9301 });
9302 cx.executor().run_until_parked();
9303
9304 cx.executor().start_waiting();
9305 let save = multi_buffer_editor
9306 .update_in(cx, |editor, window, cx| {
9307 editor.save(
9308 SaveOptions {
9309 format: true,
9310 autosave: false,
9311 },
9312 project.clone(),
9313 window,
9314 cx,
9315 )
9316 })
9317 .unwrap();
9318
9319 let fake_server = fake_servers.next().await.unwrap();
9320 fake_server
9321 .server
9322 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
9323 Ok(Some(vec![lsp::TextEdit::new(
9324 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9325 format!("[{} formatted]", params.text_document.uri),
9326 )]))
9327 })
9328 .detach();
9329 save.await;
9330
9331 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
9332 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
9333 assert_eq!(
9334 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
9335 uri!(
9336 "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}"
9337 ),
9338 );
9339 buffer_1.update(cx, |buffer, _| {
9340 assert!(!buffer.is_dirty());
9341 assert_eq!(
9342 buffer.text(),
9343 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
9344 )
9345 });
9346 buffer_2.update(cx, |buffer, _| {
9347 assert!(!buffer.is_dirty());
9348 assert_eq!(
9349 buffer.text(),
9350 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
9351 )
9352 });
9353 buffer_3.update(cx, |buffer, _| {
9354 assert!(!buffer.is_dirty());
9355 assert_eq!(buffer.text(), sample_text_3,)
9356 });
9357}
9358
9359#[gpui::test]
9360async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
9361 init_test(cx, |_| {});
9362
9363 let fs = FakeFs::new(cx.executor());
9364 fs.insert_tree(
9365 path!("/dir"),
9366 json!({
9367 "file1.rs": "fn main() { println!(\"hello\"); }",
9368 "file2.rs": "fn test() { println!(\"test\"); }",
9369 "file3.rs": "fn other() { println!(\"other\"); }\n",
9370 }),
9371 )
9372 .await;
9373
9374 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
9375 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9376 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
9377
9378 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9379 language_registry.add(rust_lang());
9380
9381 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
9382 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
9383
9384 // Open three buffers
9385 let buffer_1 = project
9386 .update(cx, |project, cx| {
9387 project.open_buffer((worktree_id, "file1.rs"), cx)
9388 })
9389 .await
9390 .unwrap();
9391 let buffer_2 = project
9392 .update(cx, |project, cx| {
9393 project.open_buffer((worktree_id, "file2.rs"), cx)
9394 })
9395 .await
9396 .unwrap();
9397 let buffer_3 = project
9398 .update(cx, |project, cx| {
9399 project.open_buffer((worktree_id, "file3.rs"), cx)
9400 })
9401 .await
9402 .unwrap();
9403
9404 // Create a multi-buffer with all three buffers
9405 let multi_buffer = cx.new(|cx| {
9406 let mut multi_buffer = MultiBuffer::new(ReadWrite);
9407 multi_buffer.push_excerpts(
9408 buffer_1.clone(),
9409 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9410 cx,
9411 );
9412 multi_buffer.push_excerpts(
9413 buffer_2.clone(),
9414 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9415 cx,
9416 );
9417 multi_buffer.push_excerpts(
9418 buffer_3.clone(),
9419 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9420 cx,
9421 );
9422 multi_buffer
9423 });
9424
9425 let editor = cx.new_window_entity(|window, cx| {
9426 Editor::new(
9427 EditorMode::full(),
9428 multi_buffer,
9429 Some(project.clone()),
9430 window,
9431 cx,
9432 )
9433 });
9434
9435 // Edit only the first buffer
9436 editor.update_in(cx, |editor, window, cx| {
9437 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
9438 s.select_ranges(Some(10..10))
9439 });
9440 editor.insert("// edited", window, cx);
9441 });
9442
9443 // Verify that only buffer 1 is dirty
9444 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
9445 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9446 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9447
9448 // Get write counts after file creation (files were created with initial content)
9449 // We expect each file to have been written once during creation
9450 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
9451 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
9452 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
9453
9454 // Perform autosave
9455 let save_task = editor.update_in(cx, |editor, window, cx| {
9456 editor.save(
9457 SaveOptions {
9458 format: true,
9459 autosave: true,
9460 },
9461 project.clone(),
9462 window,
9463 cx,
9464 )
9465 });
9466 save_task.await.unwrap();
9467
9468 // Only the dirty buffer should have been saved
9469 assert_eq!(
9470 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
9471 1,
9472 "Buffer 1 was dirty, so it should have been written once during autosave"
9473 );
9474 assert_eq!(
9475 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
9476 0,
9477 "Buffer 2 was clean, so it should not have been written during autosave"
9478 );
9479 assert_eq!(
9480 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
9481 0,
9482 "Buffer 3 was clean, so it should not have been written during autosave"
9483 );
9484
9485 // Verify buffer states after autosave
9486 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9487 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9488 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9489
9490 // Now perform a manual save (format = true)
9491 let save_task = editor.update_in(cx, |editor, window, cx| {
9492 editor.save(
9493 SaveOptions {
9494 format: true,
9495 autosave: false,
9496 },
9497 project.clone(),
9498 window,
9499 cx,
9500 )
9501 });
9502 save_task.await.unwrap();
9503
9504 // During manual save, clean buffers don't get written to disk
9505 // They just get did_save called for language server notifications
9506 assert_eq!(
9507 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
9508 1,
9509 "Buffer 1 should only have been written once total (during autosave, not manual save)"
9510 );
9511 assert_eq!(
9512 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
9513 0,
9514 "Buffer 2 should not have been written at all"
9515 );
9516 assert_eq!(
9517 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
9518 0,
9519 "Buffer 3 should not have been written at all"
9520 );
9521}
9522
9523#[gpui::test]
9524async fn test_range_format_during_save(cx: &mut TestAppContext) {
9525 init_test(cx, |_| {});
9526
9527 let fs = FakeFs::new(cx.executor());
9528 fs.insert_file(path!("/file.rs"), Default::default()).await;
9529
9530 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9531
9532 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9533 language_registry.add(rust_lang());
9534 let mut fake_servers = language_registry.register_fake_lsp(
9535 "Rust",
9536 FakeLspAdapter {
9537 capabilities: lsp::ServerCapabilities {
9538 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
9539 ..Default::default()
9540 },
9541 ..Default::default()
9542 },
9543 );
9544
9545 let buffer = project
9546 .update(cx, |project, cx| {
9547 project.open_local_buffer(path!("/file.rs"), cx)
9548 })
9549 .await
9550 .unwrap();
9551
9552 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9553 let (editor, cx) = cx.add_window_view(|window, cx| {
9554 build_editor_with_project(project.clone(), buffer, window, cx)
9555 });
9556 editor.update_in(cx, |editor, window, cx| {
9557 editor.set_text("one\ntwo\nthree\n", window, cx)
9558 });
9559 assert!(cx.read(|cx| editor.is_dirty(cx)));
9560
9561 cx.executor().start_waiting();
9562 let fake_server = fake_servers.next().await.unwrap();
9563
9564 let save = editor
9565 .update_in(cx, |editor, window, cx| {
9566 editor.save(
9567 SaveOptions {
9568 format: true,
9569 autosave: false,
9570 },
9571 project.clone(),
9572 window,
9573 cx,
9574 )
9575 })
9576 .unwrap();
9577 fake_server
9578 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
9579 assert_eq!(
9580 params.text_document.uri,
9581 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9582 );
9583 assert_eq!(params.options.tab_size, 4);
9584 Ok(Some(vec![lsp::TextEdit::new(
9585 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9586 ", ".to_string(),
9587 )]))
9588 })
9589 .next()
9590 .await;
9591 cx.executor().start_waiting();
9592 save.await;
9593 assert_eq!(
9594 editor.update(cx, |editor, cx| editor.text(cx)),
9595 "one, two\nthree\n"
9596 );
9597 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9598
9599 editor.update_in(cx, |editor, window, cx| {
9600 editor.set_text("one\ntwo\nthree\n", window, cx)
9601 });
9602 assert!(cx.read(|cx| editor.is_dirty(cx)));
9603
9604 // Ensure we can still save even if formatting hangs.
9605 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
9606 move |params, _| async move {
9607 assert_eq!(
9608 params.text_document.uri,
9609 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9610 );
9611 futures::future::pending::<()>().await;
9612 unreachable!()
9613 },
9614 );
9615 let save = editor
9616 .update_in(cx, |editor, window, cx| {
9617 editor.save(
9618 SaveOptions {
9619 format: true,
9620 autosave: false,
9621 },
9622 project.clone(),
9623 window,
9624 cx,
9625 )
9626 })
9627 .unwrap();
9628 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9629 cx.executor().start_waiting();
9630 save.await;
9631 assert_eq!(
9632 editor.update(cx, |editor, cx| editor.text(cx)),
9633 "one\ntwo\nthree\n"
9634 );
9635 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9636
9637 // For non-dirty buffer, no formatting request should be sent
9638 let save = editor
9639 .update_in(cx, |editor, window, cx| {
9640 editor.save(
9641 SaveOptions {
9642 format: false,
9643 autosave: false,
9644 },
9645 project.clone(),
9646 window,
9647 cx,
9648 )
9649 })
9650 .unwrap();
9651 let _pending_format_request = fake_server
9652 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
9653 panic!("Should not be invoked");
9654 })
9655 .next();
9656 cx.executor().start_waiting();
9657 save.await;
9658
9659 // Set Rust language override and assert overridden tabsize is sent to language server
9660 update_test_language_settings(cx, |settings| {
9661 settings.languages.insert(
9662 "Rust".into(),
9663 LanguageSettingsContent {
9664 tab_size: NonZeroU32::new(8),
9665 ..Default::default()
9666 },
9667 );
9668 });
9669
9670 editor.update_in(cx, |editor, window, cx| {
9671 editor.set_text("somehting_new\n", window, cx)
9672 });
9673 assert!(cx.read(|cx| editor.is_dirty(cx)));
9674 let save = editor
9675 .update_in(cx, |editor, window, cx| {
9676 editor.save(
9677 SaveOptions {
9678 format: true,
9679 autosave: false,
9680 },
9681 project.clone(),
9682 window,
9683 cx,
9684 )
9685 })
9686 .unwrap();
9687 fake_server
9688 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
9689 assert_eq!(
9690 params.text_document.uri,
9691 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9692 );
9693 assert_eq!(params.options.tab_size, 8);
9694 Ok(Some(Vec::new()))
9695 })
9696 .next()
9697 .await;
9698 save.await;
9699}
9700
9701#[gpui::test]
9702async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
9703 init_test(cx, |settings| {
9704 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
9705 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
9706 ))
9707 });
9708
9709 let fs = FakeFs::new(cx.executor());
9710 fs.insert_file(path!("/file.rs"), Default::default()).await;
9711
9712 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9713
9714 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9715 language_registry.add(Arc::new(Language::new(
9716 LanguageConfig {
9717 name: "Rust".into(),
9718 matcher: LanguageMatcher {
9719 path_suffixes: vec!["rs".to_string()],
9720 ..Default::default()
9721 },
9722 ..LanguageConfig::default()
9723 },
9724 Some(tree_sitter_rust::LANGUAGE.into()),
9725 )));
9726 update_test_language_settings(cx, |settings| {
9727 // Enable Prettier formatting for the same buffer, and ensure
9728 // LSP is called instead of Prettier.
9729 settings.defaults.prettier = Some(PrettierSettings {
9730 allowed: true,
9731 ..PrettierSettings::default()
9732 });
9733 });
9734 let mut fake_servers = language_registry.register_fake_lsp(
9735 "Rust",
9736 FakeLspAdapter {
9737 capabilities: lsp::ServerCapabilities {
9738 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9739 ..Default::default()
9740 },
9741 ..Default::default()
9742 },
9743 );
9744
9745 let buffer = project
9746 .update(cx, |project, cx| {
9747 project.open_local_buffer(path!("/file.rs"), cx)
9748 })
9749 .await
9750 .unwrap();
9751
9752 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9753 let (editor, cx) = cx.add_window_view(|window, cx| {
9754 build_editor_with_project(project.clone(), buffer, window, cx)
9755 });
9756 editor.update_in(cx, |editor, window, cx| {
9757 editor.set_text("one\ntwo\nthree\n", window, cx)
9758 });
9759
9760 cx.executor().start_waiting();
9761 let fake_server = fake_servers.next().await.unwrap();
9762
9763 let format = editor
9764 .update_in(cx, |editor, window, cx| {
9765 editor.perform_format(
9766 project.clone(),
9767 FormatTrigger::Manual,
9768 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
9769 window,
9770 cx,
9771 )
9772 })
9773 .unwrap();
9774 fake_server
9775 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
9776 assert_eq!(
9777 params.text_document.uri,
9778 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9779 );
9780 assert_eq!(params.options.tab_size, 4);
9781 Ok(Some(vec![lsp::TextEdit::new(
9782 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9783 ", ".to_string(),
9784 )]))
9785 })
9786 .next()
9787 .await;
9788 cx.executor().start_waiting();
9789 format.await;
9790 assert_eq!(
9791 editor.update(cx, |editor, cx| editor.text(cx)),
9792 "one, two\nthree\n"
9793 );
9794
9795 editor.update_in(cx, |editor, window, cx| {
9796 editor.set_text("one\ntwo\nthree\n", window, cx)
9797 });
9798 // Ensure we don't lock if formatting hangs.
9799 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9800 move |params, _| async move {
9801 assert_eq!(
9802 params.text_document.uri,
9803 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9804 );
9805 futures::future::pending::<()>().await;
9806 unreachable!()
9807 },
9808 );
9809 let format = editor
9810 .update_in(cx, |editor, window, cx| {
9811 editor.perform_format(
9812 project,
9813 FormatTrigger::Manual,
9814 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
9815 window,
9816 cx,
9817 )
9818 })
9819 .unwrap();
9820 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9821 cx.executor().start_waiting();
9822 format.await;
9823 assert_eq!(
9824 editor.update(cx, |editor, cx| editor.text(cx)),
9825 "one\ntwo\nthree\n"
9826 );
9827}
9828
9829#[gpui::test]
9830async fn test_multiple_formatters(cx: &mut TestAppContext) {
9831 init_test(cx, |settings| {
9832 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
9833 settings.defaults.formatter =
9834 Some(language_settings::SelectedFormatter::List(FormatterList(
9835 vec![
9836 Formatter::LanguageServer { name: None },
9837 Formatter::CodeActions(
9838 [
9839 ("code-action-1".into(), true),
9840 ("code-action-2".into(), true),
9841 ]
9842 .into_iter()
9843 .collect(),
9844 ),
9845 ]
9846 .into(),
9847 )))
9848 });
9849
9850 let fs = FakeFs::new(cx.executor());
9851 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
9852 .await;
9853
9854 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9855 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9856 language_registry.add(rust_lang());
9857
9858 let mut fake_servers = language_registry.register_fake_lsp(
9859 "Rust",
9860 FakeLspAdapter {
9861 capabilities: lsp::ServerCapabilities {
9862 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9863 execute_command_provider: Some(lsp::ExecuteCommandOptions {
9864 commands: vec!["the-command-for-code-action-1".into()],
9865 ..Default::default()
9866 }),
9867 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
9868 ..Default::default()
9869 },
9870 ..Default::default()
9871 },
9872 );
9873
9874 let buffer = project
9875 .update(cx, |project, cx| {
9876 project.open_local_buffer(path!("/file.rs"), cx)
9877 })
9878 .await
9879 .unwrap();
9880
9881 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9882 let (editor, cx) = cx.add_window_view(|window, cx| {
9883 build_editor_with_project(project.clone(), buffer, window, cx)
9884 });
9885
9886 cx.executor().start_waiting();
9887
9888 let fake_server = fake_servers.next().await.unwrap();
9889 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9890 move |_params, _| async move {
9891 Ok(Some(vec![lsp::TextEdit::new(
9892 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9893 "applied-formatting\n".to_string(),
9894 )]))
9895 },
9896 );
9897 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
9898 move |params, _| async move {
9899 assert_eq!(
9900 params.context.only,
9901 Some(vec!["code-action-1".into(), "code-action-2".into()])
9902 );
9903 let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap();
9904 Ok(Some(vec![
9905 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
9906 kind: Some("code-action-1".into()),
9907 edit: Some(lsp::WorkspaceEdit::new(
9908 [(
9909 uri.clone(),
9910 vec![lsp::TextEdit::new(
9911 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9912 "applied-code-action-1-edit\n".to_string(),
9913 )],
9914 )]
9915 .into_iter()
9916 .collect(),
9917 )),
9918 command: Some(lsp::Command {
9919 command: "the-command-for-code-action-1".into(),
9920 ..Default::default()
9921 }),
9922 ..Default::default()
9923 }),
9924 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
9925 kind: Some("code-action-2".into()),
9926 edit: Some(lsp::WorkspaceEdit::new(
9927 [(
9928 uri.clone(),
9929 vec![lsp::TextEdit::new(
9930 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9931 "applied-code-action-2-edit\n".to_string(),
9932 )],
9933 )]
9934 .into_iter()
9935 .collect(),
9936 )),
9937 ..Default::default()
9938 }),
9939 ]))
9940 },
9941 );
9942
9943 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
9944 move |params, _| async move { Ok(params) }
9945 });
9946
9947 let command_lock = Arc::new(futures::lock::Mutex::new(()));
9948 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
9949 let fake = fake_server.clone();
9950 let lock = command_lock.clone();
9951 move |params, _| {
9952 assert_eq!(params.command, "the-command-for-code-action-1");
9953 let fake = fake.clone();
9954 let lock = lock.clone();
9955 async move {
9956 lock.lock().await;
9957 fake.server
9958 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
9959 label: None,
9960 edit: lsp::WorkspaceEdit {
9961 changes: Some(
9962 [(
9963 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
9964 vec![lsp::TextEdit {
9965 range: lsp::Range::new(
9966 lsp::Position::new(0, 0),
9967 lsp::Position::new(0, 0),
9968 ),
9969 new_text: "applied-code-action-1-command\n".into(),
9970 }],
9971 )]
9972 .into_iter()
9973 .collect(),
9974 ),
9975 ..Default::default()
9976 },
9977 })
9978 .await
9979 .into_response()
9980 .unwrap();
9981 Ok(Some(json!(null)))
9982 }
9983 }
9984 });
9985
9986 cx.executor().start_waiting();
9987 editor
9988 .update_in(cx, |editor, window, cx| {
9989 editor.perform_format(
9990 project.clone(),
9991 FormatTrigger::Manual,
9992 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
9993 window,
9994 cx,
9995 )
9996 })
9997 .unwrap()
9998 .await;
9999 editor.update(cx, |editor, cx| {
10000 assert_eq!(
10001 editor.text(cx),
10002 r#"
10003 applied-code-action-2-edit
10004 applied-code-action-1-command
10005 applied-code-action-1-edit
10006 applied-formatting
10007 one
10008 two
10009 three
10010 "#
10011 .unindent()
10012 );
10013 });
10014
10015 editor.update_in(cx, |editor, window, cx| {
10016 editor.undo(&Default::default(), window, cx);
10017 assert_eq!(editor.text(cx), "one \ntwo \nthree");
10018 });
10019
10020 // Perform a manual edit while waiting for an LSP command
10021 // that's being run as part of a formatting code action.
10022 let lock_guard = command_lock.lock().await;
10023 let format = editor
10024 .update_in(cx, |editor, window, cx| {
10025 editor.perform_format(
10026 project.clone(),
10027 FormatTrigger::Manual,
10028 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10029 window,
10030 cx,
10031 )
10032 })
10033 .unwrap();
10034 cx.run_until_parked();
10035 editor.update(cx, |editor, cx| {
10036 assert_eq!(
10037 editor.text(cx),
10038 r#"
10039 applied-code-action-1-edit
10040 applied-formatting
10041 one
10042 two
10043 three
10044 "#
10045 .unindent()
10046 );
10047
10048 editor.buffer.update(cx, |buffer, cx| {
10049 let ix = buffer.len(cx);
10050 buffer.edit([(ix..ix, "edited\n")], None, cx);
10051 });
10052 });
10053
10054 // Allow the LSP command to proceed. Because the buffer was edited,
10055 // the second code action will not be run.
10056 drop(lock_guard);
10057 format.await;
10058 editor.update_in(cx, |editor, window, cx| {
10059 assert_eq!(
10060 editor.text(cx),
10061 r#"
10062 applied-code-action-1-command
10063 applied-code-action-1-edit
10064 applied-formatting
10065 one
10066 two
10067 three
10068 edited
10069 "#
10070 .unindent()
10071 );
10072
10073 // The manual edit is undone first, because it is the last thing the user did
10074 // (even though the command completed afterwards).
10075 editor.undo(&Default::default(), window, cx);
10076 assert_eq!(
10077 editor.text(cx),
10078 r#"
10079 applied-code-action-1-command
10080 applied-code-action-1-edit
10081 applied-formatting
10082 one
10083 two
10084 three
10085 "#
10086 .unindent()
10087 );
10088
10089 // All the formatting (including the command, which completed after the manual edit)
10090 // is undone together.
10091 editor.undo(&Default::default(), window, cx);
10092 assert_eq!(editor.text(cx), "one \ntwo \nthree");
10093 });
10094}
10095
10096#[gpui::test]
10097async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
10098 init_test(cx, |settings| {
10099 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
10100 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
10101 ))
10102 });
10103
10104 let fs = FakeFs::new(cx.executor());
10105 fs.insert_file(path!("/file.ts"), Default::default()).await;
10106
10107 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10108
10109 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10110 language_registry.add(Arc::new(Language::new(
10111 LanguageConfig {
10112 name: "TypeScript".into(),
10113 matcher: LanguageMatcher {
10114 path_suffixes: vec!["ts".to_string()],
10115 ..Default::default()
10116 },
10117 ..LanguageConfig::default()
10118 },
10119 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
10120 )));
10121 update_test_language_settings(cx, |settings| {
10122 settings.defaults.prettier = Some(PrettierSettings {
10123 allowed: true,
10124 ..PrettierSettings::default()
10125 });
10126 });
10127 let mut fake_servers = language_registry.register_fake_lsp(
10128 "TypeScript",
10129 FakeLspAdapter {
10130 capabilities: lsp::ServerCapabilities {
10131 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
10132 ..Default::default()
10133 },
10134 ..Default::default()
10135 },
10136 );
10137
10138 let buffer = project
10139 .update(cx, |project, cx| {
10140 project.open_local_buffer(path!("/file.ts"), cx)
10141 })
10142 .await
10143 .unwrap();
10144
10145 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10146 let (editor, cx) = cx.add_window_view(|window, cx| {
10147 build_editor_with_project(project.clone(), buffer, window, cx)
10148 });
10149 editor.update_in(cx, |editor, window, cx| {
10150 editor.set_text(
10151 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
10152 window,
10153 cx,
10154 )
10155 });
10156
10157 cx.executor().start_waiting();
10158 let fake_server = fake_servers.next().await.unwrap();
10159
10160 let format = editor
10161 .update_in(cx, |editor, window, cx| {
10162 editor.perform_code_action_kind(
10163 project.clone(),
10164 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
10165 window,
10166 cx,
10167 )
10168 })
10169 .unwrap();
10170 fake_server
10171 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
10172 assert_eq!(
10173 params.text_document.uri,
10174 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
10175 );
10176 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
10177 lsp::CodeAction {
10178 title: "Organize Imports".to_string(),
10179 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
10180 edit: Some(lsp::WorkspaceEdit {
10181 changes: Some(
10182 [(
10183 params.text_document.uri.clone(),
10184 vec![lsp::TextEdit::new(
10185 lsp::Range::new(
10186 lsp::Position::new(1, 0),
10187 lsp::Position::new(2, 0),
10188 ),
10189 "".to_string(),
10190 )],
10191 )]
10192 .into_iter()
10193 .collect(),
10194 ),
10195 ..Default::default()
10196 }),
10197 ..Default::default()
10198 },
10199 )]))
10200 })
10201 .next()
10202 .await;
10203 cx.executor().start_waiting();
10204 format.await;
10205 assert_eq!(
10206 editor.update(cx, |editor, cx| editor.text(cx)),
10207 "import { a } from 'module';\n\nconst x = a;\n"
10208 );
10209
10210 editor.update_in(cx, |editor, window, cx| {
10211 editor.set_text(
10212 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
10213 window,
10214 cx,
10215 )
10216 });
10217 // Ensure we don't lock if code action hangs.
10218 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
10219 move |params, _| async move {
10220 assert_eq!(
10221 params.text_document.uri,
10222 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
10223 );
10224 futures::future::pending::<()>().await;
10225 unreachable!()
10226 },
10227 );
10228 let format = editor
10229 .update_in(cx, |editor, window, cx| {
10230 editor.perform_code_action_kind(
10231 project,
10232 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
10233 window,
10234 cx,
10235 )
10236 })
10237 .unwrap();
10238 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
10239 cx.executor().start_waiting();
10240 format.await;
10241 assert_eq!(
10242 editor.update(cx, |editor, cx| editor.text(cx)),
10243 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
10244 );
10245}
10246
10247#[gpui::test]
10248async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
10249 init_test(cx, |_| {});
10250
10251 let mut cx = EditorLspTestContext::new_rust(
10252 lsp::ServerCapabilities {
10253 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10254 ..Default::default()
10255 },
10256 cx,
10257 )
10258 .await;
10259
10260 cx.set_state(indoc! {"
10261 one.twoˇ
10262 "});
10263
10264 // The format request takes a long time. When it completes, it inserts
10265 // a newline and an indent before the `.`
10266 cx.lsp
10267 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
10268 let executor = cx.background_executor().clone();
10269 async move {
10270 executor.timer(Duration::from_millis(100)).await;
10271 Ok(Some(vec![lsp::TextEdit {
10272 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
10273 new_text: "\n ".into(),
10274 }]))
10275 }
10276 });
10277
10278 // Submit a format request.
10279 let format_1 = cx
10280 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10281 .unwrap();
10282 cx.executor().run_until_parked();
10283
10284 // Submit a second format request.
10285 let format_2 = cx
10286 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10287 .unwrap();
10288 cx.executor().run_until_parked();
10289
10290 // Wait for both format requests to complete
10291 cx.executor().advance_clock(Duration::from_millis(200));
10292 cx.executor().start_waiting();
10293 format_1.await.unwrap();
10294 cx.executor().start_waiting();
10295 format_2.await.unwrap();
10296
10297 // The formatting edits only happens once.
10298 cx.assert_editor_state(indoc! {"
10299 one
10300 .twoˇ
10301 "});
10302}
10303
10304#[gpui::test]
10305async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
10306 init_test(cx, |settings| {
10307 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
10308 });
10309
10310 let mut cx = EditorLspTestContext::new_rust(
10311 lsp::ServerCapabilities {
10312 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10313 ..Default::default()
10314 },
10315 cx,
10316 )
10317 .await;
10318
10319 // Set up a buffer white some trailing whitespace and no trailing newline.
10320 cx.set_state(
10321 &[
10322 "one ", //
10323 "twoˇ", //
10324 "three ", //
10325 "four", //
10326 ]
10327 .join("\n"),
10328 );
10329
10330 // Submit a format request.
10331 let format = cx
10332 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10333 .unwrap();
10334
10335 // Record which buffer changes have been sent to the language server
10336 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
10337 cx.lsp
10338 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
10339 let buffer_changes = buffer_changes.clone();
10340 move |params, _| {
10341 buffer_changes.lock().extend(
10342 params
10343 .content_changes
10344 .into_iter()
10345 .map(|e| (e.range.unwrap(), e.text)),
10346 );
10347 }
10348 });
10349
10350 // Handle formatting requests to the language server.
10351 cx.lsp
10352 .set_request_handler::<lsp::request::Formatting, _, _>({
10353 let buffer_changes = buffer_changes.clone();
10354 move |_, _| {
10355 // When formatting is requested, trailing whitespace has already been stripped,
10356 // and the trailing newline has already been added.
10357 assert_eq!(
10358 &buffer_changes.lock()[1..],
10359 &[
10360 (
10361 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
10362 "".into()
10363 ),
10364 (
10365 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
10366 "".into()
10367 ),
10368 (
10369 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
10370 "\n".into()
10371 ),
10372 ]
10373 );
10374
10375 // Insert blank lines between each line of the buffer.
10376 async move {
10377 Ok(Some(vec![
10378 lsp::TextEdit {
10379 range: lsp::Range::new(
10380 lsp::Position::new(1, 0),
10381 lsp::Position::new(1, 0),
10382 ),
10383 new_text: "\n".into(),
10384 },
10385 lsp::TextEdit {
10386 range: lsp::Range::new(
10387 lsp::Position::new(2, 0),
10388 lsp::Position::new(2, 0),
10389 ),
10390 new_text: "\n".into(),
10391 },
10392 ]))
10393 }
10394 }
10395 });
10396
10397 // After formatting the buffer, the trailing whitespace is stripped,
10398 // a newline is appended, and the edits provided by the language server
10399 // have been applied.
10400 format.await.unwrap();
10401 cx.assert_editor_state(
10402 &[
10403 "one", //
10404 "", //
10405 "twoˇ", //
10406 "", //
10407 "three", //
10408 "four", //
10409 "", //
10410 ]
10411 .join("\n"),
10412 );
10413
10414 // Undoing the formatting undoes the trailing whitespace removal, the
10415 // trailing newline, and the LSP edits.
10416 cx.update_buffer(|buffer, cx| buffer.undo(cx));
10417 cx.assert_editor_state(
10418 &[
10419 "one ", //
10420 "twoˇ", //
10421 "three ", //
10422 "four", //
10423 ]
10424 .join("\n"),
10425 );
10426}
10427
10428#[gpui::test]
10429async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
10430 cx: &mut TestAppContext,
10431) {
10432 init_test(cx, |_| {});
10433
10434 cx.update(|cx| {
10435 cx.update_global::<SettingsStore, _>(|settings, cx| {
10436 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10437 settings.auto_signature_help = Some(true);
10438 });
10439 });
10440 });
10441
10442 let mut cx = EditorLspTestContext::new_rust(
10443 lsp::ServerCapabilities {
10444 signature_help_provider: Some(lsp::SignatureHelpOptions {
10445 ..Default::default()
10446 }),
10447 ..Default::default()
10448 },
10449 cx,
10450 )
10451 .await;
10452
10453 let language = Language::new(
10454 LanguageConfig {
10455 name: "Rust".into(),
10456 brackets: BracketPairConfig {
10457 pairs: vec![
10458 BracketPair {
10459 start: "{".to_string(),
10460 end: "}".to_string(),
10461 close: true,
10462 surround: true,
10463 newline: true,
10464 },
10465 BracketPair {
10466 start: "(".to_string(),
10467 end: ")".to_string(),
10468 close: true,
10469 surround: true,
10470 newline: true,
10471 },
10472 BracketPair {
10473 start: "/*".to_string(),
10474 end: " */".to_string(),
10475 close: true,
10476 surround: true,
10477 newline: true,
10478 },
10479 BracketPair {
10480 start: "[".to_string(),
10481 end: "]".to_string(),
10482 close: false,
10483 surround: false,
10484 newline: true,
10485 },
10486 BracketPair {
10487 start: "\"".to_string(),
10488 end: "\"".to_string(),
10489 close: true,
10490 surround: true,
10491 newline: false,
10492 },
10493 BracketPair {
10494 start: "<".to_string(),
10495 end: ">".to_string(),
10496 close: false,
10497 surround: true,
10498 newline: true,
10499 },
10500 ],
10501 ..Default::default()
10502 },
10503 autoclose_before: "})]".to_string(),
10504 ..Default::default()
10505 },
10506 Some(tree_sitter_rust::LANGUAGE.into()),
10507 );
10508 let language = Arc::new(language);
10509
10510 cx.language_registry().add(language.clone());
10511 cx.update_buffer(|buffer, cx| {
10512 buffer.set_language(Some(language), cx);
10513 });
10514
10515 cx.set_state(
10516 &r#"
10517 fn main() {
10518 sampleˇ
10519 }
10520 "#
10521 .unindent(),
10522 );
10523
10524 cx.update_editor(|editor, window, cx| {
10525 editor.handle_input("(", window, cx);
10526 });
10527 cx.assert_editor_state(
10528 &"
10529 fn main() {
10530 sample(ˇ)
10531 }
10532 "
10533 .unindent(),
10534 );
10535
10536 let mocked_response = lsp::SignatureHelp {
10537 signatures: vec![lsp::SignatureInformation {
10538 label: "fn sample(param1: u8, param2: u8)".to_string(),
10539 documentation: None,
10540 parameters: Some(vec![
10541 lsp::ParameterInformation {
10542 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10543 documentation: None,
10544 },
10545 lsp::ParameterInformation {
10546 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10547 documentation: None,
10548 },
10549 ]),
10550 active_parameter: None,
10551 }],
10552 active_signature: Some(0),
10553 active_parameter: Some(0),
10554 };
10555 handle_signature_help_request(&mut cx, mocked_response).await;
10556
10557 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10558 .await;
10559
10560 cx.editor(|editor, _, _| {
10561 let signature_help_state = editor.signature_help_state.popover().cloned();
10562 assert_eq!(
10563 signature_help_state.unwrap().label,
10564 "param1: u8, param2: u8"
10565 );
10566 });
10567}
10568
10569#[gpui::test]
10570async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
10571 init_test(cx, |_| {});
10572
10573 cx.update(|cx| {
10574 cx.update_global::<SettingsStore, _>(|settings, cx| {
10575 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10576 settings.auto_signature_help = Some(false);
10577 settings.show_signature_help_after_edits = Some(false);
10578 });
10579 });
10580 });
10581
10582 let mut cx = EditorLspTestContext::new_rust(
10583 lsp::ServerCapabilities {
10584 signature_help_provider: Some(lsp::SignatureHelpOptions {
10585 ..Default::default()
10586 }),
10587 ..Default::default()
10588 },
10589 cx,
10590 )
10591 .await;
10592
10593 let language = Language::new(
10594 LanguageConfig {
10595 name: "Rust".into(),
10596 brackets: BracketPairConfig {
10597 pairs: vec![
10598 BracketPair {
10599 start: "{".to_string(),
10600 end: "}".to_string(),
10601 close: true,
10602 surround: true,
10603 newline: true,
10604 },
10605 BracketPair {
10606 start: "(".to_string(),
10607 end: ")".to_string(),
10608 close: true,
10609 surround: true,
10610 newline: true,
10611 },
10612 BracketPair {
10613 start: "/*".to_string(),
10614 end: " */".to_string(),
10615 close: true,
10616 surround: true,
10617 newline: true,
10618 },
10619 BracketPair {
10620 start: "[".to_string(),
10621 end: "]".to_string(),
10622 close: false,
10623 surround: false,
10624 newline: true,
10625 },
10626 BracketPair {
10627 start: "\"".to_string(),
10628 end: "\"".to_string(),
10629 close: true,
10630 surround: true,
10631 newline: false,
10632 },
10633 BracketPair {
10634 start: "<".to_string(),
10635 end: ">".to_string(),
10636 close: false,
10637 surround: true,
10638 newline: true,
10639 },
10640 ],
10641 ..Default::default()
10642 },
10643 autoclose_before: "})]".to_string(),
10644 ..Default::default()
10645 },
10646 Some(tree_sitter_rust::LANGUAGE.into()),
10647 );
10648 let language = Arc::new(language);
10649
10650 cx.language_registry().add(language.clone());
10651 cx.update_buffer(|buffer, cx| {
10652 buffer.set_language(Some(language), cx);
10653 });
10654
10655 // Ensure that signature_help is not called when no signature help is enabled.
10656 cx.set_state(
10657 &r#"
10658 fn main() {
10659 sampleˇ
10660 }
10661 "#
10662 .unindent(),
10663 );
10664 cx.update_editor(|editor, window, cx| {
10665 editor.handle_input("(", window, cx);
10666 });
10667 cx.assert_editor_state(
10668 &"
10669 fn main() {
10670 sample(ˇ)
10671 }
10672 "
10673 .unindent(),
10674 );
10675 cx.editor(|editor, _, _| {
10676 assert!(editor.signature_help_state.task().is_none());
10677 });
10678
10679 let mocked_response = lsp::SignatureHelp {
10680 signatures: vec![lsp::SignatureInformation {
10681 label: "fn sample(param1: u8, param2: u8)".to_string(),
10682 documentation: None,
10683 parameters: Some(vec![
10684 lsp::ParameterInformation {
10685 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10686 documentation: None,
10687 },
10688 lsp::ParameterInformation {
10689 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10690 documentation: None,
10691 },
10692 ]),
10693 active_parameter: None,
10694 }],
10695 active_signature: Some(0),
10696 active_parameter: Some(0),
10697 };
10698
10699 // Ensure that signature_help is called when enabled afte edits
10700 cx.update(|_, cx| {
10701 cx.update_global::<SettingsStore, _>(|settings, cx| {
10702 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10703 settings.auto_signature_help = Some(false);
10704 settings.show_signature_help_after_edits = Some(true);
10705 });
10706 });
10707 });
10708 cx.set_state(
10709 &r#"
10710 fn main() {
10711 sampleˇ
10712 }
10713 "#
10714 .unindent(),
10715 );
10716 cx.update_editor(|editor, window, cx| {
10717 editor.handle_input("(", window, cx);
10718 });
10719 cx.assert_editor_state(
10720 &"
10721 fn main() {
10722 sample(ˇ)
10723 }
10724 "
10725 .unindent(),
10726 );
10727 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10728 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10729 .await;
10730 cx.update_editor(|editor, _, _| {
10731 let signature_help_state = editor.signature_help_state.popover().cloned();
10732 assert!(signature_help_state.is_some());
10733 assert_eq!(
10734 signature_help_state.unwrap().label,
10735 "param1: u8, param2: u8"
10736 );
10737 editor.signature_help_state = SignatureHelpState::default();
10738 });
10739
10740 // Ensure that signature_help is called when auto signature help override is enabled
10741 cx.update(|_, cx| {
10742 cx.update_global::<SettingsStore, _>(|settings, cx| {
10743 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10744 settings.auto_signature_help = Some(true);
10745 settings.show_signature_help_after_edits = Some(false);
10746 });
10747 });
10748 });
10749 cx.set_state(
10750 &r#"
10751 fn main() {
10752 sampleˇ
10753 }
10754 "#
10755 .unindent(),
10756 );
10757 cx.update_editor(|editor, window, cx| {
10758 editor.handle_input("(", window, cx);
10759 });
10760 cx.assert_editor_state(
10761 &"
10762 fn main() {
10763 sample(ˇ)
10764 }
10765 "
10766 .unindent(),
10767 );
10768 handle_signature_help_request(&mut cx, mocked_response).await;
10769 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10770 .await;
10771 cx.editor(|editor, _, _| {
10772 let signature_help_state = editor.signature_help_state.popover().cloned();
10773 assert!(signature_help_state.is_some());
10774 assert_eq!(
10775 signature_help_state.unwrap().label,
10776 "param1: u8, param2: u8"
10777 );
10778 });
10779}
10780
10781#[gpui::test]
10782async fn test_signature_help(cx: &mut TestAppContext) {
10783 init_test(cx, |_| {});
10784 cx.update(|cx| {
10785 cx.update_global::<SettingsStore, _>(|settings, cx| {
10786 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10787 settings.auto_signature_help = Some(true);
10788 });
10789 });
10790 });
10791
10792 let mut cx = EditorLspTestContext::new_rust(
10793 lsp::ServerCapabilities {
10794 signature_help_provider: Some(lsp::SignatureHelpOptions {
10795 ..Default::default()
10796 }),
10797 ..Default::default()
10798 },
10799 cx,
10800 )
10801 .await;
10802
10803 // A test that directly calls `show_signature_help`
10804 cx.update_editor(|editor, window, cx| {
10805 editor.show_signature_help(&ShowSignatureHelp, window, cx);
10806 });
10807
10808 let mocked_response = lsp::SignatureHelp {
10809 signatures: vec![lsp::SignatureInformation {
10810 label: "fn sample(param1: u8, param2: u8)".to_string(),
10811 documentation: None,
10812 parameters: Some(vec![
10813 lsp::ParameterInformation {
10814 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10815 documentation: None,
10816 },
10817 lsp::ParameterInformation {
10818 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10819 documentation: None,
10820 },
10821 ]),
10822 active_parameter: None,
10823 }],
10824 active_signature: Some(0),
10825 active_parameter: Some(0),
10826 };
10827 handle_signature_help_request(&mut cx, mocked_response).await;
10828
10829 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10830 .await;
10831
10832 cx.editor(|editor, _, _| {
10833 let signature_help_state = editor.signature_help_state.popover().cloned();
10834 assert!(signature_help_state.is_some());
10835 assert_eq!(
10836 signature_help_state.unwrap().label,
10837 "param1: u8, param2: u8"
10838 );
10839 });
10840
10841 // When exiting outside from inside the brackets, `signature_help` is closed.
10842 cx.set_state(indoc! {"
10843 fn main() {
10844 sample(ˇ);
10845 }
10846
10847 fn sample(param1: u8, param2: u8) {}
10848 "});
10849
10850 cx.update_editor(|editor, window, cx| {
10851 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
10852 });
10853
10854 let mocked_response = lsp::SignatureHelp {
10855 signatures: Vec::new(),
10856 active_signature: None,
10857 active_parameter: None,
10858 };
10859 handle_signature_help_request(&mut cx, mocked_response).await;
10860
10861 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
10862 .await;
10863
10864 cx.editor(|editor, _, _| {
10865 assert!(!editor.signature_help_state.is_shown());
10866 });
10867
10868 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
10869 cx.set_state(indoc! {"
10870 fn main() {
10871 sample(ˇ);
10872 }
10873
10874 fn sample(param1: u8, param2: u8) {}
10875 "});
10876
10877 let mocked_response = lsp::SignatureHelp {
10878 signatures: vec![lsp::SignatureInformation {
10879 label: "fn sample(param1: u8, param2: u8)".to_string(),
10880 documentation: None,
10881 parameters: Some(vec![
10882 lsp::ParameterInformation {
10883 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10884 documentation: None,
10885 },
10886 lsp::ParameterInformation {
10887 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10888 documentation: None,
10889 },
10890 ]),
10891 active_parameter: None,
10892 }],
10893 active_signature: Some(0),
10894 active_parameter: Some(0),
10895 };
10896 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10897 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10898 .await;
10899 cx.editor(|editor, _, _| {
10900 assert!(editor.signature_help_state.is_shown());
10901 });
10902
10903 // Restore the popover with more parameter input
10904 cx.set_state(indoc! {"
10905 fn main() {
10906 sample(param1, param2ˇ);
10907 }
10908
10909 fn sample(param1: u8, param2: u8) {}
10910 "});
10911
10912 let mocked_response = lsp::SignatureHelp {
10913 signatures: vec![lsp::SignatureInformation {
10914 label: "fn sample(param1: u8, param2: u8)".to_string(),
10915 documentation: None,
10916 parameters: Some(vec![
10917 lsp::ParameterInformation {
10918 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10919 documentation: None,
10920 },
10921 lsp::ParameterInformation {
10922 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10923 documentation: None,
10924 },
10925 ]),
10926 active_parameter: None,
10927 }],
10928 active_signature: Some(0),
10929 active_parameter: Some(1),
10930 };
10931 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10932 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10933 .await;
10934
10935 // When selecting a range, the popover is gone.
10936 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
10937 cx.update_editor(|editor, window, cx| {
10938 editor.change_selections(None, window, cx, |s| {
10939 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
10940 })
10941 });
10942 cx.assert_editor_state(indoc! {"
10943 fn main() {
10944 sample(param1, «ˇparam2»);
10945 }
10946
10947 fn sample(param1: u8, param2: u8) {}
10948 "});
10949 cx.editor(|editor, _, _| {
10950 assert!(!editor.signature_help_state.is_shown());
10951 });
10952
10953 // When unselecting again, the popover is back if within the brackets.
10954 cx.update_editor(|editor, window, cx| {
10955 editor.change_selections(None, window, cx, |s| {
10956 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10957 })
10958 });
10959 cx.assert_editor_state(indoc! {"
10960 fn main() {
10961 sample(param1, ˇparam2);
10962 }
10963
10964 fn sample(param1: u8, param2: u8) {}
10965 "});
10966 handle_signature_help_request(&mut cx, mocked_response).await;
10967 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10968 .await;
10969 cx.editor(|editor, _, _| {
10970 assert!(editor.signature_help_state.is_shown());
10971 });
10972
10973 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
10974 cx.update_editor(|editor, window, cx| {
10975 editor.change_selections(None, window, cx, |s| {
10976 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
10977 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10978 })
10979 });
10980 cx.assert_editor_state(indoc! {"
10981 fn main() {
10982 sample(param1, ˇparam2);
10983 }
10984
10985 fn sample(param1: u8, param2: u8) {}
10986 "});
10987
10988 let mocked_response = lsp::SignatureHelp {
10989 signatures: vec![lsp::SignatureInformation {
10990 label: "fn sample(param1: u8, param2: u8)".to_string(),
10991 documentation: None,
10992 parameters: Some(vec![
10993 lsp::ParameterInformation {
10994 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10995 documentation: None,
10996 },
10997 lsp::ParameterInformation {
10998 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10999 documentation: None,
11000 },
11001 ]),
11002 active_parameter: None,
11003 }],
11004 active_signature: Some(0),
11005 active_parameter: Some(1),
11006 };
11007 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11008 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11009 .await;
11010 cx.update_editor(|editor, _, cx| {
11011 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
11012 });
11013 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
11014 .await;
11015 cx.update_editor(|editor, window, cx| {
11016 editor.change_selections(None, window, cx, |s| {
11017 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
11018 })
11019 });
11020 cx.assert_editor_state(indoc! {"
11021 fn main() {
11022 sample(param1, «ˇparam2»);
11023 }
11024
11025 fn sample(param1: u8, param2: u8) {}
11026 "});
11027 cx.update_editor(|editor, window, cx| {
11028 editor.change_selections(None, window, cx, |s| {
11029 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11030 })
11031 });
11032 cx.assert_editor_state(indoc! {"
11033 fn main() {
11034 sample(param1, ˇparam2);
11035 }
11036
11037 fn sample(param1: u8, param2: u8) {}
11038 "});
11039 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
11040 .await;
11041}
11042
11043#[gpui::test]
11044async fn test_completion_mode(cx: &mut TestAppContext) {
11045 init_test(cx, |_| {});
11046 let mut cx = EditorLspTestContext::new_rust(
11047 lsp::ServerCapabilities {
11048 completion_provider: Some(lsp::CompletionOptions {
11049 resolve_provider: Some(true),
11050 ..Default::default()
11051 }),
11052 ..Default::default()
11053 },
11054 cx,
11055 )
11056 .await;
11057
11058 struct Run {
11059 run_description: &'static str,
11060 initial_state: String,
11061 buffer_marked_text: String,
11062 completion_label: &'static str,
11063 completion_text: &'static str,
11064 expected_with_insert_mode: String,
11065 expected_with_replace_mode: String,
11066 expected_with_replace_subsequence_mode: String,
11067 expected_with_replace_suffix_mode: String,
11068 }
11069
11070 let runs = [
11071 Run {
11072 run_description: "Start of word matches completion text",
11073 initial_state: "before ediˇ after".into(),
11074 buffer_marked_text: "before <edi|> after".into(),
11075 completion_label: "editor",
11076 completion_text: "editor",
11077 expected_with_insert_mode: "before editorˇ after".into(),
11078 expected_with_replace_mode: "before editorˇ after".into(),
11079 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11080 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11081 },
11082 Run {
11083 run_description: "Accept same text at the middle of the word",
11084 initial_state: "before ediˇtor after".into(),
11085 buffer_marked_text: "before <edi|tor> after".into(),
11086 completion_label: "editor",
11087 completion_text: "editor",
11088 expected_with_insert_mode: "before editorˇtor after".into(),
11089 expected_with_replace_mode: "before editorˇ after".into(),
11090 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11091 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11092 },
11093 Run {
11094 run_description: "End of word matches completion text -- cursor at end",
11095 initial_state: "before torˇ after".into(),
11096 buffer_marked_text: "before <tor|> after".into(),
11097 completion_label: "editor",
11098 completion_text: "editor",
11099 expected_with_insert_mode: "before editorˇ after".into(),
11100 expected_with_replace_mode: "before editorˇ after".into(),
11101 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11102 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11103 },
11104 Run {
11105 run_description: "End of word matches completion text -- cursor at start",
11106 initial_state: "before ˇtor after".into(),
11107 buffer_marked_text: "before <|tor> after".into(),
11108 completion_label: "editor",
11109 completion_text: "editor",
11110 expected_with_insert_mode: "before editorˇtor after".into(),
11111 expected_with_replace_mode: "before editorˇ after".into(),
11112 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11113 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11114 },
11115 Run {
11116 run_description: "Prepend text containing whitespace",
11117 initial_state: "pˇfield: bool".into(),
11118 buffer_marked_text: "<p|field>: bool".into(),
11119 completion_label: "pub ",
11120 completion_text: "pub ",
11121 expected_with_insert_mode: "pub ˇfield: bool".into(),
11122 expected_with_replace_mode: "pub ˇ: bool".into(),
11123 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
11124 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
11125 },
11126 Run {
11127 run_description: "Add element to start of list",
11128 initial_state: "[element_ˇelement_2]".into(),
11129 buffer_marked_text: "[<element_|element_2>]".into(),
11130 completion_label: "element_1",
11131 completion_text: "element_1",
11132 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
11133 expected_with_replace_mode: "[element_1ˇ]".into(),
11134 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
11135 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
11136 },
11137 Run {
11138 run_description: "Add element to start of list -- first and second elements are equal",
11139 initial_state: "[elˇelement]".into(),
11140 buffer_marked_text: "[<el|element>]".into(),
11141 completion_label: "element",
11142 completion_text: "element",
11143 expected_with_insert_mode: "[elementˇelement]".into(),
11144 expected_with_replace_mode: "[elementˇ]".into(),
11145 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
11146 expected_with_replace_suffix_mode: "[elementˇ]".into(),
11147 },
11148 Run {
11149 run_description: "Ends with matching suffix",
11150 initial_state: "SubˇError".into(),
11151 buffer_marked_text: "<Sub|Error>".into(),
11152 completion_label: "SubscriptionError",
11153 completion_text: "SubscriptionError",
11154 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
11155 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11156 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11157 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
11158 },
11159 Run {
11160 run_description: "Suffix is a subsequence -- contiguous",
11161 initial_state: "SubˇErr".into(),
11162 buffer_marked_text: "<Sub|Err>".into(),
11163 completion_label: "SubscriptionError",
11164 completion_text: "SubscriptionError",
11165 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
11166 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11167 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11168 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
11169 },
11170 Run {
11171 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
11172 initial_state: "Suˇscrirr".into(),
11173 buffer_marked_text: "<Su|scrirr>".into(),
11174 completion_label: "SubscriptionError",
11175 completion_text: "SubscriptionError",
11176 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
11177 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11178 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11179 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
11180 },
11181 Run {
11182 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
11183 initial_state: "foo(indˇix)".into(),
11184 buffer_marked_text: "foo(<ind|ix>)".into(),
11185 completion_label: "node_index",
11186 completion_text: "node_index",
11187 expected_with_insert_mode: "foo(node_indexˇix)".into(),
11188 expected_with_replace_mode: "foo(node_indexˇ)".into(),
11189 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
11190 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
11191 },
11192 Run {
11193 run_description: "Replace range ends before cursor - should extend to cursor",
11194 initial_state: "before editˇo after".into(),
11195 buffer_marked_text: "before <{ed}>it|o after".into(),
11196 completion_label: "editor",
11197 completion_text: "editor",
11198 expected_with_insert_mode: "before editorˇo after".into(),
11199 expected_with_replace_mode: "before editorˇo after".into(),
11200 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
11201 expected_with_replace_suffix_mode: "before editorˇo after".into(),
11202 },
11203 Run {
11204 run_description: "Uses label for suffix matching",
11205 initial_state: "before ediˇtor after".into(),
11206 buffer_marked_text: "before <edi|tor> after".into(),
11207 completion_label: "editor",
11208 completion_text: "editor()",
11209 expected_with_insert_mode: "before editor()ˇtor after".into(),
11210 expected_with_replace_mode: "before editor()ˇ after".into(),
11211 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
11212 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
11213 },
11214 Run {
11215 run_description: "Case insensitive subsequence and suffix matching",
11216 initial_state: "before EDiˇtoR after".into(),
11217 buffer_marked_text: "before <EDi|toR> after".into(),
11218 completion_label: "editor",
11219 completion_text: "editor",
11220 expected_with_insert_mode: "before editorˇtoR after".into(),
11221 expected_with_replace_mode: "before editorˇ after".into(),
11222 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11223 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11224 },
11225 ];
11226
11227 for run in runs {
11228 let run_variations = [
11229 (LspInsertMode::Insert, run.expected_with_insert_mode),
11230 (LspInsertMode::Replace, run.expected_with_replace_mode),
11231 (
11232 LspInsertMode::ReplaceSubsequence,
11233 run.expected_with_replace_subsequence_mode,
11234 ),
11235 (
11236 LspInsertMode::ReplaceSuffix,
11237 run.expected_with_replace_suffix_mode,
11238 ),
11239 ];
11240
11241 for (lsp_insert_mode, expected_text) in run_variations {
11242 eprintln!(
11243 "run = {:?}, mode = {lsp_insert_mode:.?}",
11244 run.run_description,
11245 );
11246
11247 update_test_language_settings(&mut cx, |settings| {
11248 settings.defaults.completions = Some(CompletionSettings {
11249 lsp_insert_mode,
11250 words: WordsCompletionMode::Disabled,
11251 lsp: true,
11252 lsp_fetch_timeout_ms: 0,
11253 });
11254 });
11255
11256 cx.set_state(&run.initial_state);
11257 cx.update_editor(|editor, window, cx| {
11258 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11259 });
11260
11261 let counter = Arc::new(AtomicUsize::new(0));
11262 handle_completion_request_with_insert_and_replace(
11263 &mut cx,
11264 &run.buffer_marked_text,
11265 vec![(run.completion_label, run.completion_text)],
11266 counter.clone(),
11267 )
11268 .await;
11269 cx.condition(|editor, _| editor.context_menu_visible())
11270 .await;
11271 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11272
11273 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11274 editor
11275 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11276 .unwrap()
11277 });
11278 cx.assert_editor_state(&expected_text);
11279 handle_resolve_completion_request(&mut cx, None).await;
11280 apply_additional_edits.await.unwrap();
11281 }
11282 }
11283}
11284
11285#[gpui::test]
11286async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
11287 init_test(cx, |_| {});
11288 let mut cx = EditorLspTestContext::new_rust(
11289 lsp::ServerCapabilities {
11290 completion_provider: Some(lsp::CompletionOptions {
11291 resolve_provider: Some(true),
11292 ..Default::default()
11293 }),
11294 ..Default::default()
11295 },
11296 cx,
11297 )
11298 .await;
11299
11300 let initial_state = "SubˇError";
11301 let buffer_marked_text = "<Sub|Error>";
11302 let completion_text = "SubscriptionError";
11303 let expected_with_insert_mode = "SubscriptionErrorˇError";
11304 let expected_with_replace_mode = "SubscriptionErrorˇ";
11305
11306 update_test_language_settings(&mut cx, |settings| {
11307 settings.defaults.completions = Some(CompletionSettings {
11308 words: WordsCompletionMode::Disabled,
11309 // set the opposite here to ensure that the action is overriding the default behavior
11310 lsp_insert_mode: LspInsertMode::Insert,
11311 lsp: true,
11312 lsp_fetch_timeout_ms: 0,
11313 });
11314 });
11315
11316 cx.set_state(initial_state);
11317 cx.update_editor(|editor, window, cx| {
11318 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11319 });
11320
11321 let counter = Arc::new(AtomicUsize::new(0));
11322 handle_completion_request_with_insert_and_replace(
11323 &mut cx,
11324 &buffer_marked_text,
11325 vec![(completion_text, completion_text)],
11326 counter.clone(),
11327 )
11328 .await;
11329 cx.condition(|editor, _| editor.context_menu_visible())
11330 .await;
11331 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11332
11333 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11334 editor
11335 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11336 .unwrap()
11337 });
11338 cx.assert_editor_state(&expected_with_replace_mode);
11339 handle_resolve_completion_request(&mut cx, None).await;
11340 apply_additional_edits.await.unwrap();
11341
11342 update_test_language_settings(&mut cx, |settings| {
11343 settings.defaults.completions = Some(CompletionSettings {
11344 words: WordsCompletionMode::Disabled,
11345 // set the opposite here to ensure that the action is overriding the default behavior
11346 lsp_insert_mode: LspInsertMode::Replace,
11347 lsp: true,
11348 lsp_fetch_timeout_ms: 0,
11349 });
11350 });
11351
11352 cx.set_state(initial_state);
11353 cx.update_editor(|editor, window, cx| {
11354 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11355 });
11356 handle_completion_request_with_insert_and_replace(
11357 &mut cx,
11358 &buffer_marked_text,
11359 vec![(completion_text, completion_text)],
11360 counter.clone(),
11361 )
11362 .await;
11363 cx.condition(|editor, _| editor.context_menu_visible())
11364 .await;
11365 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
11366
11367 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11368 editor
11369 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
11370 .unwrap()
11371 });
11372 cx.assert_editor_state(&expected_with_insert_mode);
11373 handle_resolve_completion_request(&mut cx, None).await;
11374 apply_additional_edits.await.unwrap();
11375}
11376
11377#[gpui::test]
11378async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
11379 init_test(cx, |_| {});
11380 let mut cx = EditorLspTestContext::new_rust(
11381 lsp::ServerCapabilities {
11382 completion_provider: Some(lsp::CompletionOptions {
11383 resolve_provider: Some(true),
11384 ..Default::default()
11385 }),
11386 ..Default::default()
11387 },
11388 cx,
11389 )
11390 .await;
11391
11392 // scenario: surrounding text matches completion text
11393 let completion_text = "to_offset";
11394 let initial_state = indoc! {"
11395 1. buf.to_offˇsuffix
11396 2. buf.to_offˇsuf
11397 3. buf.to_offˇfix
11398 4. buf.to_offˇ
11399 5. into_offˇensive
11400 6. ˇsuffix
11401 7. let ˇ //
11402 8. aaˇzz
11403 9. buf.to_off«zzzzzˇ»suffix
11404 10. buf.«ˇzzzzz»suffix
11405 11. to_off«ˇzzzzz»
11406
11407 buf.to_offˇsuffix // newest cursor
11408 "};
11409 let completion_marked_buffer = indoc! {"
11410 1. buf.to_offsuffix
11411 2. buf.to_offsuf
11412 3. buf.to_offfix
11413 4. buf.to_off
11414 5. into_offensive
11415 6. suffix
11416 7. let //
11417 8. aazz
11418 9. buf.to_offzzzzzsuffix
11419 10. buf.zzzzzsuffix
11420 11. to_offzzzzz
11421
11422 buf.<to_off|suffix> // newest cursor
11423 "};
11424 let expected = indoc! {"
11425 1. buf.to_offsetˇ
11426 2. buf.to_offsetˇsuf
11427 3. buf.to_offsetˇfix
11428 4. buf.to_offsetˇ
11429 5. into_offsetˇensive
11430 6. to_offsetˇsuffix
11431 7. let to_offsetˇ //
11432 8. aato_offsetˇzz
11433 9. buf.to_offsetˇ
11434 10. buf.to_offsetˇsuffix
11435 11. to_offsetˇ
11436
11437 buf.to_offsetˇ // newest cursor
11438 "};
11439 cx.set_state(initial_state);
11440 cx.update_editor(|editor, window, cx| {
11441 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11442 });
11443 handle_completion_request_with_insert_and_replace(
11444 &mut cx,
11445 completion_marked_buffer,
11446 vec![(completion_text, completion_text)],
11447 Arc::new(AtomicUsize::new(0)),
11448 )
11449 .await;
11450 cx.condition(|editor, _| editor.context_menu_visible())
11451 .await;
11452 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11453 editor
11454 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11455 .unwrap()
11456 });
11457 cx.assert_editor_state(expected);
11458 handle_resolve_completion_request(&mut cx, None).await;
11459 apply_additional_edits.await.unwrap();
11460
11461 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
11462 let completion_text = "foo_and_bar";
11463 let initial_state = indoc! {"
11464 1. ooanbˇ
11465 2. zooanbˇ
11466 3. ooanbˇz
11467 4. zooanbˇz
11468 5. ooanˇ
11469 6. oanbˇ
11470
11471 ooanbˇ
11472 "};
11473 let completion_marked_buffer = indoc! {"
11474 1. ooanb
11475 2. zooanb
11476 3. ooanbz
11477 4. zooanbz
11478 5. ooan
11479 6. oanb
11480
11481 <ooanb|>
11482 "};
11483 let expected = indoc! {"
11484 1. foo_and_barˇ
11485 2. zfoo_and_barˇ
11486 3. foo_and_barˇz
11487 4. zfoo_and_barˇz
11488 5. ooanfoo_and_barˇ
11489 6. oanbfoo_and_barˇ
11490
11491 foo_and_barˇ
11492 "};
11493 cx.set_state(initial_state);
11494 cx.update_editor(|editor, window, cx| {
11495 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11496 });
11497 handle_completion_request_with_insert_and_replace(
11498 &mut cx,
11499 completion_marked_buffer,
11500 vec![(completion_text, completion_text)],
11501 Arc::new(AtomicUsize::new(0)),
11502 )
11503 .await;
11504 cx.condition(|editor, _| editor.context_menu_visible())
11505 .await;
11506 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11507 editor
11508 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11509 .unwrap()
11510 });
11511 cx.assert_editor_state(expected);
11512 handle_resolve_completion_request(&mut cx, None).await;
11513 apply_additional_edits.await.unwrap();
11514
11515 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
11516 // (expects the same as if it was inserted at the end)
11517 let completion_text = "foo_and_bar";
11518 let initial_state = indoc! {"
11519 1. ooˇanb
11520 2. zooˇanb
11521 3. ooˇanbz
11522 4. zooˇanbz
11523
11524 ooˇanb
11525 "};
11526 let completion_marked_buffer = indoc! {"
11527 1. ooanb
11528 2. zooanb
11529 3. ooanbz
11530 4. zooanbz
11531
11532 <oo|anb>
11533 "};
11534 let expected = indoc! {"
11535 1. foo_and_barˇ
11536 2. zfoo_and_barˇ
11537 3. foo_and_barˇz
11538 4. zfoo_and_barˇz
11539
11540 foo_and_barˇ
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 handle_completion_request_with_insert_and_replace(
11547 &mut cx,
11548 completion_marked_buffer,
11549 vec![(completion_text, completion_text)],
11550 Arc::new(AtomicUsize::new(0)),
11551 )
11552 .await;
11553 cx.condition(|editor, _| editor.context_menu_visible())
11554 .await;
11555 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11556 editor
11557 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11558 .unwrap()
11559 });
11560 cx.assert_editor_state(expected);
11561 handle_resolve_completion_request(&mut cx, None).await;
11562 apply_additional_edits.await.unwrap();
11563}
11564
11565// This used to crash
11566#[gpui::test]
11567async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
11568 init_test(cx, |_| {});
11569
11570 let buffer_text = indoc! {"
11571 fn main() {
11572 10.satu;
11573
11574 //
11575 // separate cursors so they open in different excerpts (manually reproducible)
11576 //
11577
11578 10.satu20;
11579 }
11580 "};
11581 let multibuffer_text_with_selections = indoc! {"
11582 fn main() {
11583 10.satuˇ;
11584
11585 //
11586
11587 //
11588
11589 10.satuˇ20;
11590 }
11591 "};
11592 let expected_multibuffer = indoc! {"
11593 fn main() {
11594 10.saturating_sub()ˇ;
11595
11596 //
11597
11598 //
11599
11600 10.saturating_sub()ˇ;
11601 }
11602 "};
11603
11604 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
11605 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
11606
11607 let fs = FakeFs::new(cx.executor());
11608 fs.insert_tree(
11609 path!("/a"),
11610 json!({
11611 "main.rs": buffer_text,
11612 }),
11613 )
11614 .await;
11615
11616 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11617 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11618 language_registry.add(rust_lang());
11619 let mut fake_servers = language_registry.register_fake_lsp(
11620 "Rust",
11621 FakeLspAdapter {
11622 capabilities: lsp::ServerCapabilities {
11623 completion_provider: Some(lsp::CompletionOptions {
11624 resolve_provider: None,
11625 ..lsp::CompletionOptions::default()
11626 }),
11627 ..lsp::ServerCapabilities::default()
11628 },
11629 ..FakeLspAdapter::default()
11630 },
11631 );
11632 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11633 let cx = &mut VisualTestContext::from_window(*workspace, cx);
11634 let buffer = project
11635 .update(cx, |project, cx| {
11636 project.open_local_buffer(path!("/a/main.rs"), cx)
11637 })
11638 .await
11639 .unwrap();
11640
11641 let multi_buffer = cx.new(|cx| {
11642 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
11643 multi_buffer.push_excerpts(
11644 buffer.clone(),
11645 [ExcerptRange::new(0..first_excerpt_end)],
11646 cx,
11647 );
11648 multi_buffer.push_excerpts(
11649 buffer.clone(),
11650 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
11651 cx,
11652 );
11653 multi_buffer
11654 });
11655
11656 let editor = workspace
11657 .update(cx, |_, window, cx| {
11658 cx.new(|cx| {
11659 Editor::new(
11660 EditorMode::Full {
11661 scale_ui_elements_with_buffer_font_size: false,
11662 show_active_line_background: false,
11663 sized_by_content: false,
11664 },
11665 multi_buffer.clone(),
11666 Some(project.clone()),
11667 window,
11668 cx,
11669 )
11670 })
11671 })
11672 .unwrap();
11673
11674 let pane = workspace
11675 .update(cx, |workspace, _, _| workspace.active_pane().clone())
11676 .unwrap();
11677 pane.update_in(cx, |pane, window, cx| {
11678 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
11679 });
11680
11681 let fake_server = fake_servers.next().await.unwrap();
11682
11683 editor.update_in(cx, |editor, window, cx| {
11684 editor.change_selections(None, window, cx, |s| {
11685 s.select_ranges([
11686 Point::new(1, 11)..Point::new(1, 11),
11687 Point::new(7, 11)..Point::new(7, 11),
11688 ])
11689 });
11690
11691 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
11692 });
11693
11694 editor.update_in(cx, |editor, window, cx| {
11695 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11696 });
11697
11698 fake_server
11699 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11700 let completion_item = lsp::CompletionItem {
11701 label: "saturating_sub()".into(),
11702 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
11703 lsp::InsertReplaceEdit {
11704 new_text: "saturating_sub()".to_owned(),
11705 insert: lsp::Range::new(
11706 lsp::Position::new(7, 7),
11707 lsp::Position::new(7, 11),
11708 ),
11709 replace: lsp::Range::new(
11710 lsp::Position::new(7, 7),
11711 lsp::Position::new(7, 13),
11712 ),
11713 },
11714 )),
11715 ..lsp::CompletionItem::default()
11716 };
11717
11718 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
11719 })
11720 .next()
11721 .await
11722 .unwrap();
11723
11724 cx.condition(&editor, |editor, _| editor.context_menu_visible())
11725 .await;
11726
11727 editor
11728 .update_in(cx, |editor, window, cx| {
11729 editor
11730 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11731 .unwrap()
11732 })
11733 .await
11734 .unwrap();
11735
11736 editor.update(cx, |editor, cx| {
11737 assert_text_with_selections(editor, expected_multibuffer, cx);
11738 })
11739}
11740
11741#[gpui::test]
11742async fn test_completion(cx: &mut TestAppContext) {
11743 init_test(cx, |_| {});
11744
11745 let mut cx = EditorLspTestContext::new_rust(
11746 lsp::ServerCapabilities {
11747 completion_provider: Some(lsp::CompletionOptions {
11748 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11749 resolve_provider: Some(true),
11750 ..Default::default()
11751 }),
11752 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11753 ..Default::default()
11754 },
11755 cx,
11756 )
11757 .await;
11758 let counter = Arc::new(AtomicUsize::new(0));
11759
11760 cx.set_state(indoc! {"
11761 oneˇ
11762 two
11763 three
11764 "});
11765 cx.simulate_keystroke(".");
11766 handle_completion_request(
11767 indoc! {"
11768 one.|<>
11769 two
11770 three
11771 "},
11772 vec!["first_completion", "second_completion"],
11773 true,
11774 counter.clone(),
11775 &mut cx,
11776 )
11777 .await;
11778 cx.condition(|editor, _| editor.context_menu_visible())
11779 .await;
11780 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11781
11782 let _handler = handle_signature_help_request(
11783 &mut cx,
11784 lsp::SignatureHelp {
11785 signatures: vec![lsp::SignatureInformation {
11786 label: "test signature".to_string(),
11787 documentation: None,
11788 parameters: Some(vec![lsp::ParameterInformation {
11789 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
11790 documentation: None,
11791 }]),
11792 active_parameter: None,
11793 }],
11794 active_signature: None,
11795 active_parameter: None,
11796 },
11797 );
11798 cx.update_editor(|editor, window, cx| {
11799 assert!(
11800 !editor.signature_help_state.is_shown(),
11801 "No signature help was called for"
11802 );
11803 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11804 });
11805 cx.run_until_parked();
11806 cx.update_editor(|editor, _, _| {
11807 assert!(
11808 !editor.signature_help_state.is_shown(),
11809 "No signature help should be shown when completions menu is open"
11810 );
11811 });
11812
11813 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11814 editor.context_menu_next(&Default::default(), window, cx);
11815 editor
11816 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11817 .unwrap()
11818 });
11819 cx.assert_editor_state(indoc! {"
11820 one.second_completionˇ
11821 two
11822 three
11823 "});
11824
11825 handle_resolve_completion_request(
11826 &mut cx,
11827 Some(vec![
11828 (
11829 //This overlaps with the primary completion edit which is
11830 //misbehavior from the LSP spec, test that we filter it out
11831 indoc! {"
11832 one.second_ˇcompletion
11833 two
11834 threeˇ
11835 "},
11836 "overlapping additional edit",
11837 ),
11838 (
11839 indoc! {"
11840 one.second_completion
11841 two
11842 threeˇ
11843 "},
11844 "\nadditional edit",
11845 ),
11846 ]),
11847 )
11848 .await;
11849 apply_additional_edits.await.unwrap();
11850 cx.assert_editor_state(indoc! {"
11851 one.second_completionˇ
11852 two
11853 three
11854 additional edit
11855 "});
11856
11857 cx.set_state(indoc! {"
11858 one.second_completion
11859 twoˇ
11860 threeˇ
11861 additional edit
11862 "});
11863 cx.simulate_keystroke(" ");
11864 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11865 cx.simulate_keystroke("s");
11866 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11867
11868 cx.assert_editor_state(indoc! {"
11869 one.second_completion
11870 two sˇ
11871 three sˇ
11872 additional edit
11873 "});
11874 handle_completion_request(
11875 indoc! {"
11876 one.second_completion
11877 two s
11878 three <s|>
11879 additional edit
11880 "},
11881 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
11882 true,
11883 counter.clone(),
11884 &mut cx,
11885 )
11886 .await;
11887 cx.condition(|editor, _| editor.context_menu_visible())
11888 .await;
11889 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
11890
11891 cx.simulate_keystroke("i");
11892
11893 handle_completion_request(
11894 indoc! {"
11895 one.second_completion
11896 two si
11897 three <si|>
11898 additional edit
11899 "},
11900 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
11901 true,
11902 counter.clone(),
11903 &mut cx,
11904 )
11905 .await;
11906 cx.condition(|editor, _| editor.context_menu_visible())
11907 .await;
11908 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
11909
11910 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11911 editor
11912 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11913 .unwrap()
11914 });
11915 cx.assert_editor_state(indoc! {"
11916 one.second_completion
11917 two sixth_completionˇ
11918 three sixth_completionˇ
11919 additional edit
11920 "});
11921
11922 apply_additional_edits.await.unwrap();
11923
11924 update_test_language_settings(&mut cx, |settings| {
11925 settings.defaults.show_completions_on_input = Some(false);
11926 });
11927 cx.set_state("editorˇ");
11928 cx.simulate_keystroke(".");
11929 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11930 cx.simulate_keystrokes("c l o");
11931 cx.assert_editor_state("editor.cloˇ");
11932 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11933 cx.update_editor(|editor, window, cx| {
11934 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11935 });
11936 handle_completion_request(
11937 "editor.<clo|>",
11938 vec!["close", "clobber"],
11939 true,
11940 counter.clone(),
11941 &mut cx,
11942 )
11943 .await;
11944 cx.condition(|editor, _| editor.context_menu_visible())
11945 .await;
11946 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
11947
11948 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11949 editor
11950 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11951 .unwrap()
11952 });
11953 cx.assert_editor_state("editor.clobberˇ");
11954 handle_resolve_completion_request(&mut cx, None).await;
11955 apply_additional_edits.await.unwrap();
11956}
11957
11958#[gpui::test]
11959async fn test_completion_reuse(cx: &mut TestAppContext) {
11960 init_test(cx, |_| {});
11961
11962 let mut cx = EditorLspTestContext::new_rust(
11963 lsp::ServerCapabilities {
11964 completion_provider: Some(lsp::CompletionOptions {
11965 trigger_characters: Some(vec![".".to_string()]),
11966 ..Default::default()
11967 }),
11968 ..Default::default()
11969 },
11970 cx,
11971 )
11972 .await;
11973
11974 let counter = Arc::new(AtomicUsize::new(0));
11975 cx.set_state("objˇ");
11976 cx.simulate_keystroke(".");
11977
11978 // Initial completion request returns complete results
11979 let is_incomplete = false;
11980 handle_completion_request(
11981 "obj.|<>",
11982 vec!["a", "ab", "abc"],
11983 is_incomplete,
11984 counter.clone(),
11985 &mut cx,
11986 )
11987 .await;
11988 cx.run_until_parked();
11989 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11990 cx.assert_editor_state("obj.ˇ");
11991 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
11992
11993 // Type "a" - filters existing completions
11994 cx.simulate_keystroke("a");
11995 cx.run_until_parked();
11996 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11997 cx.assert_editor_state("obj.aˇ");
11998 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
11999
12000 // Type "b" - filters existing completions
12001 cx.simulate_keystroke("b");
12002 cx.run_until_parked();
12003 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12004 cx.assert_editor_state("obj.abˇ");
12005 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12006
12007 // Type "c" - filters existing completions
12008 cx.simulate_keystroke("c");
12009 cx.run_until_parked();
12010 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12011 cx.assert_editor_state("obj.abcˇ");
12012 check_displayed_completions(vec!["abc"], &mut cx);
12013
12014 // Backspace to delete "c" - filters existing completions
12015 cx.update_editor(|editor, window, cx| {
12016 editor.backspace(&Backspace, window, cx);
12017 });
12018 cx.run_until_parked();
12019 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12020 cx.assert_editor_state("obj.abˇ");
12021 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12022
12023 // Moving cursor to the left dismisses menu.
12024 cx.update_editor(|editor, window, cx| {
12025 editor.move_left(&MoveLeft, window, cx);
12026 });
12027 cx.run_until_parked();
12028 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12029 cx.assert_editor_state("obj.aˇb");
12030 cx.update_editor(|editor, _, _| {
12031 assert_eq!(editor.context_menu_visible(), false);
12032 });
12033
12034 // Type "b" - new request
12035 cx.simulate_keystroke("b");
12036 let is_incomplete = false;
12037 handle_completion_request(
12038 "obj.<ab|>a",
12039 vec!["ab", "abc"],
12040 is_incomplete,
12041 counter.clone(),
12042 &mut cx,
12043 )
12044 .await;
12045 cx.run_until_parked();
12046 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12047 cx.assert_editor_state("obj.abˇb");
12048 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12049
12050 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
12051 cx.update_editor(|editor, window, cx| {
12052 editor.backspace(&Backspace, window, cx);
12053 });
12054 let is_incomplete = false;
12055 handle_completion_request(
12056 "obj.<a|>b",
12057 vec!["a", "ab", "abc"],
12058 is_incomplete,
12059 counter.clone(),
12060 &mut cx,
12061 )
12062 .await;
12063 cx.run_until_parked();
12064 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12065 cx.assert_editor_state("obj.aˇb");
12066 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12067
12068 // Backspace to delete "a" - dismisses menu.
12069 cx.update_editor(|editor, window, cx| {
12070 editor.backspace(&Backspace, window, cx);
12071 });
12072 cx.run_until_parked();
12073 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12074 cx.assert_editor_state("obj.ˇb");
12075 cx.update_editor(|editor, _, _| {
12076 assert_eq!(editor.context_menu_visible(), false);
12077 });
12078}
12079
12080#[gpui::test]
12081async fn test_word_completion(cx: &mut TestAppContext) {
12082 let lsp_fetch_timeout_ms = 10;
12083 init_test(cx, |language_settings| {
12084 language_settings.defaults.completions = Some(CompletionSettings {
12085 words: WordsCompletionMode::Fallback,
12086 lsp: true,
12087 lsp_fetch_timeout_ms: 10,
12088 lsp_insert_mode: LspInsertMode::Insert,
12089 });
12090 });
12091
12092 let mut cx = EditorLspTestContext::new_rust(
12093 lsp::ServerCapabilities {
12094 completion_provider: Some(lsp::CompletionOptions {
12095 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12096 ..lsp::CompletionOptions::default()
12097 }),
12098 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12099 ..lsp::ServerCapabilities::default()
12100 },
12101 cx,
12102 )
12103 .await;
12104
12105 let throttle_completions = Arc::new(AtomicBool::new(false));
12106
12107 let lsp_throttle_completions = throttle_completions.clone();
12108 let _completion_requests_handler =
12109 cx.lsp
12110 .server
12111 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
12112 let lsp_throttle_completions = lsp_throttle_completions.clone();
12113 let cx = cx.clone();
12114 async move {
12115 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
12116 cx.background_executor()
12117 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
12118 .await;
12119 }
12120 Ok(Some(lsp::CompletionResponse::Array(vec![
12121 lsp::CompletionItem {
12122 label: "first".into(),
12123 ..lsp::CompletionItem::default()
12124 },
12125 lsp::CompletionItem {
12126 label: "last".into(),
12127 ..lsp::CompletionItem::default()
12128 },
12129 ])))
12130 }
12131 });
12132
12133 cx.set_state(indoc! {"
12134 oneˇ
12135 two
12136 three
12137 "});
12138 cx.simulate_keystroke(".");
12139 cx.executor().run_until_parked();
12140 cx.condition(|editor, _| editor.context_menu_visible())
12141 .await;
12142 cx.update_editor(|editor, window, cx| {
12143 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12144 {
12145 assert_eq!(
12146 completion_menu_entries(&menu),
12147 &["first", "last"],
12148 "When LSP server is fast to reply, no fallback word completions are used"
12149 );
12150 } else {
12151 panic!("expected completion menu to be open");
12152 }
12153 editor.cancel(&Cancel, window, cx);
12154 });
12155 cx.executor().run_until_parked();
12156 cx.condition(|editor, _| !editor.context_menu_visible())
12157 .await;
12158
12159 throttle_completions.store(true, atomic::Ordering::Release);
12160 cx.simulate_keystroke(".");
12161 cx.executor()
12162 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
12163 cx.executor().run_until_parked();
12164 cx.condition(|editor, _| editor.context_menu_visible())
12165 .await;
12166 cx.update_editor(|editor, _, _| {
12167 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12168 {
12169 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
12170 "When LSP server is slow, document words can be shown instead, if configured accordingly");
12171 } else {
12172 panic!("expected completion menu to be open");
12173 }
12174 });
12175}
12176
12177#[gpui::test]
12178async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
12179 init_test(cx, |language_settings| {
12180 language_settings.defaults.completions = Some(CompletionSettings {
12181 words: WordsCompletionMode::Enabled,
12182 lsp: true,
12183 lsp_fetch_timeout_ms: 0,
12184 lsp_insert_mode: LspInsertMode::Insert,
12185 });
12186 });
12187
12188 let mut cx = EditorLspTestContext::new_rust(
12189 lsp::ServerCapabilities {
12190 completion_provider: Some(lsp::CompletionOptions {
12191 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12192 ..lsp::CompletionOptions::default()
12193 }),
12194 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12195 ..lsp::ServerCapabilities::default()
12196 },
12197 cx,
12198 )
12199 .await;
12200
12201 let _completion_requests_handler =
12202 cx.lsp
12203 .server
12204 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
12205 Ok(Some(lsp::CompletionResponse::Array(vec![
12206 lsp::CompletionItem {
12207 label: "first".into(),
12208 ..lsp::CompletionItem::default()
12209 },
12210 lsp::CompletionItem {
12211 label: "last".into(),
12212 ..lsp::CompletionItem::default()
12213 },
12214 ])))
12215 });
12216
12217 cx.set_state(indoc! {"ˇ
12218 first
12219 last
12220 second
12221 "});
12222 cx.simulate_keystroke(".");
12223 cx.executor().run_until_parked();
12224 cx.condition(|editor, _| editor.context_menu_visible())
12225 .await;
12226 cx.update_editor(|editor, _, _| {
12227 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12228 {
12229 assert_eq!(
12230 completion_menu_entries(&menu),
12231 &["first", "last", "second"],
12232 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
12233 );
12234 } else {
12235 panic!("expected completion menu to be open");
12236 }
12237 });
12238}
12239
12240#[gpui::test]
12241async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
12242 init_test(cx, |language_settings| {
12243 language_settings.defaults.completions = Some(CompletionSettings {
12244 words: WordsCompletionMode::Disabled,
12245 lsp: true,
12246 lsp_fetch_timeout_ms: 0,
12247 lsp_insert_mode: LspInsertMode::Insert,
12248 });
12249 });
12250
12251 let mut cx = EditorLspTestContext::new_rust(
12252 lsp::ServerCapabilities {
12253 completion_provider: Some(lsp::CompletionOptions {
12254 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12255 ..lsp::CompletionOptions::default()
12256 }),
12257 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12258 ..lsp::ServerCapabilities::default()
12259 },
12260 cx,
12261 )
12262 .await;
12263
12264 let _completion_requests_handler =
12265 cx.lsp
12266 .server
12267 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
12268 panic!("LSP completions should not be queried when dealing with word completions")
12269 });
12270
12271 cx.set_state(indoc! {"ˇ
12272 first
12273 last
12274 second
12275 "});
12276 cx.update_editor(|editor, window, cx| {
12277 editor.show_word_completions(&ShowWordCompletions, window, cx);
12278 });
12279 cx.executor().run_until_parked();
12280 cx.condition(|editor, _| editor.context_menu_visible())
12281 .await;
12282 cx.update_editor(|editor, _, _| {
12283 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12284 {
12285 assert_eq!(
12286 completion_menu_entries(&menu),
12287 &["first", "last", "second"],
12288 "`ShowWordCompletions` action should show word completions"
12289 );
12290 } else {
12291 panic!("expected completion menu to be open");
12292 }
12293 });
12294
12295 cx.simulate_keystroke("l");
12296 cx.executor().run_until_parked();
12297 cx.condition(|editor, _| editor.context_menu_visible())
12298 .await;
12299 cx.update_editor(|editor, _, _| {
12300 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12301 {
12302 assert_eq!(
12303 completion_menu_entries(&menu),
12304 &["last"],
12305 "After showing word completions, further editing should filter them and not query the LSP"
12306 );
12307 } else {
12308 panic!("expected completion menu to be open");
12309 }
12310 });
12311}
12312
12313#[gpui::test]
12314async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
12315 init_test(cx, |language_settings| {
12316 language_settings.defaults.completions = Some(CompletionSettings {
12317 words: WordsCompletionMode::Fallback,
12318 lsp: false,
12319 lsp_fetch_timeout_ms: 0,
12320 lsp_insert_mode: LspInsertMode::Insert,
12321 });
12322 });
12323
12324 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12325
12326 cx.set_state(indoc! {"ˇ
12327 0_usize
12328 let
12329 33
12330 4.5f32
12331 "});
12332 cx.update_editor(|editor, window, cx| {
12333 editor.show_completions(&ShowCompletions::default(), window, cx);
12334 });
12335 cx.executor().run_until_parked();
12336 cx.condition(|editor, _| editor.context_menu_visible())
12337 .await;
12338 cx.update_editor(|editor, window, cx| {
12339 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12340 {
12341 assert_eq!(
12342 completion_menu_entries(&menu),
12343 &["let"],
12344 "With no digits in the completion query, no digits should be in the word completions"
12345 );
12346 } else {
12347 panic!("expected completion menu to be open");
12348 }
12349 editor.cancel(&Cancel, window, cx);
12350 });
12351
12352 cx.set_state(indoc! {"3ˇ
12353 0_usize
12354 let
12355 3
12356 33.35f32
12357 "});
12358 cx.update_editor(|editor, window, cx| {
12359 editor.show_completions(&ShowCompletions::default(), window, cx);
12360 });
12361 cx.executor().run_until_parked();
12362 cx.condition(|editor, _| editor.context_menu_visible())
12363 .await;
12364 cx.update_editor(|editor, _, _| {
12365 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12366 {
12367 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
12368 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
12369 } else {
12370 panic!("expected completion menu to be open");
12371 }
12372 });
12373}
12374
12375fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
12376 let position = || lsp::Position {
12377 line: params.text_document_position.position.line,
12378 character: params.text_document_position.position.character,
12379 };
12380 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12381 range: lsp::Range {
12382 start: position(),
12383 end: position(),
12384 },
12385 new_text: text.to_string(),
12386 }))
12387}
12388
12389#[gpui::test]
12390async fn test_multiline_completion(cx: &mut TestAppContext) {
12391 init_test(cx, |_| {});
12392
12393 let fs = FakeFs::new(cx.executor());
12394 fs.insert_tree(
12395 path!("/a"),
12396 json!({
12397 "main.ts": "a",
12398 }),
12399 )
12400 .await;
12401
12402 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12403 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12404 let typescript_language = Arc::new(Language::new(
12405 LanguageConfig {
12406 name: "TypeScript".into(),
12407 matcher: LanguageMatcher {
12408 path_suffixes: vec!["ts".to_string()],
12409 ..LanguageMatcher::default()
12410 },
12411 line_comments: vec!["// ".into()],
12412 ..LanguageConfig::default()
12413 },
12414 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12415 ));
12416 language_registry.add(typescript_language.clone());
12417 let mut fake_servers = language_registry.register_fake_lsp(
12418 "TypeScript",
12419 FakeLspAdapter {
12420 capabilities: lsp::ServerCapabilities {
12421 completion_provider: Some(lsp::CompletionOptions {
12422 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12423 ..lsp::CompletionOptions::default()
12424 }),
12425 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12426 ..lsp::ServerCapabilities::default()
12427 },
12428 // Emulate vtsls label generation
12429 label_for_completion: Some(Box::new(|item, _| {
12430 let text = if let Some(description) = item
12431 .label_details
12432 .as_ref()
12433 .and_then(|label_details| label_details.description.as_ref())
12434 {
12435 format!("{} {}", item.label, description)
12436 } else if let Some(detail) = &item.detail {
12437 format!("{} {}", item.label, detail)
12438 } else {
12439 item.label.clone()
12440 };
12441 let len = text.len();
12442 Some(language::CodeLabel {
12443 text,
12444 runs: Vec::new(),
12445 filter_range: 0..len,
12446 })
12447 })),
12448 ..FakeLspAdapter::default()
12449 },
12450 );
12451 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12452 let cx = &mut VisualTestContext::from_window(*workspace, cx);
12453 let worktree_id = workspace
12454 .update(cx, |workspace, _window, cx| {
12455 workspace.project().update(cx, |project, cx| {
12456 project.worktrees(cx).next().unwrap().read(cx).id()
12457 })
12458 })
12459 .unwrap();
12460 let _buffer = project
12461 .update(cx, |project, cx| {
12462 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
12463 })
12464 .await
12465 .unwrap();
12466 let editor = workspace
12467 .update(cx, |workspace, window, cx| {
12468 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
12469 })
12470 .unwrap()
12471 .await
12472 .unwrap()
12473 .downcast::<Editor>()
12474 .unwrap();
12475 let fake_server = fake_servers.next().await.unwrap();
12476
12477 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
12478 let multiline_label_2 = "a\nb\nc\n";
12479 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
12480 let multiline_description = "d\ne\nf\n";
12481 let multiline_detail_2 = "g\nh\ni\n";
12482
12483 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
12484 move |params, _| async move {
12485 Ok(Some(lsp::CompletionResponse::Array(vec![
12486 lsp::CompletionItem {
12487 label: multiline_label.to_string(),
12488 text_edit: gen_text_edit(¶ms, "new_text_1"),
12489 ..lsp::CompletionItem::default()
12490 },
12491 lsp::CompletionItem {
12492 label: "single line label 1".to_string(),
12493 detail: Some(multiline_detail.to_string()),
12494 text_edit: gen_text_edit(¶ms, "new_text_2"),
12495 ..lsp::CompletionItem::default()
12496 },
12497 lsp::CompletionItem {
12498 label: "single line label 2".to_string(),
12499 label_details: Some(lsp::CompletionItemLabelDetails {
12500 description: Some(multiline_description.to_string()),
12501 detail: None,
12502 }),
12503 text_edit: gen_text_edit(¶ms, "new_text_2"),
12504 ..lsp::CompletionItem::default()
12505 },
12506 lsp::CompletionItem {
12507 label: multiline_label_2.to_string(),
12508 detail: Some(multiline_detail_2.to_string()),
12509 text_edit: gen_text_edit(¶ms, "new_text_3"),
12510 ..lsp::CompletionItem::default()
12511 },
12512 lsp::CompletionItem {
12513 label: "Label with many spaces and \t but without newlines".to_string(),
12514 detail: Some(
12515 "Details with many spaces and \t but without newlines".to_string(),
12516 ),
12517 text_edit: gen_text_edit(¶ms, "new_text_4"),
12518 ..lsp::CompletionItem::default()
12519 },
12520 ])))
12521 },
12522 );
12523
12524 editor.update_in(cx, |editor, window, cx| {
12525 cx.focus_self(window);
12526 editor.move_to_end(&MoveToEnd, window, cx);
12527 editor.handle_input(".", window, cx);
12528 });
12529 cx.run_until_parked();
12530 completion_handle.next().await.unwrap();
12531
12532 editor.update(cx, |editor, _| {
12533 assert!(editor.context_menu_visible());
12534 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12535 {
12536 let completion_labels = menu
12537 .completions
12538 .borrow()
12539 .iter()
12540 .map(|c| c.label.text.clone())
12541 .collect::<Vec<_>>();
12542 assert_eq!(
12543 completion_labels,
12544 &[
12545 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
12546 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
12547 "single line label 2 d e f ",
12548 "a b c g h i ",
12549 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
12550 ],
12551 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
12552 );
12553
12554 for completion in menu
12555 .completions
12556 .borrow()
12557 .iter() {
12558 assert_eq!(
12559 completion.label.filter_range,
12560 0..completion.label.text.len(),
12561 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
12562 );
12563 }
12564 } else {
12565 panic!("expected completion menu to be open");
12566 }
12567 });
12568}
12569
12570#[gpui::test]
12571async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
12572 init_test(cx, |_| {});
12573 let mut cx = EditorLspTestContext::new_rust(
12574 lsp::ServerCapabilities {
12575 completion_provider: Some(lsp::CompletionOptions {
12576 trigger_characters: Some(vec![".".to_string()]),
12577 ..Default::default()
12578 }),
12579 ..Default::default()
12580 },
12581 cx,
12582 )
12583 .await;
12584 cx.lsp
12585 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
12586 Ok(Some(lsp::CompletionResponse::Array(vec![
12587 lsp::CompletionItem {
12588 label: "first".into(),
12589 ..Default::default()
12590 },
12591 lsp::CompletionItem {
12592 label: "last".into(),
12593 ..Default::default()
12594 },
12595 ])))
12596 });
12597 cx.set_state("variableˇ");
12598 cx.simulate_keystroke(".");
12599 cx.executor().run_until_parked();
12600
12601 cx.update_editor(|editor, _, _| {
12602 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12603 {
12604 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
12605 } else {
12606 panic!("expected completion menu to be open");
12607 }
12608 });
12609
12610 cx.update_editor(|editor, window, cx| {
12611 editor.move_page_down(&MovePageDown::default(), window, cx);
12612 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12613 {
12614 assert!(
12615 menu.selected_item == 1,
12616 "expected PageDown to select the last item from the context menu"
12617 );
12618 } else {
12619 panic!("expected completion menu to stay open after PageDown");
12620 }
12621 });
12622
12623 cx.update_editor(|editor, window, cx| {
12624 editor.move_page_up(&MovePageUp::default(), window, cx);
12625 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12626 {
12627 assert!(
12628 menu.selected_item == 0,
12629 "expected PageUp to select the first item from the context menu"
12630 );
12631 } else {
12632 panic!("expected completion menu to stay open after PageUp");
12633 }
12634 });
12635}
12636
12637#[gpui::test]
12638async fn test_as_is_completions(cx: &mut TestAppContext) {
12639 init_test(cx, |_| {});
12640 let mut cx = EditorLspTestContext::new_rust(
12641 lsp::ServerCapabilities {
12642 completion_provider: Some(lsp::CompletionOptions {
12643 ..Default::default()
12644 }),
12645 ..Default::default()
12646 },
12647 cx,
12648 )
12649 .await;
12650 cx.lsp
12651 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
12652 Ok(Some(lsp::CompletionResponse::Array(vec![
12653 lsp::CompletionItem {
12654 label: "unsafe".into(),
12655 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12656 range: lsp::Range {
12657 start: lsp::Position {
12658 line: 1,
12659 character: 2,
12660 },
12661 end: lsp::Position {
12662 line: 1,
12663 character: 3,
12664 },
12665 },
12666 new_text: "unsafe".to_string(),
12667 })),
12668 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
12669 ..Default::default()
12670 },
12671 ])))
12672 });
12673 cx.set_state("fn a() {}\n nˇ");
12674 cx.executor().run_until_parked();
12675 cx.update_editor(|editor, window, cx| {
12676 editor.show_completions(
12677 &ShowCompletions {
12678 trigger: Some("\n".into()),
12679 },
12680 window,
12681 cx,
12682 );
12683 });
12684 cx.executor().run_until_parked();
12685
12686 cx.update_editor(|editor, window, cx| {
12687 editor.confirm_completion(&Default::default(), window, cx)
12688 });
12689 cx.executor().run_until_parked();
12690 cx.assert_editor_state("fn a() {}\n unsafeˇ");
12691}
12692
12693#[gpui::test]
12694async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
12695 init_test(cx, |_| {});
12696
12697 let mut cx = EditorLspTestContext::new_rust(
12698 lsp::ServerCapabilities {
12699 completion_provider: Some(lsp::CompletionOptions {
12700 trigger_characters: Some(vec![".".to_string()]),
12701 resolve_provider: Some(true),
12702 ..Default::default()
12703 }),
12704 ..Default::default()
12705 },
12706 cx,
12707 )
12708 .await;
12709
12710 cx.set_state("fn main() { let a = 2ˇ; }");
12711 cx.simulate_keystroke(".");
12712 let completion_item = lsp::CompletionItem {
12713 label: "Some".into(),
12714 kind: Some(lsp::CompletionItemKind::SNIPPET),
12715 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
12716 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
12717 kind: lsp::MarkupKind::Markdown,
12718 value: "```rust\nSome(2)\n```".to_string(),
12719 })),
12720 deprecated: Some(false),
12721 sort_text: Some("Some".to_string()),
12722 filter_text: Some("Some".to_string()),
12723 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
12724 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12725 range: lsp::Range {
12726 start: lsp::Position {
12727 line: 0,
12728 character: 22,
12729 },
12730 end: lsp::Position {
12731 line: 0,
12732 character: 22,
12733 },
12734 },
12735 new_text: "Some(2)".to_string(),
12736 })),
12737 additional_text_edits: Some(vec![lsp::TextEdit {
12738 range: lsp::Range {
12739 start: lsp::Position {
12740 line: 0,
12741 character: 20,
12742 },
12743 end: lsp::Position {
12744 line: 0,
12745 character: 22,
12746 },
12747 },
12748 new_text: "".to_string(),
12749 }]),
12750 ..Default::default()
12751 };
12752
12753 let closure_completion_item = completion_item.clone();
12754 let counter = Arc::new(AtomicUsize::new(0));
12755 let counter_clone = counter.clone();
12756 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
12757 let task_completion_item = closure_completion_item.clone();
12758 counter_clone.fetch_add(1, atomic::Ordering::Release);
12759 async move {
12760 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
12761 is_incomplete: true,
12762 item_defaults: None,
12763 items: vec![task_completion_item],
12764 })))
12765 }
12766 });
12767
12768 cx.condition(|editor, _| editor.context_menu_visible())
12769 .await;
12770 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
12771 assert!(request.next().await.is_some());
12772 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12773
12774 cx.simulate_keystrokes("S o m");
12775 cx.condition(|editor, _| editor.context_menu_visible())
12776 .await;
12777 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
12778 assert!(request.next().await.is_some());
12779 assert!(request.next().await.is_some());
12780 assert!(request.next().await.is_some());
12781 request.close();
12782 assert!(request.next().await.is_none());
12783 assert_eq!(
12784 counter.load(atomic::Ordering::Acquire),
12785 4,
12786 "With the completions menu open, only one LSP request should happen per input"
12787 );
12788}
12789
12790#[gpui::test]
12791async fn test_toggle_comment(cx: &mut TestAppContext) {
12792 init_test(cx, |_| {});
12793 let mut cx = EditorTestContext::new(cx).await;
12794 let language = Arc::new(Language::new(
12795 LanguageConfig {
12796 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
12797 ..Default::default()
12798 },
12799 Some(tree_sitter_rust::LANGUAGE.into()),
12800 ));
12801 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
12802
12803 // If multiple selections intersect a line, the line is only toggled once.
12804 cx.set_state(indoc! {"
12805 fn a() {
12806 «//b();
12807 ˇ»// «c();
12808 //ˇ» d();
12809 }
12810 "});
12811
12812 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12813
12814 cx.assert_editor_state(indoc! {"
12815 fn a() {
12816 «b();
12817 c();
12818 ˇ» d();
12819 }
12820 "});
12821
12822 // The comment prefix is inserted at the same column for every line in a
12823 // selection.
12824 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12825
12826 cx.assert_editor_state(indoc! {"
12827 fn a() {
12828 // «b();
12829 // c();
12830 ˇ»// d();
12831 }
12832 "});
12833
12834 // If a selection ends at the beginning of a line, that line is not toggled.
12835 cx.set_selections_state(indoc! {"
12836 fn a() {
12837 // b();
12838 «// c();
12839 ˇ» // d();
12840 }
12841 "});
12842
12843 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12844
12845 cx.assert_editor_state(indoc! {"
12846 fn a() {
12847 // b();
12848 «c();
12849 ˇ» // d();
12850 }
12851 "});
12852
12853 // If a selection span a single line and is empty, the line is toggled.
12854 cx.set_state(indoc! {"
12855 fn a() {
12856 a();
12857 b();
12858 ˇ
12859 }
12860 "});
12861
12862 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12863
12864 cx.assert_editor_state(indoc! {"
12865 fn a() {
12866 a();
12867 b();
12868 //•ˇ
12869 }
12870 "});
12871
12872 // If a selection span multiple lines, empty lines are not toggled.
12873 cx.set_state(indoc! {"
12874 fn a() {
12875 «a();
12876
12877 c();ˇ»
12878 }
12879 "});
12880
12881 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12882
12883 cx.assert_editor_state(indoc! {"
12884 fn a() {
12885 // «a();
12886
12887 // c();ˇ»
12888 }
12889 "});
12890
12891 // If a selection includes multiple comment prefixes, all lines are uncommented.
12892 cx.set_state(indoc! {"
12893 fn a() {
12894 «// a();
12895 /// b();
12896 //! c();ˇ»
12897 }
12898 "});
12899
12900 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12901
12902 cx.assert_editor_state(indoc! {"
12903 fn a() {
12904 «a();
12905 b();
12906 c();ˇ»
12907 }
12908 "});
12909}
12910
12911#[gpui::test]
12912async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
12913 init_test(cx, |_| {});
12914 let mut cx = EditorTestContext::new(cx).await;
12915 let language = Arc::new(Language::new(
12916 LanguageConfig {
12917 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
12918 ..Default::default()
12919 },
12920 Some(tree_sitter_rust::LANGUAGE.into()),
12921 ));
12922 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
12923
12924 let toggle_comments = &ToggleComments {
12925 advance_downwards: false,
12926 ignore_indent: true,
12927 };
12928
12929 // If multiple selections intersect a line, the line is only toggled once.
12930 cx.set_state(indoc! {"
12931 fn a() {
12932 // «b();
12933 // c();
12934 // ˇ» d();
12935 }
12936 "});
12937
12938 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12939
12940 cx.assert_editor_state(indoc! {"
12941 fn a() {
12942 «b();
12943 c();
12944 ˇ» d();
12945 }
12946 "});
12947
12948 // The comment prefix is inserted at the beginning of each line
12949 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12950
12951 cx.assert_editor_state(indoc! {"
12952 fn a() {
12953 // «b();
12954 // c();
12955 // ˇ» d();
12956 }
12957 "});
12958
12959 // If a selection ends at the beginning of a line, that line is not toggled.
12960 cx.set_selections_state(indoc! {"
12961 fn a() {
12962 // b();
12963 // «c();
12964 ˇ»// d();
12965 }
12966 "});
12967
12968 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12969
12970 cx.assert_editor_state(indoc! {"
12971 fn a() {
12972 // b();
12973 «c();
12974 ˇ»// d();
12975 }
12976 "});
12977
12978 // If a selection span a single line and is empty, the line is toggled.
12979 cx.set_state(indoc! {"
12980 fn a() {
12981 a();
12982 b();
12983 ˇ
12984 }
12985 "});
12986
12987 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12988
12989 cx.assert_editor_state(indoc! {"
12990 fn a() {
12991 a();
12992 b();
12993 //ˇ
12994 }
12995 "});
12996
12997 // If a selection span multiple lines, empty lines are not toggled.
12998 cx.set_state(indoc! {"
12999 fn a() {
13000 «a();
13001
13002 c();ˇ»
13003 }
13004 "});
13005
13006 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13007
13008 cx.assert_editor_state(indoc! {"
13009 fn a() {
13010 // «a();
13011
13012 // c();ˇ»
13013 }
13014 "});
13015
13016 // If a selection includes multiple comment prefixes, all lines are uncommented.
13017 cx.set_state(indoc! {"
13018 fn a() {
13019 // «a();
13020 /// b();
13021 //! c();ˇ»
13022 }
13023 "});
13024
13025 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13026
13027 cx.assert_editor_state(indoc! {"
13028 fn a() {
13029 «a();
13030 b();
13031 c();ˇ»
13032 }
13033 "});
13034}
13035
13036#[gpui::test]
13037async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
13038 init_test(cx, |_| {});
13039
13040 let language = Arc::new(Language::new(
13041 LanguageConfig {
13042 line_comments: vec!["// ".into()],
13043 ..Default::default()
13044 },
13045 Some(tree_sitter_rust::LANGUAGE.into()),
13046 ));
13047
13048 let mut cx = EditorTestContext::new(cx).await;
13049
13050 cx.language_registry().add(language.clone());
13051 cx.update_buffer(|buffer, cx| {
13052 buffer.set_language(Some(language), cx);
13053 });
13054
13055 let toggle_comments = &ToggleComments {
13056 advance_downwards: true,
13057 ignore_indent: false,
13058 };
13059
13060 // Single cursor on one line -> advance
13061 // Cursor moves horizontally 3 characters as well on non-blank line
13062 cx.set_state(indoc!(
13063 "fn a() {
13064 ˇdog();
13065 cat();
13066 }"
13067 ));
13068 cx.update_editor(|editor, window, cx| {
13069 editor.toggle_comments(toggle_comments, window, cx);
13070 });
13071 cx.assert_editor_state(indoc!(
13072 "fn a() {
13073 // dog();
13074 catˇ();
13075 }"
13076 ));
13077
13078 // Single selection on one line -> don't advance
13079 cx.set_state(indoc!(
13080 "fn a() {
13081 «dog()ˇ»;
13082 cat();
13083 }"
13084 ));
13085 cx.update_editor(|editor, window, cx| {
13086 editor.toggle_comments(toggle_comments, window, cx);
13087 });
13088 cx.assert_editor_state(indoc!(
13089 "fn a() {
13090 // «dog()ˇ»;
13091 cat();
13092 }"
13093 ));
13094
13095 // Multiple cursors on one line -> advance
13096 cx.set_state(indoc!(
13097 "fn a() {
13098 ˇdˇog();
13099 cat();
13100 }"
13101 ));
13102 cx.update_editor(|editor, window, cx| {
13103 editor.toggle_comments(toggle_comments, window, cx);
13104 });
13105 cx.assert_editor_state(indoc!(
13106 "fn a() {
13107 // dog();
13108 catˇ(ˇ);
13109 }"
13110 ));
13111
13112 // Multiple cursors on one line, with selection -> don't advance
13113 cx.set_state(indoc!(
13114 "fn a() {
13115 ˇdˇog«()ˇ»;
13116 cat();
13117 }"
13118 ));
13119 cx.update_editor(|editor, window, cx| {
13120 editor.toggle_comments(toggle_comments, window, cx);
13121 });
13122 cx.assert_editor_state(indoc!(
13123 "fn a() {
13124 // ˇdˇog«()ˇ»;
13125 cat();
13126 }"
13127 ));
13128
13129 // Single cursor on one line -> advance
13130 // Cursor moves to column 0 on blank line
13131 cx.set_state(indoc!(
13132 "fn a() {
13133 ˇdog();
13134
13135 cat();
13136 }"
13137 ));
13138 cx.update_editor(|editor, window, cx| {
13139 editor.toggle_comments(toggle_comments, window, cx);
13140 });
13141 cx.assert_editor_state(indoc!(
13142 "fn a() {
13143 // dog();
13144 ˇ
13145 cat();
13146 }"
13147 ));
13148
13149 // Single cursor on one line -> advance
13150 // Cursor starts and ends at column 0
13151 cx.set_state(indoc!(
13152 "fn a() {
13153 ˇ dog();
13154 cat();
13155 }"
13156 ));
13157 cx.update_editor(|editor, window, cx| {
13158 editor.toggle_comments(toggle_comments, window, cx);
13159 });
13160 cx.assert_editor_state(indoc!(
13161 "fn a() {
13162 // dog();
13163 ˇ cat();
13164 }"
13165 ));
13166}
13167
13168#[gpui::test]
13169async fn test_toggle_block_comment(cx: &mut TestAppContext) {
13170 init_test(cx, |_| {});
13171
13172 let mut cx = EditorTestContext::new(cx).await;
13173
13174 let html_language = Arc::new(
13175 Language::new(
13176 LanguageConfig {
13177 name: "HTML".into(),
13178 block_comment: Some(("<!-- ".into(), " -->".into())),
13179 ..Default::default()
13180 },
13181 Some(tree_sitter_html::LANGUAGE.into()),
13182 )
13183 .with_injection_query(
13184 r#"
13185 (script_element
13186 (raw_text) @injection.content
13187 (#set! injection.language "javascript"))
13188 "#,
13189 )
13190 .unwrap(),
13191 );
13192
13193 let javascript_language = Arc::new(Language::new(
13194 LanguageConfig {
13195 name: "JavaScript".into(),
13196 line_comments: vec!["// ".into()],
13197 ..Default::default()
13198 },
13199 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
13200 ));
13201
13202 cx.language_registry().add(html_language.clone());
13203 cx.language_registry().add(javascript_language.clone());
13204 cx.update_buffer(|buffer, cx| {
13205 buffer.set_language(Some(html_language), cx);
13206 });
13207
13208 // Toggle comments for empty selections
13209 cx.set_state(
13210 &r#"
13211 <p>A</p>ˇ
13212 <p>B</p>ˇ
13213 <p>C</p>ˇ
13214 "#
13215 .unindent(),
13216 );
13217 cx.update_editor(|editor, window, cx| {
13218 editor.toggle_comments(&ToggleComments::default(), window, cx)
13219 });
13220 cx.assert_editor_state(
13221 &r#"
13222 <!-- <p>A</p>ˇ -->
13223 <!-- <p>B</p>ˇ -->
13224 <!-- <p>C</p>ˇ -->
13225 "#
13226 .unindent(),
13227 );
13228 cx.update_editor(|editor, window, cx| {
13229 editor.toggle_comments(&ToggleComments::default(), window, cx)
13230 });
13231 cx.assert_editor_state(
13232 &r#"
13233 <p>A</p>ˇ
13234 <p>B</p>ˇ
13235 <p>C</p>ˇ
13236 "#
13237 .unindent(),
13238 );
13239
13240 // Toggle comments for mixture of empty and non-empty selections, where
13241 // multiple selections occupy a given line.
13242 cx.set_state(
13243 &r#"
13244 <p>A«</p>
13245 <p>ˇ»B</p>ˇ
13246 <p>C«</p>
13247 <p>ˇ»D</p>ˇ
13248 "#
13249 .unindent(),
13250 );
13251
13252 cx.update_editor(|editor, window, cx| {
13253 editor.toggle_comments(&ToggleComments::default(), window, cx)
13254 });
13255 cx.assert_editor_state(
13256 &r#"
13257 <!-- <p>A«</p>
13258 <p>ˇ»B</p>ˇ -->
13259 <!-- <p>C«</p>
13260 <p>ˇ»D</p>ˇ -->
13261 "#
13262 .unindent(),
13263 );
13264 cx.update_editor(|editor, window, cx| {
13265 editor.toggle_comments(&ToggleComments::default(), window, cx)
13266 });
13267 cx.assert_editor_state(
13268 &r#"
13269 <p>A«</p>
13270 <p>ˇ»B</p>ˇ
13271 <p>C«</p>
13272 <p>ˇ»D</p>ˇ
13273 "#
13274 .unindent(),
13275 );
13276
13277 // Toggle comments when different languages are active for different
13278 // selections.
13279 cx.set_state(
13280 &r#"
13281 ˇ<script>
13282 ˇvar x = new Y();
13283 ˇ</script>
13284 "#
13285 .unindent(),
13286 );
13287 cx.executor().run_until_parked();
13288 cx.update_editor(|editor, window, cx| {
13289 editor.toggle_comments(&ToggleComments::default(), window, cx)
13290 });
13291 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
13292 // Uncommenting and commenting from this position brings in even more wrong artifacts.
13293 cx.assert_editor_state(
13294 &r#"
13295 <!-- ˇ<script> -->
13296 // ˇvar x = new Y();
13297 <!-- ˇ</script> -->
13298 "#
13299 .unindent(),
13300 );
13301}
13302
13303#[gpui::test]
13304fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
13305 init_test(cx, |_| {});
13306
13307 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
13308 let multibuffer = cx.new(|cx| {
13309 let mut multibuffer = MultiBuffer::new(ReadWrite);
13310 multibuffer.push_excerpts(
13311 buffer.clone(),
13312 [
13313 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
13314 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
13315 ],
13316 cx,
13317 );
13318 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
13319 multibuffer
13320 });
13321
13322 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
13323 editor.update_in(cx, |editor, window, cx| {
13324 assert_eq!(editor.text(cx), "aaaa\nbbbb");
13325 editor.change_selections(None, window, cx, |s| {
13326 s.select_ranges([
13327 Point::new(0, 0)..Point::new(0, 0),
13328 Point::new(1, 0)..Point::new(1, 0),
13329 ])
13330 });
13331
13332 editor.handle_input("X", window, cx);
13333 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
13334 assert_eq!(
13335 editor.selections.ranges(cx),
13336 [
13337 Point::new(0, 1)..Point::new(0, 1),
13338 Point::new(1, 1)..Point::new(1, 1),
13339 ]
13340 );
13341
13342 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
13343 editor.change_selections(None, window, cx, |s| {
13344 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
13345 });
13346 editor.backspace(&Default::default(), window, cx);
13347 assert_eq!(editor.text(cx), "Xa\nbbb");
13348 assert_eq!(
13349 editor.selections.ranges(cx),
13350 [Point::new(1, 0)..Point::new(1, 0)]
13351 );
13352
13353 editor.change_selections(None, window, cx, |s| {
13354 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
13355 });
13356 editor.backspace(&Default::default(), window, cx);
13357 assert_eq!(editor.text(cx), "X\nbb");
13358 assert_eq!(
13359 editor.selections.ranges(cx),
13360 [Point::new(0, 1)..Point::new(0, 1)]
13361 );
13362 });
13363}
13364
13365#[gpui::test]
13366fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
13367 init_test(cx, |_| {});
13368
13369 let markers = vec![('[', ']').into(), ('(', ')').into()];
13370 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
13371 indoc! {"
13372 [aaaa
13373 (bbbb]
13374 cccc)",
13375 },
13376 markers.clone(),
13377 );
13378 let excerpt_ranges = markers.into_iter().map(|marker| {
13379 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
13380 ExcerptRange::new(context.clone())
13381 });
13382 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
13383 let multibuffer = cx.new(|cx| {
13384 let mut multibuffer = MultiBuffer::new(ReadWrite);
13385 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
13386 multibuffer
13387 });
13388
13389 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
13390 editor.update_in(cx, |editor, window, cx| {
13391 let (expected_text, selection_ranges) = marked_text_ranges(
13392 indoc! {"
13393 aaaa
13394 bˇbbb
13395 bˇbbˇb
13396 cccc"
13397 },
13398 true,
13399 );
13400 assert_eq!(editor.text(cx), expected_text);
13401 editor.change_selections(None, window, cx, |s| s.select_ranges(selection_ranges));
13402
13403 editor.handle_input("X", window, cx);
13404
13405 let (expected_text, expected_selections) = marked_text_ranges(
13406 indoc! {"
13407 aaaa
13408 bXˇbbXb
13409 bXˇbbXˇb
13410 cccc"
13411 },
13412 false,
13413 );
13414 assert_eq!(editor.text(cx), expected_text);
13415 assert_eq!(editor.selections.ranges(cx), expected_selections);
13416
13417 editor.newline(&Newline, window, cx);
13418 let (expected_text, expected_selections) = marked_text_ranges(
13419 indoc! {"
13420 aaaa
13421 bX
13422 ˇbbX
13423 b
13424 bX
13425 ˇbbX
13426 ˇb
13427 cccc"
13428 },
13429 false,
13430 );
13431 assert_eq!(editor.text(cx), expected_text);
13432 assert_eq!(editor.selections.ranges(cx), expected_selections);
13433 });
13434}
13435
13436#[gpui::test]
13437fn test_refresh_selections(cx: &mut TestAppContext) {
13438 init_test(cx, |_| {});
13439
13440 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
13441 let mut excerpt1_id = None;
13442 let multibuffer = cx.new(|cx| {
13443 let mut multibuffer = MultiBuffer::new(ReadWrite);
13444 excerpt1_id = multibuffer
13445 .push_excerpts(
13446 buffer.clone(),
13447 [
13448 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
13449 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
13450 ],
13451 cx,
13452 )
13453 .into_iter()
13454 .next();
13455 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
13456 multibuffer
13457 });
13458
13459 let editor = cx.add_window(|window, cx| {
13460 let mut editor = build_editor(multibuffer.clone(), window, cx);
13461 let snapshot = editor.snapshot(window, cx);
13462 editor.change_selections(None, window, cx, |s| {
13463 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
13464 });
13465 editor.begin_selection(
13466 Point::new(2, 1).to_display_point(&snapshot),
13467 true,
13468 1,
13469 window,
13470 cx,
13471 );
13472 assert_eq!(
13473 editor.selections.ranges(cx),
13474 [
13475 Point::new(1, 3)..Point::new(1, 3),
13476 Point::new(2, 1)..Point::new(2, 1),
13477 ]
13478 );
13479 editor
13480 });
13481
13482 // Refreshing selections is a no-op when excerpts haven't changed.
13483 _ = editor.update(cx, |editor, window, cx| {
13484 editor.change_selections(None, window, cx, |s| s.refresh());
13485 assert_eq!(
13486 editor.selections.ranges(cx),
13487 [
13488 Point::new(1, 3)..Point::new(1, 3),
13489 Point::new(2, 1)..Point::new(2, 1),
13490 ]
13491 );
13492 });
13493
13494 multibuffer.update(cx, |multibuffer, cx| {
13495 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
13496 });
13497 _ = editor.update(cx, |editor, window, cx| {
13498 // Removing an excerpt causes the first selection to become degenerate.
13499 assert_eq!(
13500 editor.selections.ranges(cx),
13501 [
13502 Point::new(0, 0)..Point::new(0, 0),
13503 Point::new(0, 1)..Point::new(0, 1)
13504 ]
13505 );
13506
13507 // Refreshing selections will relocate the first selection to the original buffer
13508 // location.
13509 editor.change_selections(None, window, cx, |s| s.refresh());
13510 assert_eq!(
13511 editor.selections.ranges(cx),
13512 [
13513 Point::new(0, 1)..Point::new(0, 1),
13514 Point::new(0, 3)..Point::new(0, 3)
13515 ]
13516 );
13517 assert!(editor.selections.pending_anchor().is_some());
13518 });
13519}
13520
13521#[gpui::test]
13522fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
13523 init_test(cx, |_| {});
13524
13525 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
13526 let mut excerpt1_id = None;
13527 let multibuffer = cx.new(|cx| {
13528 let mut multibuffer = MultiBuffer::new(ReadWrite);
13529 excerpt1_id = multibuffer
13530 .push_excerpts(
13531 buffer.clone(),
13532 [
13533 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
13534 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
13535 ],
13536 cx,
13537 )
13538 .into_iter()
13539 .next();
13540 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
13541 multibuffer
13542 });
13543
13544 let editor = cx.add_window(|window, cx| {
13545 let mut editor = build_editor(multibuffer.clone(), window, cx);
13546 let snapshot = editor.snapshot(window, cx);
13547 editor.begin_selection(
13548 Point::new(1, 3).to_display_point(&snapshot),
13549 false,
13550 1,
13551 window,
13552 cx,
13553 );
13554 assert_eq!(
13555 editor.selections.ranges(cx),
13556 [Point::new(1, 3)..Point::new(1, 3)]
13557 );
13558 editor
13559 });
13560
13561 multibuffer.update(cx, |multibuffer, cx| {
13562 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
13563 });
13564 _ = editor.update(cx, |editor, window, cx| {
13565 assert_eq!(
13566 editor.selections.ranges(cx),
13567 [Point::new(0, 0)..Point::new(0, 0)]
13568 );
13569
13570 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
13571 editor.change_selections(None, window, cx, |s| s.refresh());
13572 assert_eq!(
13573 editor.selections.ranges(cx),
13574 [Point::new(0, 3)..Point::new(0, 3)]
13575 );
13576 assert!(editor.selections.pending_anchor().is_some());
13577 });
13578}
13579
13580#[gpui::test]
13581async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
13582 init_test(cx, |_| {});
13583
13584 let language = Arc::new(
13585 Language::new(
13586 LanguageConfig {
13587 brackets: BracketPairConfig {
13588 pairs: vec![
13589 BracketPair {
13590 start: "{".to_string(),
13591 end: "}".to_string(),
13592 close: true,
13593 surround: true,
13594 newline: true,
13595 },
13596 BracketPair {
13597 start: "/* ".to_string(),
13598 end: " */".to_string(),
13599 close: true,
13600 surround: true,
13601 newline: true,
13602 },
13603 ],
13604 ..Default::default()
13605 },
13606 ..Default::default()
13607 },
13608 Some(tree_sitter_rust::LANGUAGE.into()),
13609 )
13610 .with_indents_query("")
13611 .unwrap(),
13612 );
13613
13614 let text = concat!(
13615 "{ }\n", //
13616 " x\n", //
13617 " /* */\n", //
13618 "x\n", //
13619 "{{} }\n", //
13620 );
13621
13622 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
13623 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
13624 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
13625 editor
13626 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
13627 .await;
13628
13629 editor.update_in(cx, |editor, window, cx| {
13630 editor.change_selections(None, window, cx, |s| {
13631 s.select_display_ranges([
13632 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
13633 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
13634 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
13635 ])
13636 });
13637 editor.newline(&Newline, window, cx);
13638
13639 assert_eq!(
13640 editor.buffer().read(cx).read(cx).text(),
13641 concat!(
13642 "{ \n", // Suppress rustfmt
13643 "\n", //
13644 "}\n", //
13645 " x\n", //
13646 " /* \n", //
13647 " \n", //
13648 " */\n", //
13649 "x\n", //
13650 "{{} \n", //
13651 "}\n", //
13652 )
13653 );
13654 });
13655}
13656
13657#[gpui::test]
13658fn test_highlighted_ranges(cx: &mut TestAppContext) {
13659 init_test(cx, |_| {});
13660
13661 let editor = cx.add_window(|window, cx| {
13662 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
13663 build_editor(buffer.clone(), window, cx)
13664 });
13665
13666 _ = editor.update(cx, |editor, window, cx| {
13667 struct Type1;
13668 struct Type2;
13669
13670 let buffer = editor.buffer.read(cx).snapshot(cx);
13671
13672 let anchor_range =
13673 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
13674
13675 editor.highlight_background::<Type1>(
13676 &[
13677 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
13678 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
13679 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
13680 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
13681 ],
13682 |_| Hsla::red(),
13683 cx,
13684 );
13685 editor.highlight_background::<Type2>(
13686 &[
13687 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
13688 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
13689 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
13690 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
13691 ],
13692 |_| Hsla::green(),
13693 cx,
13694 );
13695
13696 let snapshot = editor.snapshot(window, cx);
13697 let mut highlighted_ranges = editor.background_highlights_in_range(
13698 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
13699 &snapshot,
13700 cx.theme(),
13701 );
13702 // Enforce a consistent ordering based on color without relying on the ordering of the
13703 // highlight's `TypeId` which is non-executor.
13704 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
13705 assert_eq!(
13706 highlighted_ranges,
13707 &[
13708 (
13709 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
13710 Hsla::red(),
13711 ),
13712 (
13713 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
13714 Hsla::red(),
13715 ),
13716 (
13717 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
13718 Hsla::green(),
13719 ),
13720 (
13721 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
13722 Hsla::green(),
13723 ),
13724 ]
13725 );
13726 assert_eq!(
13727 editor.background_highlights_in_range(
13728 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
13729 &snapshot,
13730 cx.theme(),
13731 ),
13732 &[(
13733 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
13734 Hsla::red(),
13735 )]
13736 );
13737 });
13738}
13739
13740#[gpui::test]
13741async fn test_following(cx: &mut TestAppContext) {
13742 init_test(cx, |_| {});
13743
13744 let fs = FakeFs::new(cx.executor());
13745 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
13746
13747 let buffer = project.update(cx, |project, cx| {
13748 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
13749 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
13750 });
13751 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
13752 let follower = cx.update(|cx| {
13753 cx.open_window(
13754 WindowOptions {
13755 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
13756 gpui::Point::new(px(0.), px(0.)),
13757 gpui::Point::new(px(10.), px(80.)),
13758 ))),
13759 ..Default::default()
13760 },
13761 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
13762 )
13763 .unwrap()
13764 });
13765
13766 let is_still_following = Rc::new(RefCell::new(true));
13767 let follower_edit_event_count = Rc::new(RefCell::new(0));
13768 let pending_update = Rc::new(RefCell::new(None));
13769 let leader_entity = leader.root(cx).unwrap();
13770 let follower_entity = follower.root(cx).unwrap();
13771 _ = follower.update(cx, {
13772 let update = pending_update.clone();
13773 let is_still_following = is_still_following.clone();
13774 let follower_edit_event_count = follower_edit_event_count.clone();
13775 |_, window, cx| {
13776 cx.subscribe_in(
13777 &leader_entity,
13778 window,
13779 move |_, leader, event, window, cx| {
13780 leader.read(cx).add_event_to_update_proto(
13781 event,
13782 &mut update.borrow_mut(),
13783 window,
13784 cx,
13785 );
13786 },
13787 )
13788 .detach();
13789
13790 cx.subscribe_in(
13791 &follower_entity,
13792 window,
13793 move |_, _, event: &EditorEvent, _window, _cx| {
13794 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
13795 *is_still_following.borrow_mut() = false;
13796 }
13797
13798 if let EditorEvent::BufferEdited = event {
13799 *follower_edit_event_count.borrow_mut() += 1;
13800 }
13801 },
13802 )
13803 .detach();
13804 }
13805 });
13806
13807 // Update the selections only
13808 _ = leader.update(cx, |leader, window, cx| {
13809 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
13810 });
13811 follower
13812 .update(cx, |follower, window, cx| {
13813 follower.apply_update_proto(
13814 &project,
13815 pending_update.borrow_mut().take().unwrap(),
13816 window,
13817 cx,
13818 )
13819 })
13820 .unwrap()
13821 .await
13822 .unwrap();
13823 _ = follower.update(cx, |follower, _, cx| {
13824 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
13825 });
13826 assert!(*is_still_following.borrow());
13827 assert_eq!(*follower_edit_event_count.borrow(), 0);
13828
13829 // Update the scroll position only
13830 _ = leader.update(cx, |leader, window, cx| {
13831 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
13832 });
13833 follower
13834 .update(cx, |follower, window, cx| {
13835 follower.apply_update_proto(
13836 &project,
13837 pending_update.borrow_mut().take().unwrap(),
13838 window,
13839 cx,
13840 )
13841 })
13842 .unwrap()
13843 .await
13844 .unwrap();
13845 assert_eq!(
13846 follower
13847 .update(cx, |follower, _, cx| follower.scroll_position(cx))
13848 .unwrap(),
13849 gpui::Point::new(1.5, 3.5)
13850 );
13851 assert!(*is_still_following.borrow());
13852 assert_eq!(*follower_edit_event_count.borrow(), 0);
13853
13854 // Update the selections and scroll position. The follower's scroll position is updated
13855 // via autoscroll, not via the leader's exact scroll position.
13856 _ = leader.update(cx, |leader, window, cx| {
13857 leader.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
13858 leader.request_autoscroll(Autoscroll::newest(), cx);
13859 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
13860 });
13861 follower
13862 .update(cx, |follower, window, cx| {
13863 follower.apply_update_proto(
13864 &project,
13865 pending_update.borrow_mut().take().unwrap(),
13866 window,
13867 cx,
13868 )
13869 })
13870 .unwrap()
13871 .await
13872 .unwrap();
13873 _ = follower.update(cx, |follower, _, cx| {
13874 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
13875 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
13876 });
13877 assert!(*is_still_following.borrow());
13878
13879 // Creating a pending selection that precedes another selection
13880 _ = leader.update(cx, |leader, window, cx| {
13881 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
13882 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
13883 });
13884 follower
13885 .update(cx, |follower, window, cx| {
13886 follower.apply_update_proto(
13887 &project,
13888 pending_update.borrow_mut().take().unwrap(),
13889 window,
13890 cx,
13891 )
13892 })
13893 .unwrap()
13894 .await
13895 .unwrap();
13896 _ = follower.update(cx, |follower, _, cx| {
13897 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
13898 });
13899 assert!(*is_still_following.borrow());
13900
13901 // Extend the pending selection so that it surrounds another selection
13902 _ = leader.update(cx, |leader, window, cx| {
13903 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
13904 });
13905 follower
13906 .update(cx, |follower, window, cx| {
13907 follower.apply_update_proto(
13908 &project,
13909 pending_update.borrow_mut().take().unwrap(),
13910 window,
13911 cx,
13912 )
13913 })
13914 .unwrap()
13915 .await
13916 .unwrap();
13917 _ = follower.update(cx, |follower, _, cx| {
13918 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
13919 });
13920
13921 // Scrolling locally breaks the follow
13922 _ = follower.update(cx, |follower, window, cx| {
13923 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
13924 follower.set_scroll_anchor(
13925 ScrollAnchor {
13926 anchor: top_anchor,
13927 offset: gpui::Point::new(0.0, 0.5),
13928 },
13929 window,
13930 cx,
13931 );
13932 });
13933 assert!(!(*is_still_following.borrow()));
13934}
13935
13936#[gpui::test]
13937async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
13938 init_test(cx, |_| {});
13939
13940 let fs = FakeFs::new(cx.executor());
13941 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
13942 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13943 let pane = workspace
13944 .update(cx, |workspace, _, _| workspace.active_pane().clone())
13945 .unwrap();
13946
13947 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
13948
13949 let leader = pane.update_in(cx, |_, window, cx| {
13950 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
13951 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
13952 });
13953
13954 // Start following the editor when it has no excerpts.
13955 let mut state_message =
13956 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
13957 let workspace_entity = workspace.root(cx).unwrap();
13958 let follower_1 = cx
13959 .update_window(*workspace.deref(), |_, window, cx| {
13960 Editor::from_state_proto(
13961 workspace_entity,
13962 ViewId {
13963 creator: CollaboratorId::PeerId(PeerId::default()),
13964 id: 0,
13965 },
13966 &mut state_message,
13967 window,
13968 cx,
13969 )
13970 })
13971 .unwrap()
13972 .unwrap()
13973 .await
13974 .unwrap();
13975
13976 let update_message = Rc::new(RefCell::new(None));
13977 follower_1.update_in(cx, {
13978 let update = update_message.clone();
13979 |_, window, cx| {
13980 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
13981 leader.read(cx).add_event_to_update_proto(
13982 event,
13983 &mut update.borrow_mut(),
13984 window,
13985 cx,
13986 );
13987 })
13988 .detach();
13989 }
13990 });
13991
13992 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
13993 (
13994 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
13995 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
13996 )
13997 });
13998
13999 // Insert some excerpts.
14000 leader.update(cx, |leader, cx| {
14001 leader.buffer.update(cx, |multibuffer, cx| {
14002 multibuffer.set_excerpts_for_path(
14003 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
14004 buffer_1.clone(),
14005 vec![
14006 Point::row_range(0..3),
14007 Point::row_range(1..6),
14008 Point::row_range(12..15),
14009 ],
14010 0,
14011 cx,
14012 );
14013 multibuffer.set_excerpts_for_path(
14014 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
14015 buffer_2.clone(),
14016 vec![Point::row_range(0..6), Point::row_range(8..12)],
14017 0,
14018 cx,
14019 );
14020 });
14021 });
14022
14023 // Apply the update of adding the excerpts.
14024 follower_1
14025 .update_in(cx, |follower, window, cx| {
14026 follower.apply_update_proto(
14027 &project,
14028 update_message.borrow().clone().unwrap(),
14029 window,
14030 cx,
14031 )
14032 })
14033 .await
14034 .unwrap();
14035 assert_eq!(
14036 follower_1.update(cx, |editor, cx| editor.text(cx)),
14037 leader.update(cx, |editor, cx| editor.text(cx))
14038 );
14039 update_message.borrow_mut().take();
14040
14041 // Start following separately after it already has excerpts.
14042 let mut state_message =
14043 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
14044 let workspace_entity = workspace.root(cx).unwrap();
14045 let follower_2 = cx
14046 .update_window(*workspace.deref(), |_, window, cx| {
14047 Editor::from_state_proto(
14048 workspace_entity,
14049 ViewId {
14050 creator: CollaboratorId::PeerId(PeerId::default()),
14051 id: 0,
14052 },
14053 &mut state_message,
14054 window,
14055 cx,
14056 )
14057 })
14058 .unwrap()
14059 .unwrap()
14060 .await
14061 .unwrap();
14062 assert_eq!(
14063 follower_2.update(cx, |editor, cx| editor.text(cx)),
14064 leader.update(cx, |editor, cx| editor.text(cx))
14065 );
14066
14067 // Remove some excerpts.
14068 leader.update(cx, |leader, cx| {
14069 leader.buffer.update(cx, |multibuffer, cx| {
14070 let excerpt_ids = multibuffer.excerpt_ids();
14071 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
14072 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
14073 });
14074 });
14075
14076 // Apply the update of removing the excerpts.
14077 follower_1
14078 .update_in(cx, |follower, window, cx| {
14079 follower.apply_update_proto(
14080 &project,
14081 update_message.borrow().clone().unwrap(),
14082 window,
14083 cx,
14084 )
14085 })
14086 .await
14087 .unwrap();
14088 follower_2
14089 .update_in(cx, |follower, window, cx| {
14090 follower.apply_update_proto(
14091 &project,
14092 update_message.borrow().clone().unwrap(),
14093 window,
14094 cx,
14095 )
14096 })
14097 .await
14098 .unwrap();
14099 update_message.borrow_mut().take();
14100 assert_eq!(
14101 follower_1.update(cx, |editor, cx| editor.text(cx)),
14102 leader.update(cx, |editor, cx| editor.text(cx))
14103 );
14104}
14105
14106#[gpui::test]
14107async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14108 init_test(cx, |_| {});
14109
14110 let mut cx = EditorTestContext::new(cx).await;
14111 let lsp_store =
14112 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
14113
14114 cx.set_state(indoc! {"
14115 ˇfn func(abc def: i32) -> u32 {
14116 }
14117 "});
14118
14119 cx.update(|_, cx| {
14120 lsp_store.update(cx, |lsp_store, cx| {
14121 lsp_store
14122 .update_diagnostics(
14123 LanguageServerId(0),
14124 lsp::PublishDiagnosticsParams {
14125 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
14126 version: None,
14127 diagnostics: vec![
14128 lsp::Diagnostic {
14129 range: lsp::Range::new(
14130 lsp::Position::new(0, 11),
14131 lsp::Position::new(0, 12),
14132 ),
14133 severity: Some(lsp::DiagnosticSeverity::ERROR),
14134 ..Default::default()
14135 },
14136 lsp::Diagnostic {
14137 range: lsp::Range::new(
14138 lsp::Position::new(0, 12),
14139 lsp::Position::new(0, 15),
14140 ),
14141 severity: Some(lsp::DiagnosticSeverity::ERROR),
14142 ..Default::default()
14143 },
14144 lsp::Diagnostic {
14145 range: lsp::Range::new(
14146 lsp::Position::new(0, 25),
14147 lsp::Position::new(0, 28),
14148 ),
14149 severity: Some(lsp::DiagnosticSeverity::ERROR),
14150 ..Default::default()
14151 },
14152 ],
14153 },
14154 None,
14155 DiagnosticSourceKind::Pushed,
14156 &[],
14157 cx,
14158 )
14159 .unwrap()
14160 });
14161 });
14162
14163 executor.run_until_parked();
14164
14165 cx.update_editor(|editor, window, cx| {
14166 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
14167 });
14168
14169 cx.assert_editor_state(indoc! {"
14170 fn func(abc def: i32) -> ˇu32 {
14171 }
14172 "});
14173
14174 cx.update_editor(|editor, window, cx| {
14175 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
14176 });
14177
14178 cx.assert_editor_state(indoc! {"
14179 fn func(abc ˇdef: i32) -> u32 {
14180 }
14181 "});
14182
14183 cx.update_editor(|editor, window, cx| {
14184 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
14185 });
14186
14187 cx.assert_editor_state(indoc! {"
14188 fn func(abcˇ def: i32) -> u32 {
14189 }
14190 "});
14191
14192 cx.update_editor(|editor, window, cx| {
14193 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
14194 });
14195
14196 cx.assert_editor_state(indoc! {"
14197 fn func(abc def: i32) -> ˇu32 {
14198 }
14199 "});
14200}
14201
14202#[gpui::test]
14203async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14204 init_test(cx, |_| {});
14205
14206 let mut cx = EditorTestContext::new(cx).await;
14207
14208 let diff_base = r#"
14209 use some::mod;
14210
14211 const A: u32 = 42;
14212
14213 fn main() {
14214 println!("hello");
14215
14216 println!("world");
14217 }
14218 "#
14219 .unindent();
14220
14221 // Edits are modified, removed, modified, added
14222 cx.set_state(
14223 &r#"
14224 use some::modified;
14225
14226 ˇ
14227 fn main() {
14228 println!("hello there");
14229
14230 println!("around the");
14231 println!("world");
14232 }
14233 "#
14234 .unindent(),
14235 );
14236
14237 cx.set_head_text(&diff_base);
14238 executor.run_until_parked();
14239
14240 cx.update_editor(|editor, window, cx| {
14241 //Wrap around the bottom of the buffer
14242 for _ in 0..3 {
14243 editor.go_to_next_hunk(&GoToHunk, window, cx);
14244 }
14245 });
14246
14247 cx.assert_editor_state(
14248 &r#"
14249 ˇuse some::modified;
14250
14251
14252 fn main() {
14253 println!("hello there");
14254
14255 println!("around the");
14256 println!("world");
14257 }
14258 "#
14259 .unindent(),
14260 );
14261
14262 cx.update_editor(|editor, window, cx| {
14263 //Wrap around the top of the buffer
14264 for _ in 0..2 {
14265 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14266 }
14267 });
14268
14269 cx.assert_editor_state(
14270 &r#"
14271 use some::modified;
14272
14273
14274 fn main() {
14275 ˇ println!("hello there");
14276
14277 println!("around the");
14278 println!("world");
14279 }
14280 "#
14281 .unindent(),
14282 );
14283
14284 cx.update_editor(|editor, window, cx| {
14285 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14286 });
14287
14288 cx.assert_editor_state(
14289 &r#"
14290 use some::modified;
14291
14292 ˇ
14293 fn main() {
14294 println!("hello there");
14295
14296 println!("around the");
14297 println!("world");
14298 }
14299 "#
14300 .unindent(),
14301 );
14302
14303 cx.update_editor(|editor, window, cx| {
14304 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14305 });
14306
14307 cx.assert_editor_state(
14308 &r#"
14309 ˇuse some::modified;
14310
14311
14312 fn main() {
14313 println!("hello there");
14314
14315 println!("around the");
14316 println!("world");
14317 }
14318 "#
14319 .unindent(),
14320 );
14321
14322 cx.update_editor(|editor, window, cx| {
14323 for _ in 0..2 {
14324 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14325 }
14326 });
14327
14328 cx.assert_editor_state(
14329 &r#"
14330 use some::modified;
14331
14332
14333 fn main() {
14334 ˇ println!("hello there");
14335
14336 println!("around the");
14337 println!("world");
14338 }
14339 "#
14340 .unindent(),
14341 );
14342
14343 cx.update_editor(|editor, window, cx| {
14344 editor.fold(&Fold, window, cx);
14345 });
14346
14347 cx.update_editor(|editor, window, cx| {
14348 editor.go_to_next_hunk(&GoToHunk, window, cx);
14349 });
14350
14351 cx.assert_editor_state(
14352 &r#"
14353 ˇuse some::modified;
14354
14355
14356 fn main() {
14357 println!("hello there");
14358
14359 println!("around the");
14360 println!("world");
14361 }
14362 "#
14363 .unindent(),
14364 );
14365}
14366
14367#[test]
14368fn test_split_words() {
14369 fn split(text: &str) -> Vec<&str> {
14370 split_words(text).collect()
14371 }
14372
14373 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
14374 assert_eq!(split("hello_world"), &["hello_", "world"]);
14375 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
14376 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
14377 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
14378 assert_eq!(split("helloworld"), &["helloworld"]);
14379
14380 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
14381}
14382
14383#[gpui::test]
14384async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
14385 init_test(cx, |_| {});
14386
14387 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
14388 let mut assert = |before, after| {
14389 let _state_context = cx.set_state(before);
14390 cx.run_until_parked();
14391 cx.update_editor(|editor, window, cx| {
14392 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
14393 });
14394 cx.run_until_parked();
14395 cx.assert_editor_state(after);
14396 };
14397
14398 // Outside bracket jumps to outside of matching bracket
14399 assert("console.logˇ(var);", "console.log(var)ˇ;");
14400 assert("console.log(var)ˇ;", "console.logˇ(var);");
14401
14402 // Inside bracket jumps to inside of matching bracket
14403 assert("console.log(ˇvar);", "console.log(varˇ);");
14404 assert("console.log(varˇ);", "console.log(ˇvar);");
14405
14406 // When outside a bracket and inside, favor jumping to the inside bracket
14407 assert(
14408 "console.log('foo', [1, 2, 3]ˇ);",
14409 "console.log(ˇ'foo', [1, 2, 3]);",
14410 );
14411 assert(
14412 "console.log(ˇ'foo', [1, 2, 3]);",
14413 "console.log('foo', [1, 2, 3]ˇ);",
14414 );
14415
14416 // Bias forward if two options are equally likely
14417 assert(
14418 "let result = curried_fun()ˇ();",
14419 "let result = curried_fun()()ˇ;",
14420 );
14421
14422 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
14423 assert(
14424 indoc! {"
14425 function test() {
14426 console.log('test')ˇ
14427 }"},
14428 indoc! {"
14429 function test() {
14430 console.logˇ('test')
14431 }"},
14432 );
14433}
14434
14435#[gpui::test]
14436async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
14437 init_test(cx, |_| {});
14438
14439 let fs = FakeFs::new(cx.executor());
14440 fs.insert_tree(
14441 path!("/a"),
14442 json!({
14443 "main.rs": "fn main() { let a = 5; }",
14444 "other.rs": "// Test file",
14445 }),
14446 )
14447 .await;
14448 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14449
14450 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14451 language_registry.add(Arc::new(Language::new(
14452 LanguageConfig {
14453 name: "Rust".into(),
14454 matcher: LanguageMatcher {
14455 path_suffixes: vec!["rs".to_string()],
14456 ..Default::default()
14457 },
14458 brackets: BracketPairConfig {
14459 pairs: vec![BracketPair {
14460 start: "{".to_string(),
14461 end: "}".to_string(),
14462 close: true,
14463 surround: true,
14464 newline: true,
14465 }],
14466 disabled_scopes_by_bracket_ix: Vec::new(),
14467 },
14468 ..Default::default()
14469 },
14470 Some(tree_sitter_rust::LANGUAGE.into()),
14471 )));
14472 let mut fake_servers = language_registry.register_fake_lsp(
14473 "Rust",
14474 FakeLspAdapter {
14475 capabilities: lsp::ServerCapabilities {
14476 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
14477 first_trigger_character: "{".to_string(),
14478 more_trigger_character: None,
14479 }),
14480 ..Default::default()
14481 },
14482 ..Default::default()
14483 },
14484 );
14485
14486 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14487
14488 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14489
14490 let worktree_id = workspace
14491 .update(cx, |workspace, _, cx| {
14492 workspace.project().update(cx, |project, cx| {
14493 project.worktrees(cx).next().unwrap().read(cx).id()
14494 })
14495 })
14496 .unwrap();
14497
14498 let buffer = project
14499 .update(cx, |project, cx| {
14500 project.open_local_buffer(path!("/a/main.rs"), cx)
14501 })
14502 .await
14503 .unwrap();
14504 let editor_handle = workspace
14505 .update(cx, |workspace, window, cx| {
14506 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
14507 })
14508 .unwrap()
14509 .await
14510 .unwrap()
14511 .downcast::<Editor>()
14512 .unwrap();
14513
14514 cx.executor().start_waiting();
14515 let fake_server = fake_servers.next().await.unwrap();
14516
14517 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
14518 |params, _| async move {
14519 assert_eq!(
14520 params.text_document_position.text_document.uri,
14521 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
14522 );
14523 assert_eq!(
14524 params.text_document_position.position,
14525 lsp::Position::new(0, 21),
14526 );
14527
14528 Ok(Some(vec![lsp::TextEdit {
14529 new_text: "]".to_string(),
14530 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14531 }]))
14532 },
14533 );
14534
14535 editor_handle.update_in(cx, |editor, window, cx| {
14536 window.focus(&editor.focus_handle(cx));
14537 editor.change_selections(None, window, cx, |s| {
14538 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
14539 });
14540 editor.handle_input("{", window, cx);
14541 });
14542
14543 cx.executor().run_until_parked();
14544
14545 buffer.update(cx, |buffer, _| {
14546 assert_eq!(
14547 buffer.text(),
14548 "fn main() { let a = {5}; }",
14549 "No extra braces from on type formatting should appear in the buffer"
14550 )
14551 });
14552}
14553
14554#[gpui::test(iterations = 20, seeds(31))]
14555async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
14556 init_test(cx, |_| {});
14557
14558 let mut cx = EditorLspTestContext::new_rust(
14559 lsp::ServerCapabilities {
14560 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
14561 first_trigger_character: ".".to_string(),
14562 more_trigger_character: None,
14563 }),
14564 ..Default::default()
14565 },
14566 cx,
14567 )
14568 .await;
14569
14570 cx.update_buffer(|buffer, _| {
14571 // This causes autoindent to be async.
14572 buffer.set_sync_parse_timeout(Duration::ZERO)
14573 });
14574
14575 cx.set_state("fn c() {\n d()ˇ\n}\n");
14576 cx.simulate_keystroke("\n");
14577 cx.run_until_parked();
14578
14579 let buffer_cloned =
14580 cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap().clone());
14581 let mut request =
14582 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
14583 let buffer_cloned = buffer_cloned.clone();
14584 async move {
14585 buffer_cloned.update(&mut cx, |buffer, _| {
14586 assert_eq!(
14587 buffer.text(),
14588 "fn c() {\n d()\n .\n}\n",
14589 "OnTypeFormatting should triggered after autoindent applied"
14590 )
14591 })?;
14592
14593 Ok(Some(vec![]))
14594 }
14595 });
14596
14597 cx.simulate_keystroke(".");
14598 cx.run_until_parked();
14599
14600 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
14601 assert!(request.next().await.is_some());
14602 request.close();
14603 assert!(request.next().await.is_none());
14604}
14605
14606#[gpui::test]
14607async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
14608 init_test(cx, |_| {});
14609
14610 let fs = FakeFs::new(cx.executor());
14611 fs.insert_tree(
14612 path!("/a"),
14613 json!({
14614 "main.rs": "fn main() { let a = 5; }",
14615 "other.rs": "// Test file",
14616 }),
14617 )
14618 .await;
14619
14620 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14621
14622 let server_restarts = Arc::new(AtomicUsize::new(0));
14623 let closure_restarts = Arc::clone(&server_restarts);
14624 let language_server_name = "test language server";
14625 let language_name: LanguageName = "Rust".into();
14626
14627 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14628 language_registry.add(Arc::new(Language::new(
14629 LanguageConfig {
14630 name: language_name.clone(),
14631 matcher: LanguageMatcher {
14632 path_suffixes: vec!["rs".to_string()],
14633 ..Default::default()
14634 },
14635 ..Default::default()
14636 },
14637 Some(tree_sitter_rust::LANGUAGE.into()),
14638 )));
14639 let mut fake_servers = language_registry.register_fake_lsp(
14640 "Rust",
14641 FakeLspAdapter {
14642 name: language_server_name,
14643 initialization_options: Some(json!({
14644 "testOptionValue": true
14645 })),
14646 initializer: Some(Box::new(move |fake_server| {
14647 let task_restarts = Arc::clone(&closure_restarts);
14648 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
14649 task_restarts.fetch_add(1, atomic::Ordering::Release);
14650 futures::future::ready(Ok(()))
14651 });
14652 })),
14653 ..Default::default()
14654 },
14655 );
14656
14657 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14658 let _buffer = project
14659 .update(cx, |project, cx| {
14660 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
14661 })
14662 .await
14663 .unwrap();
14664 let _fake_server = fake_servers.next().await.unwrap();
14665 update_test_language_settings(cx, |language_settings| {
14666 language_settings.languages.insert(
14667 language_name.clone(),
14668 LanguageSettingsContent {
14669 tab_size: NonZeroU32::new(8),
14670 ..Default::default()
14671 },
14672 );
14673 });
14674 cx.executor().run_until_parked();
14675 assert_eq!(
14676 server_restarts.load(atomic::Ordering::Acquire),
14677 0,
14678 "Should not restart LSP server on an unrelated change"
14679 );
14680
14681 update_test_project_settings(cx, |project_settings| {
14682 project_settings.lsp.insert(
14683 "Some other server name".into(),
14684 LspSettings {
14685 binary: None,
14686 settings: None,
14687 initialization_options: Some(json!({
14688 "some other init value": false
14689 })),
14690 enable_lsp_tasks: false,
14691 },
14692 );
14693 });
14694 cx.executor().run_until_parked();
14695 assert_eq!(
14696 server_restarts.load(atomic::Ordering::Acquire),
14697 0,
14698 "Should not restart LSP server on an unrelated LSP settings change"
14699 );
14700
14701 update_test_project_settings(cx, |project_settings| {
14702 project_settings.lsp.insert(
14703 language_server_name.into(),
14704 LspSettings {
14705 binary: None,
14706 settings: None,
14707 initialization_options: Some(json!({
14708 "anotherInitValue": false
14709 })),
14710 enable_lsp_tasks: false,
14711 },
14712 );
14713 });
14714 cx.executor().run_until_parked();
14715 assert_eq!(
14716 server_restarts.load(atomic::Ordering::Acquire),
14717 1,
14718 "Should restart LSP server on a related LSP settings change"
14719 );
14720
14721 update_test_project_settings(cx, |project_settings| {
14722 project_settings.lsp.insert(
14723 language_server_name.into(),
14724 LspSettings {
14725 binary: None,
14726 settings: None,
14727 initialization_options: Some(json!({
14728 "anotherInitValue": false
14729 })),
14730 enable_lsp_tasks: false,
14731 },
14732 );
14733 });
14734 cx.executor().run_until_parked();
14735 assert_eq!(
14736 server_restarts.load(atomic::Ordering::Acquire),
14737 1,
14738 "Should not restart LSP server on a related LSP settings change that is the same"
14739 );
14740
14741 update_test_project_settings(cx, |project_settings| {
14742 project_settings.lsp.insert(
14743 language_server_name.into(),
14744 LspSettings {
14745 binary: None,
14746 settings: None,
14747 initialization_options: None,
14748 enable_lsp_tasks: false,
14749 },
14750 );
14751 });
14752 cx.executor().run_until_parked();
14753 assert_eq!(
14754 server_restarts.load(atomic::Ordering::Acquire),
14755 2,
14756 "Should restart LSP server on another related LSP settings change"
14757 );
14758}
14759
14760#[gpui::test]
14761async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
14762 init_test(cx, |_| {});
14763
14764 let mut cx = EditorLspTestContext::new_rust(
14765 lsp::ServerCapabilities {
14766 completion_provider: Some(lsp::CompletionOptions {
14767 trigger_characters: Some(vec![".".to_string()]),
14768 resolve_provider: Some(true),
14769 ..Default::default()
14770 }),
14771 ..Default::default()
14772 },
14773 cx,
14774 )
14775 .await;
14776
14777 cx.set_state("fn main() { let a = 2ˇ; }");
14778 cx.simulate_keystroke(".");
14779 let completion_item = lsp::CompletionItem {
14780 label: "some".into(),
14781 kind: Some(lsp::CompletionItemKind::SNIPPET),
14782 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
14783 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
14784 kind: lsp::MarkupKind::Markdown,
14785 value: "```rust\nSome(2)\n```".to_string(),
14786 })),
14787 deprecated: Some(false),
14788 sort_text: Some("fffffff2".to_string()),
14789 filter_text: Some("some".to_string()),
14790 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
14791 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14792 range: lsp::Range {
14793 start: lsp::Position {
14794 line: 0,
14795 character: 22,
14796 },
14797 end: lsp::Position {
14798 line: 0,
14799 character: 22,
14800 },
14801 },
14802 new_text: "Some(2)".to_string(),
14803 })),
14804 additional_text_edits: Some(vec![lsp::TextEdit {
14805 range: lsp::Range {
14806 start: lsp::Position {
14807 line: 0,
14808 character: 20,
14809 },
14810 end: lsp::Position {
14811 line: 0,
14812 character: 22,
14813 },
14814 },
14815 new_text: "".to_string(),
14816 }]),
14817 ..Default::default()
14818 };
14819
14820 let closure_completion_item = completion_item.clone();
14821 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14822 let task_completion_item = closure_completion_item.clone();
14823 async move {
14824 Ok(Some(lsp::CompletionResponse::Array(vec![
14825 task_completion_item,
14826 ])))
14827 }
14828 });
14829
14830 request.next().await;
14831
14832 cx.condition(|editor, _| editor.context_menu_visible())
14833 .await;
14834 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14835 editor
14836 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14837 .unwrap()
14838 });
14839 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
14840
14841 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
14842 let task_completion_item = completion_item.clone();
14843 async move { Ok(task_completion_item) }
14844 })
14845 .next()
14846 .await
14847 .unwrap();
14848 apply_additional_edits.await.unwrap();
14849 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
14850}
14851
14852#[gpui::test]
14853async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
14854 init_test(cx, |_| {});
14855
14856 let mut cx = EditorLspTestContext::new_rust(
14857 lsp::ServerCapabilities {
14858 completion_provider: Some(lsp::CompletionOptions {
14859 trigger_characters: Some(vec![".".to_string()]),
14860 resolve_provider: Some(true),
14861 ..Default::default()
14862 }),
14863 ..Default::default()
14864 },
14865 cx,
14866 )
14867 .await;
14868
14869 cx.set_state("fn main() { let a = 2ˇ; }");
14870 cx.simulate_keystroke(".");
14871
14872 let item1 = lsp::CompletionItem {
14873 label: "method id()".to_string(),
14874 filter_text: Some("id".to_string()),
14875 detail: None,
14876 documentation: None,
14877 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14878 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14879 new_text: ".id".to_string(),
14880 })),
14881 ..lsp::CompletionItem::default()
14882 };
14883
14884 let item2 = lsp::CompletionItem {
14885 label: "other".to_string(),
14886 filter_text: Some("other".to_string()),
14887 detail: None,
14888 documentation: None,
14889 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14890 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14891 new_text: ".other".to_string(),
14892 })),
14893 ..lsp::CompletionItem::default()
14894 };
14895
14896 let item1 = item1.clone();
14897 cx.set_request_handler::<lsp::request::Completion, _, _>({
14898 let item1 = item1.clone();
14899 move |_, _, _| {
14900 let item1 = item1.clone();
14901 let item2 = item2.clone();
14902 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
14903 }
14904 })
14905 .next()
14906 .await;
14907
14908 cx.condition(|editor, _| editor.context_menu_visible())
14909 .await;
14910 cx.update_editor(|editor, _, _| {
14911 let context_menu = editor.context_menu.borrow_mut();
14912 let context_menu = context_menu
14913 .as_ref()
14914 .expect("Should have the context menu deployed");
14915 match context_menu {
14916 CodeContextMenu::Completions(completions_menu) => {
14917 let completions = completions_menu.completions.borrow_mut();
14918 assert_eq!(
14919 completions
14920 .iter()
14921 .map(|completion| &completion.label.text)
14922 .collect::<Vec<_>>(),
14923 vec!["method id()", "other"]
14924 )
14925 }
14926 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
14927 }
14928 });
14929
14930 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
14931 let item1 = item1.clone();
14932 move |_, item_to_resolve, _| {
14933 let item1 = item1.clone();
14934 async move {
14935 if item1 == item_to_resolve {
14936 Ok(lsp::CompletionItem {
14937 label: "method id()".to_string(),
14938 filter_text: Some("id".to_string()),
14939 detail: Some("Now resolved!".to_string()),
14940 documentation: Some(lsp::Documentation::String("Docs".to_string())),
14941 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14942 range: lsp::Range::new(
14943 lsp::Position::new(0, 22),
14944 lsp::Position::new(0, 22),
14945 ),
14946 new_text: ".id".to_string(),
14947 })),
14948 ..lsp::CompletionItem::default()
14949 })
14950 } else {
14951 Ok(item_to_resolve)
14952 }
14953 }
14954 }
14955 })
14956 .next()
14957 .await
14958 .unwrap();
14959 cx.run_until_parked();
14960
14961 cx.update_editor(|editor, window, cx| {
14962 editor.context_menu_next(&Default::default(), window, cx);
14963 });
14964
14965 cx.update_editor(|editor, _, _| {
14966 let context_menu = editor.context_menu.borrow_mut();
14967 let context_menu = context_menu
14968 .as_ref()
14969 .expect("Should have the context menu deployed");
14970 match context_menu {
14971 CodeContextMenu::Completions(completions_menu) => {
14972 let completions = completions_menu.completions.borrow_mut();
14973 assert_eq!(
14974 completions
14975 .iter()
14976 .map(|completion| &completion.label.text)
14977 .collect::<Vec<_>>(),
14978 vec!["method id() Now resolved!", "other"],
14979 "Should update first completion label, but not second as the filter text did not match."
14980 );
14981 }
14982 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
14983 }
14984 });
14985}
14986
14987#[gpui::test]
14988async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
14989 init_test(cx, |_| {});
14990 let mut cx = EditorLspTestContext::new_rust(
14991 lsp::ServerCapabilities {
14992 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
14993 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
14994 completion_provider: Some(lsp::CompletionOptions {
14995 resolve_provider: Some(true),
14996 ..Default::default()
14997 }),
14998 ..Default::default()
14999 },
15000 cx,
15001 )
15002 .await;
15003 cx.set_state(indoc! {"
15004 struct TestStruct {
15005 field: i32
15006 }
15007
15008 fn mainˇ() {
15009 let unused_var = 42;
15010 let test_struct = TestStruct { field: 42 };
15011 }
15012 "});
15013 let symbol_range = cx.lsp_range(indoc! {"
15014 struct TestStruct {
15015 field: i32
15016 }
15017
15018 «fn main»() {
15019 let unused_var = 42;
15020 let test_struct = TestStruct { field: 42 };
15021 }
15022 "});
15023 let mut hover_requests =
15024 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
15025 Ok(Some(lsp::Hover {
15026 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
15027 kind: lsp::MarkupKind::Markdown,
15028 value: "Function documentation".to_string(),
15029 }),
15030 range: Some(symbol_range),
15031 }))
15032 });
15033
15034 // Case 1: Test that code action menu hide hover popover
15035 cx.dispatch_action(Hover);
15036 hover_requests.next().await;
15037 cx.condition(|editor, _| editor.hover_state.visible()).await;
15038 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
15039 move |_, _, _| async move {
15040 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
15041 lsp::CodeAction {
15042 title: "Remove unused variable".to_string(),
15043 kind: Some(CodeActionKind::QUICKFIX),
15044 edit: Some(lsp::WorkspaceEdit {
15045 changes: Some(
15046 [(
15047 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
15048 vec![lsp::TextEdit {
15049 range: lsp::Range::new(
15050 lsp::Position::new(5, 4),
15051 lsp::Position::new(5, 27),
15052 ),
15053 new_text: "".to_string(),
15054 }],
15055 )]
15056 .into_iter()
15057 .collect(),
15058 ),
15059 ..Default::default()
15060 }),
15061 ..Default::default()
15062 },
15063 )]))
15064 },
15065 );
15066 cx.update_editor(|editor, window, cx| {
15067 editor.toggle_code_actions(
15068 &ToggleCodeActions {
15069 deployed_from: None,
15070 quick_launch: false,
15071 },
15072 window,
15073 cx,
15074 );
15075 });
15076 code_action_requests.next().await;
15077 cx.run_until_parked();
15078 cx.condition(|editor, _| editor.context_menu_visible())
15079 .await;
15080 cx.update_editor(|editor, _, _| {
15081 assert!(
15082 !editor.hover_state.visible(),
15083 "Hover popover should be hidden when code action menu is shown"
15084 );
15085 // Hide code actions
15086 editor.context_menu.take();
15087 });
15088
15089 // Case 2: Test that code completions hide hover popover
15090 cx.dispatch_action(Hover);
15091 hover_requests.next().await;
15092 cx.condition(|editor, _| editor.hover_state.visible()).await;
15093 let counter = Arc::new(AtomicUsize::new(0));
15094 let mut completion_requests =
15095 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15096 let counter = counter.clone();
15097 async move {
15098 counter.fetch_add(1, atomic::Ordering::Release);
15099 Ok(Some(lsp::CompletionResponse::Array(vec![
15100 lsp::CompletionItem {
15101 label: "main".into(),
15102 kind: Some(lsp::CompletionItemKind::FUNCTION),
15103 detail: Some("() -> ()".to_string()),
15104 ..Default::default()
15105 },
15106 lsp::CompletionItem {
15107 label: "TestStruct".into(),
15108 kind: Some(lsp::CompletionItemKind::STRUCT),
15109 detail: Some("struct TestStruct".to_string()),
15110 ..Default::default()
15111 },
15112 ])))
15113 }
15114 });
15115 cx.update_editor(|editor, window, cx| {
15116 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15117 });
15118 completion_requests.next().await;
15119 cx.condition(|editor, _| editor.context_menu_visible())
15120 .await;
15121 cx.update_editor(|editor, _, _| {
15122 assert!(
15123 !editor.hover_state.visible(),
15124 "Hover popover should be hidden when completion menu is shown"
15125 );
15126 });
15127}
15128
15129#[gpui::test]
15130async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
15131 init_test(cx, |_| {});
15132
15133 let mut cx = EditorLspTestContext::new_rust(
15134 lsp::ServerCapabilities {
15135 completion_provider: Some(lsp::CompletionOptions {
15136 trigger_characters: Some(vec![".".to_string()]),
15137 resolve_provider: Some(true),
15138 ..Default::default()
15139 }),
15140 ..Default::default()
15141 },
15142 cx,
15143 )
15144 .await;
15145
15146 cx.set_state("fn main() { let a = 2ˇ; }");
15147 cx.simulate_keystroke(".");
15148
15149 let unresolved_item_1 = lsp::CompletionItem {
15150 label: "id".to_string(),
15151 filter_text: Some("id".to_string()),
15152 detail: None,
15153 documentation: None,
15154 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15155 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15156 new_text: ".id".to_string(),
15157 })),
15158 ..lsp::CompletionItem::default()
15159 };
15160 let resolved_item_1 = lsp::CompletionItem {
15161 additional_text_edits: Some(vec![lsp::TextEdit {
15162 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
15163 new_text: "!!".to_string(),
15164 }]),
15165 ..unresolved_item_1.clone()
15166 };
15167 let unresolved_item_2 = lsp::CompletionItem {
15168 label: "other".to_string(),
15169 filter_text: Some("other".to_string()),
15170 detail: None,
15171 documentation: None,
15172 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15173 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15174 new_text: ".other".to_string(),
15175 })),
15176 ..lsp::CompletionItem::default()
15177 };
15178 let resolved_item_2 = lsp::CompletionItem {
15179 additional_text_edits: Some(vec![lsp::TextEdit {
15180 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
15181 new_text: "??".to_string(),
15182 }]),
15183 ..unresolved_item_2.clone()
15184 };
15185
15186 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
15187 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
15188 cx.lsp
15189 .server
15190 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
15191 let unresolved_item_1 = unresolved_item_1.clone();
15192 let resolved_item_1 = resolved_item_1.clone();
15193 let unresolved_item_2 = unresolved_item_2.clone();
15194 let resolved_item_2 = resolved_item_2.clone();
15195 let resolve_requests_1 = resolve_requests_1.clone();
15196 let resolve_requests_2 = resolve_requests_2.clone();
15197 move |unresolved_request, _| {
15198 let unresolved_item_1 = unresolved_item_1.clone();
15199 let resolved_item_1 = resolved_item_1.clone();
15200 let unresolved_item_2 = unresolved_item_2.clone();
15201 let resolved_item_2 = resolved_item_2.clone();
15202 let resolve_requests_1 = resolve_requests_1.clone();
15203 let resolve_requests_2 = resolve_requests_2.clone();
15204 async move {
15205 if unresolved_request == unresolved_item_1 {
15206 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
15207 Ok(resolved_item_1.clone())
15208 } else if unresolved_request == unresolved_item_2 {
15209 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
15210 Ok(resolved_item_2.clone())
15211 } else {
15212 panic!("Unexpected completion item {unresolved_request:?}")
15213 }
15214 }
15215 }
15216 })
15217 .detach();
15218
15219 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15220 let unresolved_item_1 = unresolved_item_1.clone();
15221 let unresolved_item_2 = unresolved_item_2.clone();
15222 async move {
15223 Ok(Some(lsp::CompletionResponse::Array(vec![
15224 unresolved_item_1,
15225 unresolved_item_2,
15226 ])))
15227 }
15228 })
15229 .next()
15230 .await;
15231
15232 cx.condition(|editor, _| editor.context_menu_visible())
15233 .await;
15234 cx.update_editor(|editor, _, _| {
15235 let context_menu = editor.context_menu.borrow_mut();
15236 let context_menu = context_menu
15237 .as_ref()
15238 .expect("Should have the context menu deployed");
15239 match context_menu {
15240 CodeContextMenu::Completions(completions_menu) => {
15241 let completions = completions_menu.completions.borrow_mut();
15242 assert_eq!(
15243 completions
15244 .iter()
15245 .map(|completion| &completion.label.text)
15246 .collect::<Vec<_>>(),
15247 vec!["id", "other"]
15248 )
15249 }
15250 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15251 }
15252 });
15253 cx.run_until_parked();
15254
15255 cx.update_editor(|editor, window, cx| {
15256 editor.context_menu_next(&ContextMenuNext, window, cx);
15257 });
15258 cx.run_until_parked();
15259 cx.update_editor(|editor, window, cx| {
15260 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
15261 });
15262 cx.run_until_parked();
15263 cx.update_editor(|editor, window, cx| {
15264 editor.context_menu_next(&ContextMenuNext, window, cx);
15265 });
15266 cx.run_until_parked();
15267 cx.update_editor(|editor, window, cx| {
15268 editor
15269 .compose_completion(&ComposeCompletion::default(), window, cx)
15270 .expect("No task returned")
15271 })
15272 .await
15273 .expect("Completion failed");
15274 cx.run_until_parked();
15275
15276 cx.update_editor(|editor, _, cx| {
15277 assert_eq!(
15278 resolve_requests_1.load(atomic::Ordering::Acquire),
15279 1,
15280 "Should always resolve once despite multiple selections"
15281 );
15282 assert_eq!(
15283 resolve_requests_2.load(atomic::Ordering::Acquire),
15284 1,
15285 "Should always resolve once after multiple selections and applying the completion"
15286 );
15287 assert_eq!(
15288 editor.text(cx),
15289 "fn main() { let a = ??.other; }",
15290 "Should use resolved data when applying the completion"
15291 );
15292 });
15293}
15294
15295#[gpui::test]
15296async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
15297 init_test(cx, |_| {});
15298
15299 let item_0 = lsp::CompletionItem {
15300 label: "abs".into(),
15301 insert_text: Some("abs".into()),
15302 data: Some(json!({ "very": "special"})),
15303 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
15304 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
15305 lsp::InsertReplaceEdit {
15306 new_text: "abs".to_string(),
15307 insert: lsp::Range::default(),
15308 replace: lsp::Range::default(),
15309 },
15310 )),
15311 ..lsp::CompletionItem::default()
15312 };
15313 let items = iter::once(item_0.clone())
15314 .chain((11..51).map(|i| lsp::CompletionItem {
15315 label: format!("item_{}", i),
15316 insert_text: Some(format!("item_{}", i)),
15317 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15318 ..lsp::CompletionItem::default()
15319 }))
15320 .collect::<Vec<_>>();
15321
15322 let default_commit_characters = vec!["?".to_string()];
15323 let default_data = json!({ "default": "data"});
15324 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
15325 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
15326 let default_edit_range = lsp::Range {
15327 start: lsp::Position {
15328 line: 0,
15329 character: 5,
15330 },
15331 end: lsp::Position {
15332 line: 0,
15333 character: 5,
15334 },
15335 };
15336
15337 let mut cx = EditorLspTestContext::new_rust(
15338 lsp::ServerCapabilities {
15339 completion_provider: Some(lsp::CompletionOptions {
15340 trigger_characters: Some(vec![".".to_string()]),
15341 resolve_provider: Some(true),
15342 ..Default::default()
15343 }),
15344 ..Default::default()
15345 },
15346 cx,
15347 )
15348 .await;
15349
15350 cx.set_state("fn main() { let a = 2ˇ; }");
15351 cx.simulate_keystroke(".");
15352
15353 let completion_data = default_data.clone();
15354 let completion_characters = default_commit_characters.clone();
15355 let completion_items = items.clone();
15356 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15357 let default_data = completion_data.clone();
15358 let default_commit_characters = completion_characters.clone();
15359 let items = completion_items.clone();
15360 async move {
15361 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15362 items,
15363 item_defaults: Some(lsp::CompletionListItemDefaults {
15364 data: Some(default_data.clone()),
15365 commit_characters: Some(default_commit_characters.clone()),
15366 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
15367 default_edit_range,
15368 )),
15369 insert_text_format: Some(default_insert_text_format),
15370 insert_text_mode: Some(default_insert_text_mode),
15371 }),
15372 ..lsp::CompletionList::default()
15373 })))
15374 }
15375 })
15376 .next()
15377 .await;
15378
15379 let resolved_items = Arc::new(Mutex::new(Vec::new()));
15380 cx.lsp
15381 .server
15382 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
15383 let closure_resolved_items = resolved_items.clone();
15384 move |item_to_resolve, _| {
15385 let closure_resolved_items = closure_resolved_items.clone();
15386 async move {
15387 closure_resolved_items.lock().push(item_to_resolve.clone());
15388 Ok(item_to_resolve)
15389 }
15390 }
15391 })
15392 .detach();
15393
15394 cx.condition(|editor, _| editor.context_menu_visible())
15395 .await;
15396 cx.run_until_parked();
15397 cx.update_editor(|editor, _, _| {
15398 let menu = editor.context_menu.borrow_mut();
15399 match menu.as_ref().expect("should have the completions menu") {
15400 CodeContextMenu::Completions(completions_menu) => {
15401 assert_eq!(
15402 completions_menu
15403 .entries
15404 .borrow()
15405 .iter()
15406 .map(|mat| mat.string.clone())
15407 .collect::<Vec<String>>(),
15408 items
15409 .iter()
15410 .map(|completion| completion.label.clone())
15411 .collect::<Vec<String>>()
15412 );
15413 }
15414 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
15415 }
15416 });
15417 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
15418 // with 4 from the end.
15419 assert_eq!(
15420 *resolved_items.lock(),
15421 [&items[0..16], &items[items.len() - 4..items.len()]]
15422 .concat()
15423 .iter()
15424 .cloned()
15425 .map(|mut item| {
15426 if item.data.is_none() {
15427 item.data = Some(default_data.clone());
15428 }
15429 item
15430 })
15431 .collect::<Vec<lsp::CompletionItem>>(),
15432 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
15433 );
15434 resolved_items.lock().clear();
15435
15436 cx.update_editor(|editor, window, cx| {
15437 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
15438 });
15439 cx.run_until_parked();
15440 // Completions that have already been resolved are skipped.
15441 assert_eq!(
15442 *resolved_items.lock(),
15443 items[items.len() - 16..items.len() - 4]
15444 .iter()
15445 .cloned()
15446 .map(|mut item| {
15447 if item.data.is_none() {
15448 item.data = Some(default_data.clone());
15449 }
15450 item
15451 })
15452 .collect::<Vec<lsp::CompletionItem>>()
15453 );
15454 resolved_items.lock().clear();
15455}
15456
15457#[gpui::test]
15458async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
15459 init_test(cx, |_| {});
15460
15461 let mut cx = EditorLspTestContext::new(
15462 Language::new(
15463 LanguageConfig {
15464 matcher: LanguageMatcher {
15465 path_suffixes: vec!["jsx".into()],
15466 ..Default::default()
15467 },
15468 overrides: [(
15469 "element".into(),
15470 LanguageConfigOverride {
15471 completion_query_characters: Override::Set(['-'].into_iter().collect()),
15472 ..Default::default()
15473 },
15474 )]
15475 .into_iter()
15476 .collect(),
15477 ..Default::default()
15478 },
15479 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
15480 )
15481 .with_override_query("(jsx_self_closing_element) @element")
15482 .unwrap(),
15483 lsp::ServerCapabilities {
15484 completion_provider: Some(lsp::CompletionOptions {
15485 trigger_characters: Some(vec![":".to_string()]),
15486 ..Default::default()
15487 }),
15488 ..Default::default()
15489 },
15490 cx,
15491 )
15492 .await;
15493
15494 cx.lsp
15495 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15496 Ok(Some(lsp::CompletionResponse::Array(vec![
15497 lsp::CompletionItem {
15498 label: "bg-blue".into(),
15499 ..Default::default()
15500 },
15501 lsp::CompletionItem {
15502 label: "bg-red".into(),
15503 ..Default::default()
15504 },
15505 lsp::CompletionItem {
15506 label: "bg-yellow".into(),
15507 ..Default::default()
15508 },
15509 ])))
15510 });
15511
15512 cx.set_state(r#"<p class="bgˇ" />"#);
15513
15514 // Trigger completion when typing a dash, because the dash is an extra
15515 // word character in the 'element' scope, which contains the cursor.
15516 cx.simulate_keystroke("-");
15517 cx.executor().run_until_parked();
15518 cx.update_editor(|editor, _, _| {
15519 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15520 {
15521 assert_eq!(
15522 completion_menu_entries(&menu),
15523 &["bg-blue", "bg-red", "bg-yellow"]
15524 );
15525 } else {
15526 panic!("expected completion menu to be open");
15527 }
15528 });
15529
15530 cx.simulate_keystroke("l");
15531 cx.executor().run_until_parked();
15532 cx.update_editor(|editor, _, _| {
15533 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15534 {
15535 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
15536 } else {
15537 panic!("expected completion menu to be open");
15538 }
15539 });
15540
15541 // When filtering completions, consider the character after the '-' to
15542 // be the start of a subword.
15543 cx.set_state(r#"<p class="yelˇ" />"#);
15544 cx.simulate_keystroke("l");
15545 cx.executor().run_until_parked();
15546 cx.update_editor(|editor, _, _| {
15547 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15548 {
15549 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
15550 } else {
15551 panic!("expected completion menu to be open");
15552 }
15553 });
15554}
15555
15556fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
15557 let entries = menu.entries.borrow();
15558 entries.iter().map(|mat| mat.string.clone()).collect()
15559}
15560
15561#[gpui::test]
15562async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
15563 init_test(cx, |settings| {
15564 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
15565 FormatterList(vec![Formatter::Prettier].into()),
15566 ))
15567 });
15568
15569 let fs = FakeFs::new(cx.executor());
15570 fs.insert_file(path!("/file.ts"), Default::default()).await;
15571
15572 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
15573 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15574
15575 language_registry.add(Arc::new(Language::new(
15576 LanguageConfig {
15577 name: "TypeScript".into(),
15578 matcher: LanguageMatcher {
15579 path_suffixes: vec!["ts".to_string()],
15580 ..Default::default()
15581 },
15582 ..Default::default()
15583 },
15584 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
15585 )));
15586 update_test_language_settings(cx, |settings| {
15587 settings.defaults.prettier = Some(PrettierSettings {
15588 allowed: true,
15589 ..PrettierSettings::default()
15590 });
15591 });
15592
15593 let test_plugin = "test_plugin";
15594 let _ = language_registry.register_fake_lsp(
15595 "TypeScript",
15596 FakeLspAdapter {
15597 prettier_plugins: vec![test_plugin],
15598 ..Default::default()
15599 },
15600 );
15601
15602 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
15603 let buffer = project
15604 .update(cx, |project, cx| {
15605 project.open_local_buffer(path!("/file.ts"), cx)
15606 })
15607 .await
15608 .unwrap();
15609
15610 let buffer_text = "one\ntwo\nthree\n";
15611 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
15612 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
15613 editor.update_in(cx, |editor, window, cx| {
15614 editor.set_text(buffer_text, window, cx)
15615 });
15616
15617 editor
15618 .update_in(cx, |editor, window, cx| {
15619 editor.perform_format(
15620 project.clone(),
15621 FormatTrigger::Manual,
15622 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
15623 window,
15624 cx,
15625 )
15626 })
15627 .unwrap()
15628 .await;
15629 assert_eq!(
15630 editor.update(cx, |editor, cx| editor.text(cx)),
15631 buffer_text.to_string() + prettier_format_suffix,
15632 "Test prettier formatting was not applied to the original buffer text",
15633 );
15634
15635 update_test_language_settings(cx, |settings| {
15636 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
15637 });
15638 let format = editor.update_in(cx, |editor, window, cx| {
15639 editor.perform_format(
15640 project.clone(),
15641 FormatTrigger::Manual,
15642 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
15643 window,
15644 cx,
15645 )
15646 });
15647 format.await.unwrap();
15648 assert_eq!(
15649 editor.update(cx, |editor, cx| editor.text(cx)),
15650 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
15651 "Autoformatting (via test prettier) was not applied to the original buffer text",
15652 );
15653}
15654
15655#[gpui::test]
15656async fn test_addition_reverts(cx: &mut TestAppContext) {
15657 init_test(cx, |_| {});
15658 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15659 let base_text = indoc! {r#"
15660 struct Row;
15661 struct Row1;
15662 struct Row2;
15663
15664 struct Row4;
15665 struct Row5;
15666 struct Row6;
15667
15668 struct Row8;
15669 struct Row9;
15670 struct Row10;"#};
15671
15672 // When addition hunks are not adjacent to carets, no hunk revert is performed
15673 assert_hunk_revert(
15674 indoc! {r#"struct Row;
15675 struct Row1;
15676 struct Row1.1;
15677 struct Row1.2;
15678 struct Row2;ˇ
15679
15680 struct Row4;
15681 struct Row5;
15682 struct Row6;
15683
15684 struct Row8;
15685 ˇstruct Row9;
15686 struct Row9.1;
15687 struct Row9.2;
15688 struct Row9.3;
15689 struct Row10;"#},
15690 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
15691 indoc! {r#"struct Row;
15692 struct Row1;
15693 struct Row1.1;
15694 struct Row1.2;
15695 struct Row2;ˇ
15696
15697 struct Row4;
15698 struct Row5;
15699 struct Row6;
15700
15701 struct Row8;
15702 ˇstruct Row9;
15703 struct Row9.1;
15704 struct Row9.2;
15705 struct Row9.3;
15706 struct Row10;"#},
15707 base_text,
15708 &mut cx,
15709 );
15710 // Same for selections
15711 assert_hunk_revert(
15712 indoc! {r#"struct Row;
15713 struct Row1;
15714 struct Row2;
15715 struct Row2.1;
15716 struct Row2.2;
15717 «ˇ
15718 struct Row4;
15719 struct» Row5;
15720 «struct Row6;
15721 ˇ»
15722 struct Row9.1;
15723 struct Row9.2;
15724 struct Row9.3;
15725 struct Row8;
15726 struct Row9;
15727 struct Row10;"#},
15728 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
15729 indoc! {r#"struct Row;
15730 struct Row1;
15731 struct Row2;
15732 struct Row2.1;
15733 struct Row2.2;
15734 «ˇ
15735 struct Row4;
15736 struct» Row5;
15737 «struct Row6;
15738 ˇ»
15739 struct Row9.1;
15740 struct Row9.2;
15741 struct Row9.3;
15742 struct Row8;
15743 struct Row9;
15744 struct Row10;"#},
15745 base_text,
15746 &mut cx,
15747 );
15748
15749 // When carets and selections intersect the addition hunks, those are reverted.
15750 // Adjacent carets got merged.
15751 assert_hunk_revert(
15752 indoc! {r#"struct Row;
15753 ˇ// something on the top
15754 struct Row1;
15755 struct Row2;
15756 struct Roˇw3.1;
15757 struct Row2.2;
15758 struct Row2.3;ˇ
15759
15760 struct Row4;
15761 struct ˇRow5.1;
15762 struct Row5.2;
15763 struct «Rowˇ»5.3;
15764 struct Row5;
15765 struct Row6;
15766 ˇ
15767 struct Row9.1;
15768 struct «Rowˇ»9.2;
15769 struct «ˇRow»9.3;
15770 struct Row8;
15771 struct Row9;
15772 «ˇ// something on bottom»
15773 struct Row10;"#},
15774 vec![
15775 DiffHunkStatusKind::Added,
15776 DiffHunkStatusKind::Added,
15777 DiffHunkStatusKind::Added,
15778 DiffHunkStatusKind::Added,
15779 DiffHunkStatusKind::Added,
15780 ],
15781 indoc! {r#"struct Row;
15782 ˇstruct Row1;
15783 struct Row2;
15784 ˇ
15785 struct Row4;
15786 ˇstruct Row5;
15787 struct Row6;
15788 ˇ
15789 ˇstruct Row8;
15790 struct Row9;
15791 ˇstruct Row10;"#},
15792 base_text,
15793 &mut cx,
15794 );
15795}
15796
15797#[gpui::test]
15798async fn test_modification_reverts(cx: &mut TestAppContext) {
15799 init_test(cx, |_| {});
15800 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15801 let base_text = indoc! {r#"
15802 struct Row;
15803 struct Row1;
15804 struct Row2;
15805
15806 struct Row4;
15807 struct Row5;
15808 struct Row6;
15809
15810 struct Row8;
15811 struct Row9;
15812 struct Row10;"#};
15813
15814 // Modification hunks behave the same as the addition ones.
15815 assert_hunk_revert(
15816 indoc! {r#"struct Row;
15817 struct Row1;
15818 struct Row33;
15819 ˇ
15820 struct Row4;
15821 struct Row5;
15822 struct Row6;
15823 ˇ
15824 struct Row99;
15825 struct Row9;
15826 struct Row10;"#},
15827 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
15828 indoc! {r#"struct Row;
15829 struct Row1;
15830 struct Row33;
15831 ˇ
15832 struct Row4;
15833 struct Row5;
15834 struct Row6;
15835 ˇ
15836 struct Row99;
15837 struct Row9;
15838 struct Row10;"#},
15839 base_text,
15840 &mut cx,
15841 );
15842 assert_hunk_revert(
15843 indoc! {r#"struct Row;
15844 struct Row1;
15845 struct Row33;
15846 «ˇ
15847 struct Row4;
15848 struct» Row5;
15849 «struct Row6;
15850 ˇ»
15851 struct Row99;
15852 struct Row9;
15853 struct Row10;"#},
15854 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
15855 indoc! {r#"struct Row;
15856 struct Row1;
15857 struct Row33;
15858 «ˇ
15859 struct Row4;
15860 struct» Row5;
15861 «struct Row6;
15862 ˇ»
15863 struct Row99;
15864 struct Row9;
15865 struct Row10;"#},
15866 base_text,
15867 &mut cx,
15868 );
15869
15870 assert_hunk_revert(
15871 indoc! {r#"ˇstruct Row1.1;
15872 struct Row1;
15873 «ˇstr»uct Row22;
15874
15875 struct ˇRow44;
15876 struct Row5;
15877 struct «Rˇ»ow66;ˇ
15878
15879 «struˇ»ct Row88;
15880 struct Row9;
15881 struct Row1011;ˇ"#},
15882 vec![
15883 DiffHunkStatusKind::Modified,
15884 DiffHunkStatusKind::Modified,
15885 DiffHunkStatusKind::Modified,
15886 DiffHunkStatusKind::Modified,
15887 DiffHunkStatusKind::Modified,
15888 DiffHunkStatusKind::Modified,
15889 ],
15890 indoc! {r#"struct Row;
15891 ˇstruct Row1;
15892 struct Row2;
15893 ˇ
15894 struct Row4;
15895 ˇstruct Row5;
15896 struct Row6;
15897 ˇ
15898 struct Row8;
15899 ˇstruct Row9;
15900 struct Row10;ˇ"#},
15901 base_text,
15902 &mut cx,
15903 );
15904}
15905
15906#[gpui::test]
15907async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
15908 init_test(cx, |_| {});
15909 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15910 let base_text = indoc! {r#"
15911 one
15912
15913 two
15914 three
15915 "#};
15916
15917 cx.set_head_text(base_text);
15918 cx.set_state("\nˇ\n");
15919 cx.executor().run_until_parked();
15920 cx.update_editor(|editor, _window, cx| {
15921 editor.expand_selected_diff_hunks(cx);
15922 });
15923 cx.executor().run_until_parked();
15924 cx.update_editor(|editor, window, cx| {
15925 editor.backspace(&Default::default(), window, cx);
15926 });
15927 cx.run_until_parked();
15928 cx.assert_state_with_diff(
15929 indoc! {r#"
15930
15931 - two
15932 - threeˇ
15933 +
15934 "#}
15935 .to_string(),
15936 );
15937}
15938
15939#[gpui::test]
15940async fn test_deletion_reverts(cx: &mut TestAppContext) {
15941 init_test(cx, |_| {});
15942 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15943 let base_text = indoc! {r#"struct Row;
15944struct Row1;
15945struct Row2;
15946
15947struct Row4;
15948struct Row5;
15949struct Row6;
15950
15951struct Row8;
15952struct Row9;
15953struct Row10;"#};
15954
15955 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
15956 assert_hunk_revert(
15957 indoc! {r#"struct Row;
15958 struct Row2;
15959
15960 ˇstruct Row4;
15961 struct Row5;
15962 struct Row6;
15963 ˇ
15964 struct Row8;
15965 struct Row10;"#},
15966 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
15967 indoc! {r#"struct Row;
15968 struct Row2;
15969
15970 ˇstruct Row4;
15971 struct Row5;
15972 struct Row6;
15973 ˇ
15974 struct Row8;
15975 struct Row10;"#},
15976 base_text,
15977 &mut cx,
15978 );
15979 assert_hunk_revert(
15980 indoc! {r#"struct Row;
15981 struct Row2;
15982
15983 «ˇstruct Row4;
15984 struct» Row5;
15985 «struct Row6;
15986 ˇ»
15987 struct Row8;
15988 struct Row10;"#},
15989 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
15990 indoc! {r#"struct Row;
15991 struct Row2;
15992
15993 «ˇstruct Row4;
15994 struct» Row5;
15995 «struct Row6;
15996 ˇ»
15997 struct Row8;
15998 struct Row10;"#},
15999 base_text,
16000 &mut cx,
16001 );
16002
16003 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
16004 assert_hunk_revert(
16005 indoc! {r#"struct Row;
16006 ˇstruct Row2;
16007
16008 struct Row4;
16009 struct Row5;
16010 struct Row6;
16011
16012 struct Row8;ˇ
16013 struct Row10;"#},
16014 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16015 indoc! {r#"struct Row;
16016 struct Row1;
16017 ˇstruct Row2;
16018
16019 struct Row4;
16020 struct Row5;
16021 struct Row6;
16022
16023 struct Row8;ˇ
16024 struct Row9;
16025 struct Row10;"#},
16026 base_text,
16027 &mut cx,
16028 );
16029 assert_hunk_revert(
16030 indoc! {r#"struct Row;
16031 struct Row2«ˇ;
16032 struct Row4;
16033 struct» Row5;
16034 «struct Row6;
16035
16036 struct Row8;ˇ»
16037 struct Row10;"#},
16038 vec![
16039 DiffHunkStatusKind::Deleted,
16040 DiffHunkStatusKind::Deleted,
16041 DiffHunkStatusKind::Deleted,
16042 ],
16043 indoc! {r#"struct Row;
16044 struct Row1;
16045 struct Row2«ˇ;
16046
16047 struct Row4;
16048 struct» Row5;
16049 «struct Row6;
16050
16051 struct Row8;ˇ»
16052 struct Row9;
16053 struct Row10;"#},
16054 base_text,
16055 &mut cx,
16056 );
16057}
16058
16059#[gpui::test]
16060async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
16061 init_test(cx, |_| {});
16062
16063 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
16064 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
16065 let base_text_3 =
16066 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
16067
16068 let text_1 = edit_first_char_of_every_line(base_text_1);
16069 let text_2 = edit_first_char_of_every_line(base_text_2);
16070 let text_3 = edit_first_char_of_every_line(base_text_3);
16071
16072 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
16073 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
16074 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
16075
16076 let multibuffer = cx.new(|cx| {
16077 let mut multibuffer = MultiBuffer::new(ReadWrite);
16078 multibuffer.push_excerpts(
16079 buffer_1.clone(),
16080 [
16081 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16082 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16083 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16084 ],
16085 cx,
16086 );
16087 multibuffer.push_excerpts(
16088 buffer_2.clone(),
16089 [
16090 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16091 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16092 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16093 ],
16094 cx,
16095 );
16096 multibuffer.push_excerpts(
16097 buffer_3.clone(),
16098 [
16099 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16100 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16101 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16102 ],
16103 cx,
16104 );
16105 multibuffer
16106 });
16107
16108 let fs = FakeFs::new(cx.executor());
16109 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
16110 let (editor, cx) = cx
16111 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
16112 editor.update_in(cx, |editor, _window, cx| {
16113 for (buffer, diff_base) in [
16114 (buffer_1.clone(), base_text_1),
16115 (buffer_2.clone(), base_text_2),
16116 (buffer_3.clone(), base_text_3),
16117 ] {
16118 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
16119 editor
16120 .buffer
16121 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
16122 }
16123 });
16124 cx.executor().run_until_parked();
16125
16126 editor.update_in(cx, |editor, window, cx| {
16127 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}");
16128 editor.select_all(&SelectAll, window, cx);
16129 editor.git_restore(&Default::default(), window, cx);
16130 });
16131 cx.executor().run_until_parked();
16132
16133 // When all ranges are selected, all buffer hunks are reverted.
16134 editor.update(cx, |editor, cx| {
16135 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");
16136 });
16137 buffer_1.update(cx, |buffer, _| {
16138 assert_eq!(buffer.text(), base_text_1);
16139 });
16140 buffer_2.update(cx, |buffer, _| {
16141 assert_eq!(buffer.text(), base_text_2);
16142 });
16143 buffer_3.update(cx, |buffer, _| {
16144 assert_eq!(buffer.text(), base_text_3);
16145 });
16146
16147 editor.update_in(cx, |editor, window, cx| {
16148 editor.undo(&Default::default(), window, cx);
16149 });
16150
16151 editor.update_in(cx, |editor, window, cx| {
16152 editor.change_selections(None, window, cx, |s| {
16153 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
16154 });
16155 editor.git_restore(&Default::default(), window, cx);
16156 });
16157
16158 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
16159 // but not affect buffer_2 and its related excerpts.
16160 editor.update(cx, |editor, cx| {
16161 assert_eq!(
16162 editor.text(cx),
16163 "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}"
16164 );
16165 });
16166 buffer_1.update(cx, |buffer, _| {
16167 assert_eq!(buffer.text(), base_text_1);
16168 });
16169 buffer_2.update(cx, |buffer, _| {
16170 assert_eq!(
16171 buffer.text(),
16172 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
16173 );
16174 });
16175 buffer_3.update(cx, |buffer, _| {
16176 assert_eq!(
16177 buffer.text(),
16178 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
16179 );
16180 });
16181
16182 fn edit_first_char_of_every_line(text: &str) -> String {
16183 text.split('\n')
16184 .map(|line| format!("X{}", &line[1..]))
16185 .collect::<Vec<_>>()
16186 .join("\n")
16187 }
16188}
16189
16190#[gpui::test]
16191async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
16192 init_test(cx, |_| {});
16193
16194 let cols = 4;
16195 let rows = 10;
16196 let sample_text_1 = sample_text(rows, cols, 'a');
16197 assert_eq!(
16198 sample_text_1,
16199 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
16200 );
16201 let sample_text_2 = sample_text(rows, cols, 'l');
16202 assert_eq!(
16203 sample_text_2,
16204 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
16205 );
16206 let sample_text_3 = sample_text(rows, cols, 'v');
16207 assert_eq!(
16208 sample_text_3,
16209 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
16210 );
16211
16212 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
16213 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
16214 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
16215
16216 let multi_buffer = cx.new(|cx| {
16217 let mut multibuffer = MultiBuffer::new(ReadWrite);
16218 multibuffer.push_excerpts(
16219 buffer_1.clone(),
16220 [
16221 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16222 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16223 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16224 ],
16225 cx,
16226 );
16227 multibuffer.push_excerpts(
16228 buffer_2.clone(),
16229 [
16230 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16231 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16232 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16233 ],
16234 cx,
16235 );
16236 multibuffer.push_excerpts(
16237 buffer_3.clone(),
16238 [
16239 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16240 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16241 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16242 ],
16243 cx,
16244 );
16245 multibuffer
16246 });
16247
16248 let fs = FakeFs::new(cx.executor());
16249 fs.insert_tree(
16250 "/a",
16251 json!({
16252 "main.rs": sample_text_1,
16253 "other.rs": sample_text_2,
16254 "lib.rs": sample_text_3,
16255 }),
16256 )
16257 .await;
16258 let project = Project::test(fs, ["/a".as_ref()], cx).await;
16259 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16260 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16261 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
16262 Editor::new(
16263 EditorMode::full(),
16264 multi_buffer,
16265 Some(project.clone()),
16266 window,
16267 cx,
16268 )
16269 });
16270 let multibuffer_item_id = workspace
16271 .update(cx, |workspace, window, cx| {
16272 assert!(
16273 workspace.active_item(cx).is_none(),
16274 "active item should be None before the first item is added"
16275 );
16276 workspace.add_item_to_active_pane(
16277 Box::new(multi_buffer_editor.clone()),
16278 None,
16279 true,
16280 window,
16281 cx,
16282 );
16283 let active_item = workspace
16284 .active_item(cx)
16285 .expect("should have an active item after adding the multi buffer");
16286 assert!(
16287 !active_item.is_singleton(cx),
16288 "A multi buffer was expected to active after adding"
16289 );
16290 active_item.item_id()
16291 })
16292 .unwrap();
16293 cx.executor().run_until_parked();
16294
16295 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16296 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
16297 s.select_ranges(Some(1..2))
16298 });
16299 editor.open_excerpts(&OpenExcerpts, window, cx);
16300 });
16301 cx.executor().run_until_parked();
16302 let first_item_id = workspace
16303 .update(cx, |workspace, window, cx| {
16304 let active_item = workspace
16305 .active_item(cx)
16306 .expect("should have an active item after navigating into the 1st buffer");
16307 let first_item_id = active_item.item_id();
16308 assert_ne!(
16309 first_item_id, multibuffer_item_id,
16310 "Should navigate into the 1st buffer and activate it"
16311 );
16312 assert!(
16313 active_item.is_singleton(cx),
16314 "New active item should be a singleton buffer"
16315 );
16316 assert_eq!(
16317 active_item
16318 .act_as::<Editor>(cx)
16319 .expect("should have navigated into an editor for the 1st buffer")
16320 .read(cx)
16321 .text(cx),
16322 sample_text_1
16323 );
16324
16325 workspace
16326 .go_back(workspace.active_pane().downgrade(), window, cx)
16327 .detach_and_log_err(cx);
16328
16329 first_item_id
16330 })
16331 .unwrap();
16332 cx.executor().run_until_parked();
16333 workspace
16334 .update(cx, |workspace, _, cx| {
16335 let active_item = workspace
16336 .active_item(cx)
16337 .expect("should have an active item after navigating back");
16338 assert_eq!(
16339 active_item.item_id(),
16340 multibuffer_item_id,
16341 "Should navigate back to the multi buffer"
16342 );
16343 assert!(!active_item.is_singleton(cx));
16344 })
16345 .unwrap();
16346
16347 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16348 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
16349 s.select_ranges(Some(39..40))
16350 });
16351 editor.open_excerpts(&OpenExcerpts, window, cx);
16352 });
16353 cx.executor().run_until_parked();
16354 let second_item_id = workspace
16355 .update(cx, |workspace, window, cx| {
16356 let active_item = workspace
16357 .active_item(cx)
16358 .expect("should have an active item after navigating into the 2nd buffer");
16359 let second_item_id = active_item.item_id();
16360 assert_ne!(
16361 second_item_id, multibuffer_item_id,
16362 "Should navigate away from the multibuffer"
16363 );
16364 assert_ne!(
16365 second_item_id, first_item_id,
16366 "Should navigate into the 2nd buffer and activate it"
16367 );
16368 assert!(
16369 active_item.is_singleton(cx),
16370 "New active item should be a singleton buffer"
16371 );
16372 assert_eq!(
16373 active_item
16374 .act_as::<Editor>(cx)
16375 .expect("should have navigated into an editor")
16376 .read(cx)
16377 .text(cx),
16378 sample_text_2
16379 );
16380
16381 workspace
16382 .go_back(workspace.active_pane().downgrade(), window, cx)
16383 .detach_and_log_err(cx);
16384
16385 second_item_id
16386 })
16387 .unwrap();
16388 cx.executor().run_until_parked();
16389 workspace
16390 .update(cx, |workspace, _, cx| {
16391 let active_item = workspace
16392 .active_item(cx)
16393 .expect("should have an active item after navigating back from the 2nd buffer");
16394 assert_eq!(
16395 active_item.item_id(),
16396 multibuffer_item_id,
16397 "Should navigate back from the 2nd buffer to the multi buffer"
16398 );
16399 assert!(!active_item.is_singleton(cx));
16400 })
16401 .unwrap();
16402
16403 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16404 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
16405 s.select_ranges(Some(70..70))
16406 });
16407 editor.open_excerpts(&OpenExcerpts, window, cx);
16408 });
16409 cx.executor().run_until_parked();
16410 workspace
16411 .update(cx, |workspace, window, cx| {
16412 let active_item = workspace
16413 .active_item(cx)
16414 .expect("should have an active item after navigating into the 3rd buffer");
16415 let third_item_id = active_item.item_id();
16416 assert_ne!(
16417 third_item_id, multibuffer_item_id,
16418 "Should navigate into the 3rd buffer and activate it"
16419 );
16420 assert_ne!(third_item_id, first_item_id);
16421 assert_ne!(third_item_id, second_item_id);
16422 assert!(
16423 active_item.is_singleton(cx),
16424 "New active item should be a singleton buffer"
16425 );
16426 assert_eq!(
16427 active_item
16428 .act_as::<Editor>(cx)
16429 .expect("should have navigated into an editor")
16430 .read(cx)
16431 .text(cx),
16432 sample_text_3
16433 );
16434
16435 workspace
16436 .go_back(workspace.active_pane().downgrade(), window, cx)
16437 .detach_and_log_err(cx);
16438 })
16439 .unwrap();
16440 cx.executor().run_until_parked();
16441 workspace
16442 .update(cx, |workspace, _, cx| {
16443 let active_item = workspace
16444 .active_item(cx)
16445 .expect("should have an active item after navigating back from the 3rd buffer");
16446 assert_eq!(
16447 active_item.item_id(),
16448 multibuffer_item_id,
16449 "Should navigate back from the 3rd buffer to the multi buffer"
16450 );
16451 assert!(!active_item.is_singleton(cx));
16452 })
16453 .unwrap();
16454}
16455
16456#[gpui::test]
16457async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16458 init_test(cx, |_| {});
16459
16460 let mut cx = EditorTestContext::new(cx).await;
16461
16462 let diff_base = r#"
16463 use some::mod;
16464
16465 const A: u32 = 42;
16466
16467 fn main() {
16468 println!("hello");
16469
16470 println!("world");
16471 }
16472 "#
16473 .unindent();
16474
16475 cx.set_state(
16476 &r#"
16477 use some::modified;
16478
16479 ˇ
16480 fn main() {
16481 println!("hello there");
16482
16483 println!("around the");
16484 println!("world");
16485 }
16486 "#
16487 .unindent(),
16488 );
16489
16490 cx.set_head_text(&diff_base);
16491 executor.run_until_parked();
16492
16493 cx.update_editor(|editor, window, cx| {
16494 editor.go_to_next_hunk(&GoToHunk, window, cx);
16495 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
16496 });
16497 executor.run_until_parked();
16498 cx.assert_state_with_diff(
16499 r#"
16500 use some::modified;
16501
16502
16503 fn main() {
16504 - println!("hello");
16505 + ˇ println!("hello there");
16506
16507 println!("around the");
16508 println!("world");
16509 }
16510 "#
16511 .unindent(),
16512 );
16513
16514 cx.update_editor(|editor, window, cx| {
16515 for _ in 0..2 {
16516 editor.go_to_next_hunk(&GoToHunk, window, cx);
16517 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
16518 }
16519 });
16520 executor.run_until_parked();
16521 cx.assert_state_with_diff(
16522 r#"
16523 - use some::mod;
16524 + ˇuse some::modified;
16525
16526
16527 fn main() {
16528 - println!("hello");
16529 + println!("hello there");
16530
16531 + println!("around the");
16532 println!("world");
16533 }
16534 "#
16535 .unindent(),
16536 );
16537
16538 cx.update_editor(|editor, window, cx| {
16539 editor.go_to_next_hunk(&GoToHunk, window, cx);
16540 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
16541 });
16542 executor.run_until_parked();
16543 cx.assert_state_with_diff(
16544 r#"
16545 - use some::mod;
16546 + use some::modified;
16547
16548 - const A: u32 = 42;
16549 ˇ
16550 fn main() {
16551 - println!("hello");
16552 + println!("hello there");
16553
16554 + println!("around the");
16555 println!("world");
16556 }
16557 "#
16558 .unindent(),
16559 );
16560
16561 cx.update_editor(|editor, window, cx| {
16562 editor.cancel(&Cancel, window, cx);
16563 });
16564
16565 cx.assert_state_with_diff(
16566 r#"
16567 use some::modified;
16568
16569 ˇ
16570 fn main() {
16571 println!("hello there");
16572
16573 println!("around the");
16574 println!("world");
16575 }
16576 "#
16577 .unindent(),
16578 );
16579}
16580
16581#[gpui::test]
16582async fn test_diff_base_change_with_expanded_diff_hunks(
16583 executor: BackgroundExecutor,
16584 cx: &mut TestAppContext,
16585) {
16586 init_test(cx, |_| {});
16587
16588 let mut cx = EditorTestContext::new(cx).await;
16589
16590 let diff_base = r#"
16591 use some::mod1;
16592 use some::mod2;
16593
16594 const A: u32 = 42;
16595 const B: u32 = 42;
16596 const C: u32 = 42;
16597
16598 fn main() {
16599 println!("hello");
16600
16601 println!("world");
16602 }
16603 "#
16604 .unindent();
16605
16606 cx.set_state(
16607 &r#"
16608 use some::mod2;
16609
16610 const A: u32 = 42;
16611 const C: u32 = 42;
16612
16613 fn main(ˇ) {
16614 //println!("hello");
16615
16616 println!("world");
16617 //
16618 //
16619 }
16620 "#
16621 .unindent(),
16622 );
16623
16624 cx.set_head_text(&diff_base);
16625 executor.run_until_parked();
16626
16627 cx.update_editor(|editor, window, cx| {
16628 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16629 });
16630 executor.run_until_parked();
16631 cx.assert_state_with_diff(
16632 r#"
16633 - use some::mod1;
16634 use some::mod2;
16635
16636 const A: u32 = 42;
16637 - const B: u32 = 42;
16638 const C: u32 = 42;
16639
16640 fn main(ˇ) {
16641 - println!("hello");
16642 + //println!("hello");
16643
16644 println!("world");
16645 + //
16646 + //
16647 }
16648 "#
16649 .unindent(),
16650 );
16651
16652 cx.set_head_text("new diff base!");
16653 executor.run_until_parked();
16654 cx.assert_state_with_diff(
16655 r#"
16656 - new diff base!
16657 + use some::mod2;
16658 +
16659 + const A: u32 = 42;
16660 + const C: u32 = 42;
16661 +
16662 + fn main(ˇ) {
16663 + //println!("hello");
16664 +
16665 + println!("world");
16666 + //
16667 + //
16668 + }
16669 "#
16670 .unindent(),
16671 );
16672}
16673
16674#[gpui::test]
16675async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
16676 init_test(cx, |_| {});
16677
16678 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
16679 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
16680 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
16681 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
16682 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
16683 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
16684
16685 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
16686 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
16687 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
16688
16689 let multi_buffer = cx.new(|cx| {
16690 let mut multibuffer = MultiBuffer::new(ReadWrite);
16691 multibuffer.push_excerpts(
16692 buffer_1.clone(),
16693 [
16694 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16695 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16696 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
16697 ],
16698 cx,
16699 );
16700 multibuffer.push_excerpts(
16701 buffer_2.clone(),
16702 [
16703 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16704 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16705 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
16706 ],
16707 cx,
16708 );
16709 multibuffer.push_excerpts(
16710 buffer_3.clone(),
16711 [
16712 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16713 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16714 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
16715 ],
16716 cx,
16717 );
16718 multibuffer
16719 });
16720
16721 let editor =
16722 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
16723 editor
16724 .update(cx, |editor, _window, cx| {
16725 for (buffer, diff_base) in [
16726 (buffer_1.clone(), file_1_old),
16727 (buffer_2.clone(), file_2_old),
16728 (buffer_3.clone(), file_3_old),
16729 ] {
16730 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
16731 editor
16732 .buffer
16733 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
16734 }
16735 })
16736 .unwrap();
16737
16738 let mut cx = EditorTestContext::for_editor(editor, cx).await;
16739 cx.run_until_parked();
16740
16741 cx.assert_editor_state(
16742 &"
16743 ˇaaa
16744 ccc
16745 ddd
16746
16747 ggg
16748 hhh
16749
16750
16751 lll
16752 mmm
16753 NNN
16754
16755 qqq
16756 rrr
16757
16758 uuu
16759 111
16760 222
16761 333
16762
16763 666
16764 777
16765
16766 000
16767 !!!"
16768 .unindent(),
16769 );
16770
16771 cx.update_editor(|editor, window, cx| {
16772 editor.select_all(&SelectAll, window, cx);
16773 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
16774 });
16775 cx.executor().run_until_parked();
16776
16777 cx.assert_state_with_diff(
16778 "
16779 «aaa
16780 - bbb
16781 ccc
16782 ddd
16783
16784 ggg
16785 hhh
16786
16787
16788 lll
16789 mmm
16790 - nnn
16791 + NNN
16792
16793 qqq
16794 rrr
16795
16796 uuu
16797 111
16798 222
16799 333
16800
16801 + 666
16802 777
16803
16804 000
16805 !!!ˇ»"
16806 .unindent(),
16807 );
16808}
16809
16810#[gpui::test]
16811async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
16812 init_test(cx, |_| {});
16813
16814 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
16815 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
16816
16817 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
16818 let multi_buffer = cx.new(|cx| {
16819 let mut multibuffer = MultiBuffer::new(ReadWrite);
16820 multibuffer.push_excerpts(
16821 buffer.clone(),
16822 [
16823 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
16824 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
16825 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
16826 ],
16827 cx,
16828 );
16829 multibuffer
16830 });
16831
16832 let editor =
16833 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
16834 editor
16835 .update(cx, |editor, _window, cx| {
16836 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
16837 editor
16838 .buffer
16839 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
16840 })
16841 .unwrap();
16842
16843 let mut cx = EditorTestContext::for_editor(editor, cx).await;
16844 cx.run_until_parked();
16845
16846 cx.update_editor(|editor, window, cx| {
16847 editor.expand_all_diff_hunks(&Default::default(), window, cx)
16848 });
16849 cx.executor().run_until_parked();
16850
16851 // When the start of a hunk coincides with the start of its excerpt,
16852 // the hunk is expanded. When the start of a a hunk is earlier than
16853 // the start of its excerpt, the hunk is not expanded.
16854 cx.assert_state_with_diff(
16855 "
16856 ˇaaa
16857 - bbb
16858 + BBB
16859
16860 - ddd
16861 - eee
16862 + DDD
16863 + EEE
16864 fff
16865
16866 iii
16867 "
16868 .unindent(),
16869 );
16870}
16871
16872#[gpui::test]
16873async fn test_edits_around_expanded_insertion_hunks(
16874 executor: BackgroundExecutor,
16875 cx: &mut TestAppContext,
16876) {
16877 init_test(cx, |_| {});
16878
16879 let mut cx = EditorTestContext::new(cx).await;
16880
16881 let diff_base = r#"
16882 use some::mod1;
16883 use some::mod2;
16884
16885 const A: u32 = 42;
16886
16887 fn main() {
16888 println!("hello");
16889
16890 println!("world");
16891 }
16892 "#
16893 .unindent();
16894 executor.run_until_parked();
16895 cx.set_state(
16896 &r#"
16897 use some::mod1;
16898 use some::mod2;
16899
16900 const A: u32 = 42;
16901 const B: u32 = 42;
16902 const C: u32 = 42;
16903 ˇ
16904
16905 fn main() {
16906 println!("hello");
16907
16908 println!("world");
16909 }
16910 "#
16911 .unindent(),
16912 );
16913
16914 cx.set_head_text(&diff_base);
16915 executor.run_until_parked();
16916
16917 cx.update_editor(|editor, window, cx| {
16918 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16919 });
16920 executor.run_until_parked();
16921
16922 cx.assert_state_with_diff(
16923 r#"
16924 use some::mod1;
16925 use some::mod2;
16926
16927 const A: u32 = 42;
16928 + const B: u32 = 42;
16929 + const C: u32 = 42;
16930 + ˇ
16931
16932 fn main() {
16933 println!("hello");
16934
16935 println!("world");
16936 }
16937 "#
16938 .unindent(),
16939 );
16940
16941 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
16942 executor.run_until_parked();
16943
16944 cx.assert_state_with_diff(
16945 r#"
16946 use some::mod1;
16947 use some::mod2;
16948
16949 const A: u32 = 42;
16950 + const B: u32 = 42;
16951 + const C: u32 = 42;
16952 + const D: u32 = 42;
16953 + ˇ
16954
16955 fn main() {
16956 println!("hello");
16957
16958 println!("world");
16959 }
16960 "#
16961 .unindent(),
16962 );
16963
16964 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
16965 executor.run_until_parked();
16966
16967 cx.assert_state_with_diff(
16968 r#"
16969 use some::mod1;
16970 use some::mod2;
16971
16972 const A: u32 = 42;
16973 + const B: u32 = 42;
16974 + const C: u32 = 42;
16975 + const D: u32 = 42;
16976 + const E: u32 = 42;
16977 + ˇ
16978
16979 fn main() {
16980 println!("hello");
16981
16982 println!("world");
16983 }
16984 "#
16985 .unindent(),
16986 );
16987
16988 cx.update_editor(|editor, window, cx| {
16989 editor.delete_line(&DeleteLine, window, cx);
16990 });
16991 executor.run_until_parked();
16992
16993 cx.assert_state_with_diff(
16994 r#"
16995 use some::mod1;
16996 use some::mod2;
16997
16998 const A: u32 = 42;
16999 + const B: u32 = 42;
17000 + const C: u32 = 42;
17001 + const D: u32 = 42;
17002 + const E: u32 = 42;
17003 ˇ
17004 fn main() {
17005 println!("hello");
17006
17007 println!("world");
17008 }
17009 "#
17010 .unindent(),
17011 );
17012
17013 cx.update_editor(|editor, window, cx| {
17014 editor.move_up(&MoveUp, window, cx);
17015 editor.delete_line(&DeleteLine, window, cx);
17016 editor.move_up(&MoveUp, window, cx);
17017 editor.delete_line(&DeleteLine, window, cx);
17018 editor.move_up(&MoveUp, window, cx);
17019 editor.delete_line(&DeleteLine, window, cx);
17020 });
17021 executor.run_until_parked();
17022 cx.assert_state_with_diff(
17023 r#"
17024 use some::mod1;
17025 use some::mod2;
17026
17027 const A: u32 = 42;
17028 + const B: u32 = 42;
17029 ˇ
17030 fn main() {
17031 println!("hello");
17032
17033 println!("world");
17034 }
17035 "#
17036 .unindent(),
17037 );
17038
17039 cx.update_editor(|editor, window, cx| {
17040 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
17041 editor.delete_line(&DeleteLine, window, cx);
17042 });
17043 executor.run_until_parked();
17044 cx.assert_state_with_diff(
17045 r#"
17046 ˇ
17047 fn main() {
17048 println!("hello");
17049
17050 println!("world");
17051 }
17052 "#
17053 .unindent(),
17054 );
17055}
17056
17057#[gpui::test]
17058async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
17059 init_test(cx, |_| {});
17060
17061 let mut cx = EditorTestContext::new(cx).await;
17062 cx.set_head_text(indoc! { "
17063 one
17064 two
17065 three
17066 four
17067 five
17068 "
17069 });
17070 cx.set_state(indoc! { "
17071 one
17072 ˇthree
17073 five
17074 "});
17075 cx.run_until_parked();
17076 cx.update_editor(|editor, window, cx| {
17077 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17078 });
17079 cx.assert_state_with_diff(
17080 indoc! { "
17081 one
17082 - two
17083 ˇthree
17084 - four
17085 five
17086 "}
17087 .to_string(),
17088 );
17089 cx.update_editor(|editor, window, cx| {
17090 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17091 });
17092
17093 cx.assert_state_with_diff(
17094 indoc! { "
17095 one
17096 ˇthree
17097 five
17098 "}
17099 .to_string(),
17100 );
17101
17102 cx.set_state(indoc! { "
17103 one
17104 ˇTWO
17105 three
17106 four
17107 five
17108 "});
17109 cx.run_until_parked();
17110 cx.update_editor(|editor, window, cx| {
17111 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17112 });
17113
17114 cx.assert_state_with_diff(
17115 indoc! { "
17116 one
17117 - two
17118 + ˇTWO
17119 three
17120 four
17121 five
17122 "}
17123 .to_string(),
17124 );
17125 cx.update_editor(|editor, window, cx| {
17126 editor.move_up(&Default::default(), window, cx);
17127 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17128 });
17129 cx.assert_state_with_diff(
17130 indoc! { "
17131 one
17132 ˇTWO
17133 three
17134 four
17135 five
17136 "}
17137 .to_string(),
17138 );
17139}
17140
17141#[gpui::test]
17142async fn test_edits_around_expanded_deletion_hunks(
17143 executor: BackgroundExecutor,
17144 cx: &mut TestAppContext,
17145) {
17146 init_test(cx, |_| {});
17147
17148 let mut cx = EditorTestContext::new(cx).await;
17149
17150 let diff_base = r#"
17151 use some::mod1;
17152 use some::mod2;
17153
17154 const A: u32 = 42;
17155 const B: u32 = 42;
17156 const C: u32 = 42;
17157
17158
17159 fn main() {
17160 println!("hello");
17161
17162 println!("world");
17163 }
17164 "#
17165 .unindent();
17166 executor.run_until_parked();
17167 cx.set_state(
17168 &r#"
17169 use some::mod1;
17170 use some::mod2;
17171
17172 ˇconst B: u32 = 42;
17173 const C: u32 = 42;
17174
17175
17176 fn main() {
17177 println!("hello");
17178
17179 println!("world");
17180 }
17181 "#
17182 .unindent(),
17183 );
17184
17185 cx.set_head_text(&diff_base);
17186 executor.run_until_parked();
17187
17188 cx.update_editor(|editor, window, cx| {
17189 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17190 });
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
17202
17203 fn main() {
17204 println!("hello");
17205
17206 println!("world");
17207 }
17208 "#
17209 .unindent(),
17210 );
17211
17212 cx.update_editor(|editor, window, cx| {
17213 editor.delete_line(&DeleteLine, window, cx);
17214 });
17215 executor.run_until_parked();
17216 cx.assert_state_with_diff(
17217 r#"
17218 use some::mod1;
17219 use some::mod2;
17220
17221 - const A: u32 = 42;
17222 - const B: u32 = 42;
17223 ˇconst C: u32 = 42;
17224
17225
17226 fn main() {
17227 println!("hello");
17228
17229 println!("world");
17230 }
17231 "#
17232 .unindent(),
17233 );
17234
17235 cx.update_editor(|editor, window, cx| {
17236 editor.delete_line(&DeleteLine, window, cx);
17237 });
17238 executor.run_until_parked();
17239 cx.assert_state_with_diff(
17240 r#"
17241 use some::mod1;
17242 use some::mod2;
17243
17244 - const A: u32 = 42;
17245 - const B: u32 = 42;
17246 - const C: u32 = 42;
17247 ˇ
17248
17249 fn main() {
17250 println!("hello");
17251
17252 println!("world");
17253 }
17254 "#
17255 .unindent(),
17256 );
17257
17258 cx.update_editor(|editor, window, cx| {
17259 editor.handle_input("replacement", window, cx);
17260 });
17261 executor.run_until_parked();
17262 cx.assert_state_with_diff(
17263 r#"
17264 use some::mod1;
17265 use some::mod2;
17266
17267 - const A: u32 = 42;
17268 - const B: u32 = 42;
17269 - const C: u32 = 42;
17270 -
17271 + replacementˇ
17272
17273 fn main() {
17274 println!("hello");
17275
17276 println!("world");
17277 }
17278 "#
17279 .unindent(),
17280 );
17281}
17282
17283#[gpui::test]
17284async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17285 init_test(cx, |_| {});
17286
17287 let mut cx = EditorTestContext::new(cx).await;
17288
17289 let base_text = r#"
17290 one
17291 two
17292 three
17293 four
17294 five
17295 "#
17296 .unindent();
17297 executor.run_until_parked();
17298 cx.set_state(
17299 &r#"
17300 one
17301 two
17302 fˇour
17303 five
17304 "#
17305 .unindent(),
17306 );
17307
17308 cx.set_head_text(&base_text);
17309 executor.run_until_parked();
17310
17311 cx.update_editor(|editor, window, cx| {
17312 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17313 });
17314 executor.run_until_parked();
17315
17316 cx.assert_state_with_diff(
17317 r#"
17318 one
17319 two
17320 - three
17321 fˇour
17322 five
17323 "#
17324 .unindent(),
17325 );
17326
17327 cx.update_editor(|editor, window, cx| {
17328 editor.backspace(&Backspace, window, cx);
17329 editor.backspace(&Backspace, window, cx);
17330 });
17331 executor.run_until_parked();
17332 cx.assert_state_with_diff(
17333 r#"
17334 one
17335 two
17336 - threeˇ
17337 - four
17338 + our
17339 five
17340 "#
17341 .unindent(),
17342 );
17343}
17344
17345#[gpui::test]
17346async fn test_edit_after_expanded_modification_hunk(
17347 executor: BackgroundExecutor,
17348 cx: &mut TestAppContext,
17349) {
17350 init_test(cx, |_| {});
17351
17352 let mut cx = EditorTestContext::new(cx).await;
17353
17354 let diff_base = r#"
17355 use some::mod1;
17356 use some::mod2;
17357
17358 const A: u32 = 42;
17359 const B: u32 = 42;
17360 const C: u32 = 42;
17361 const D: u32 = 42;
17362
17363
17364 fn main() {
17365 println!("hello");
17366
17367 println!("world");
17368 }"#
17369 .unindent();
17370
17371 cx.set_state(
17372 &r#"
17373 use some::mod1;
17374 use some::mod2;
17375
17376 const A: u32 = 42;
17377 const B: u32 = 42;
17378 const C: u32 = 43ˇ
17379 const D: u32 = 42;
17380
17381
17382 fn main() {
17383 println!("hello");
17384
17385 println!("world");
17386 }"#
17387 .unindent(),
17388 );
17389
17390 cx.set_head_text(&diff_base);
17391 executor.run_until_parked();
17392 cx.update_editor(|editor, window, cx| {
17393 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17394 });
17395 executor.run_until_parked();
17396
17397 cx.assert_state_with_diff(
17398 r#"
17399 use some::mod1;
17400 use some::mod2;
17401
17402 const A: u32 = 42;
17403 const B: u32 = 42;
17404 - const C: u32 = 42;
17405 + const C: u32 = 43ˇ
17406 const D: u32 = 42;
17407
17408
17409 fn main() {
17410 println!("hello");
17411
17412 println!("world");
17413 }"#
17414 .unindent(),
17415 );
17416
17417 cx.update_editor(|editor, window, cx| {
17418 editor.handle_input("\nnew_line\n", window, cx);
17419 });
17420 executor.run_until_parked();
17421
17422 cx.assert_state_with_diff(
17423 r#"
17424 use some::mod1;
17425 use some::mod2;
17426
17427 const A: u32 = 42;
17428 const B: u32 = 42;
17429 - const C: u32 = 42;
17430 + const C: u32 = 43
17431 + new_line
17432 + ˇ
17433 const D: u32 = 42;
17434
17435
17436 fn main() {
17437 println!("hello");
17438
17439 println!("world");
17440 }"#
17441 .unindent(),
17442 );
17443}
17444
17445#[gpui::test]
17446async fn test_stage_and_unstage_added_file_hunk(
17447 executor: BackgroundExecutor,
17448 cx: &mut TestAppContext,
17449) {
17450 init_test(cx, |_| {});
17451
17452 let mut cx = EditorTestContext::new(cx).await;
17453 cx.update_editor(|editor, _, cx| {
17454 editor.set_expand_all_diff_hunks(cx);
17455 });
17456
17457 let working_copy = r#"
17458 ˇfn main() {
17459 println!("hello, world!");
17460 }
17461 "#
17462 .unindent();
17463
17464 cx.set_state(&working_copy);
17465 executor.run_until_parked();
17466
17467 cx.assert_state_with_diff(
17468 r#"
17469 + ˇfn main() {
17470 + println!("hello, world!");
17471 + }
17472 "#
17473 .unindent(),
17474 );
17475 cx.assert_index_text(None);
17476
17477 cx.update_editor(|editor, window, cx| {
17478 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17479 });
17480 executor.run_until_parked();
17481 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
17482 cx.assert_state_with_diff(
17483 r#"
17484 + ˇfn main() {
17485 + println!("hello, world!");
17486 + }
17487 "#
17488 .unindent(),
17489 );
17490
17491 cx.update_editor(|editor, window, cx| {
17492 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17493 });
17494 executor.run_until_parked();
17495 cx.assert_index_text(None);
17496}
17497
17498async fn setup_indent_guides_editor(
17499 text: &str,
17500 cx: &mut TestAppContext,
17501) -> (BufferId, EditorTestContext) {
17502 init_test(cx, |_| {});
17503
17504 let mut cx = EditorTestContext::new(cx).await;
17505
17506 let buffer_id = cx.update_editor(|editor, window, cx| {
17507 editor.set_text(text, window, cx);
17508 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
17509
17510 buffer_ids[0]
17511 });
17512
17513 (buffer_id, cx)
17514}
17515
17516fn assert_indent_guides(
17517 range: Range<u32>,
17518 expected: Vec<IndentGuide>,
17519 active_indices: Option<Vec<usize>>,
17520 cx: &mut EditorTestContext,
17521) {
17522 let indent_guides = cx.update_editor(|editor, window, cx| {
17523 let snapshot = editor.snapshot(window, cx).display_snapshot;
17524 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
17525 editor,
17526 MultiBufferRow(range.start)..MultiBufferRow(range.end),
17527 true,
17528 &snapshot,
17529 cx,
17530 );
17531
17532 indent_guides.sort_by(|a, b| {
17533 a.depth.cmp(&b.depth).then(
17534 a.start_row
17535 .cmp(&b.start_row)
17536 .then(a.end_row.cmp(&b.end_row)),
17537 )
17538 });
17539 indent_guides
17540 });
17541
17542 if let Some(expected) = active_indices {
17543 let active_indices = cx.update_editor(|editor, window, cx| {
17544 let snapshot = editor.snapshot(window, cx).display_snapshot;
17545 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
17546 });
17547
17548 assert_eq!(
17549 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
17550 expected,
17551 "Active indent guide indices do not match"
17552 );
17553 }
17554
17555 assert_eq!(indent_guides, expected, "Indent guides do not match");
17556}
17557
17558fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
17559 IndentGuide {
17560 buffer_id,
17561 start_row: MultiBufferRow(start_row),
17562 end_row: MultiBufferRow(end_row),
17563 depth,
17564 tab_size: 4,
17565 settings: IndentGuideSettings {
17566 enabled: true,
17567 line_width: 1,
17568 active_line_width: 1,
17569 ..Default::default()
17570 },
17571 }
17572}
17573
17574#[gpui::test]
17575async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
17576 let (buffer_id, mut cx) = setup_indent_guides_editor(
17577 &"
17578 fn main() {
17579 let a = 1;
17580 }"
17581 .unindent(),
17582 cx,
17583 )
17584 .await;
17585
17586 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
17587}
17588
17589#[gpui::test]
17590async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
17591 let (buffer_id, mut cx) = setup_indent_guides_editor(
17592 &"
17593 fn main() {
17594 let a = 1;
17595 let b = 2;
17596 }"
17597 .unindent(),
17598 cx,
17599 )
17600 .await;
17601
17602 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
17603}
17604
17605#[gpui::test]
17606async fn test_indent_guide_nested(cx: &mut TestAppContext) {
17607 let (buffer_id, mut cx) = setup_indent_guides_editor(
17608 &"
17609 fn main() {
17610 let a = 1;
17611 if a == 3 {
17612 let b = 2;
17613 } else {
17614 let c = 3;
17615 }
17616 }"
17617 .unindent(),
17618 cx,
17619 )
17620 .await;
17621
17622 assert_indent_guides(
17623 0..8,
17624 vec![
17625 indent_guide(buffer_id, 1, 6, 0),
17626 indent_guide(buffer_id, 3, 3, 1),
17627 indent_guide(buffer_id, 5, 5, 1),
17628 ],
17629 None,
17630 &mut cx,
17631 );
17632}
17633
17634#[gpui::test]
17635async fn test_indent_guide_tab(cx: &mut TestAppContext) {
17636 let (buffer_id, mut cx) = setup_indent_guides_editor(
17637 &"
17638 fn main() {
17639 let a = 1;
17640 let b = 2;
17641 let c = 3;
17642 }"
17643 .unindent(),
17644 cx,
17645 )
17646 .await;
17647
17648 assert_indent_guides(
17649 0..5,
17650 vec![
17651 indent_guide(buffer_id, 1, 3, 0),
17652 indent_guide(buffer_id, 2, 2, 1),
17653 ],
17654 None,
17655 &mut cx,
17656 );
17657}
17658
17659#[gpui::test]
17660async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
17661 let (buffer_id, mut cx) = setup_indent_guides_editor(
17662 &"
17663 fn main() {
17664 let a = 1;
17665
17666 let c = 3;
17667 }"
17668 .unindent(),
17669 cx,
17670 )
17671 .await;
17672
17673 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
17674}
17675
17676#[gpui::test]
17677async fn test_indent_guide_complex(cx: &mut TestAppContext) {
17678 let (buffer_id, mut cx) = setup_indent_guides_editor(
17679 &"
17680 fn main() {
17681 let a = 1;
17682
17683 let c = 3;
17684
17685 if a == 3 {
17686 let b = 2;
17687 } else {
17688 let c = 3;
17689 }
17690 }"
17691 .unindent(),
17692 cx,
17693 )
17694 .await;
17695
17696 assert_indent_guides(
17697 0..11,
17698 vec![
17699 indent_guide(buffer_id, 1, 9, 0),
17700 indent_guide(buffer_id, 6, 6, 1),
17701 indent_guide(buffer_id, 8, 8, 1),
17702 ],
17703 None,
17704 &mut cx,
17705 );
17706}
17707
17708#[gpui::test]
17709async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
17710 let (buffer_id, mut cx) = setup_indent_guides_editor(
17711 &"
17712 fn main() {
17713 let a = 1;
17714
17715 let c = 3;
17716
17717 if a == 3 {
17718 let b = 2;
17719 } else {
17720 let c = 3;
17721 }
17722 }"
17723 .unindent(),
17724 cx,
17725 )
17726 .await;
17727
17728 assert_indent_guides(
17729 1..11,
17730 vec![
17731 indent_guide(buffer_id, 1, 9, 0),
17732 indent_guide(buffer_id, 6, 6, 1),
17733 indent_guide(buffer_id, 8, 8, 1),
17734 ],
17735 None,
17736 &mut cx,
17737 );
17738}
17739
17740#[gpui::test]
17741async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
17742 let (buffer_id, mut cx) = setup_indent_guides_editor(
17743 &"
17744 fn main() {
17745 let a = 1;
17746
17747 let c = 3;
17748
17749 if a == 3 {
17750 let b = 2;
17751 } else {
17752 let c = 3;
17753 }
17754 }"
17755 .unindent(),
17756 cx,
17757 )
17758 .await;
17759
17760 assert_indent_guides(
17761 1..10,
17762 vec![
17763 indent_guide(buffer_id, 1, 9, 0),
17764 indent_guide(buffer_id, 6, 6, 1),
17765 indent_guide(buffer_id, 8, 8, 1),
17766 ],
17767 None,
17768 &mut cx,
17769 );
17770}
17771
17772#[gpui::test]
17773async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
17774 let (buffer_id, mut cx) = setup_indent_guides_editor(
17775 &"
17776 fn main() {
17777 if a {
17778 b(
17779 c,
17780 d,
17781 )
17782 } else {
17783 e(
17784 f
17785 )
17786 }
17787 }"
17788 .unindent(),
17789 cx,
17790 )
17791 .await;
17792
17793 assert_indent_guides(
17794 0..11,
17795 vec![
17796 indent_guide(buffer_id, 1, 10, 0),
17797 indent_guide(buffer_id, 2, 5, 1),
17798 indent_guide(buffer_id, 7, 9, 1),
17799 indent_guide(buffer_id, 3, 4, 2),
17800 indent_guide(buffer_id, 8, 8, 2),
17801 ],
17802 None,
17803 &mut cx,
17804 );
17805
17806 cx.update_editor(|editor, window, cx| {
17807 editor.fold_at(MultiBufferRow(2), window, cx);
17808 assert_eq!(
17809 editor.display_text(cx),
17810 "
17811 fn main() {
17812 if a {
17813 b(⋯
17814 )
17815 } else {
17816 e(
17817 f
17818 )
17819 }
17820 }"
17821 .unindent()
17822 );
17823 });
17824
17825 assert_indent_guides(
17826 0..11,
17827 vec![
17828 indent_guide(buffer_id, 1, 10, 0),
17829 indent_guide(buffer_id, 2, 5, 1),
17830 indent_guide(buffer_id, 7, 9, 1),
17831 indent_guide(buffer_id, 8, 8, 2),
17832 ],
17833 None,
17834 &mut cx,
17835 );
17836}
17837
17838#[gpui::test]
17839async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
17840 let (buffer_id, mut cx) = setup_indent_guides_editor(
17841 &"
17842 block1
17843 block2
17844 block3
17845 block4
17846 block2
17847 block1
17848 block1"
17849 .unindent(),
17850 cx,
17851 )
17852 .await;
17853
17854 assert_indent_guides(
17855 1..10,
17856 vec![
17857 indent_guide(buffer_id, 1, 4, 0),
17858 indent_guide(buffer_id, 2, 3, 1),
17859 indent_guide(buffer_id, 3, 3, 2),
17860 ],
17861 None,
17862 &mut cx,
17863 );
17864}
17865
17866#[gpui::test]
17867async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
17868 let (buffer_id, mut cx) = setup_indent_guides_editor(
17869 &"
17870 block1
17871 block2
17872 block3
17873
17874 block1
17875 block1"
17876 .unindent(),
17877 cx,
17878 )
17879 .await;
17880
17881 assert_indent_guides(
17882 0..6,
17883 vec![
17884 indent_guide(buffer_id, 1, 2, 0),
17885 indent_guide(buffer_id, 2, 2, 1),
17886 ],
17887 None,
17888 &mut cx,
17889 );
17890}
17891
17892#[gpui::test]
17893async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
17894 let (buffer_id, mut cx) = setup_indent_guides_editor(
17895 &"
17896 function component() {
17897 \treturn (
17898 \t\t\t
17899 \t\t<div>
17900 \t\t\t<abc></abc>
17901 \t\t</div>
17902 \t)
17903 }"
17904 .unindent(),
17905 cx,
17906 )
17907 .await;
17908
17909 assert_indent_guides(
17910 0..8,
17911 vec![
17912 indent_guide(buffer_id, 1, 6, 0),
17913 indent_guide(buffer_id, 2, 5, 1),
17914 indent_guide(buffer_id, 4, 4, 2),
17915 ],
17916 None,
17917 &mut cx,
17918 );
17919}
17920
17921#[gpui::test]
17922async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
17923 let (buffer_id, mut cx) = setup_indent_guides_editor(
17924 &"
17925 function component() {
17926 \treturn (
17927 \t
17928 \t\t<div>
17929 \t\t\t<abc></abc>
17930 \t\t</div>
17931 \t)
17932 }"
17933 .unindent(),
17934 cx,
17935 )
17936 .await;
17937
17938 assert_indent_guides(
17939 0..8,
17940 vec![
17941 indent_guide(buffer_id, 1, 6, 0),
17942 indent_guide(buffer_id, 2, 5, 1),
17943 indent_guide(buffer_id, 4, 4, 2),
17944 ],
17945 None,
17946 &mut cx,
17947 );
17948}
17949
17950#[gpui::test]
17951async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
17952 let (buffer_id, mut cx) = setup_indent_guides_editor(
17953 &"
17954 block1
17955
17956
17957
17958 block2
17959 "
17960 .unindent(),
17961 cx,
17962 )
17963 .await;
17964
17965 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
17966}
17967
17968#[gpui::test]
17969async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
17970 let (buffer_id, mut cx) = setup_indent_guides_editor(
17971 &"
17972 def a:
17973 \tb = 3
17974 \tif True:
17975 \t\tc = 4
17976 \t\td = 5
17977 \tprint(b)
17978 "
17979 .unindent(),
17980 cx,
17981 )
17982 .await;
17983
17984 assert_indent_guides(
17985 0..6,
17986 vec![
17987 indent_guide(buffer_id, 1, 5, 0),
17988 indent_guide(buffer_id, 3, 4, 1),
17989 ],
17990 None,
17991 &mut cx,
17992 );
17993}
17994
17995#[gpui::test]
17996async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
17997 let (buffer_id, mut cx) = setup_indent_guides_editor(
17998 &"
17999 fn main() {
18000 let a = 1;
18001 }"
18002 .unindent(),
18003 cx,
18004 )
18005 .await;
18006
18007 cx.update_editor(|editor, window, cx| {
18008 editor.change_selections(None, window, cx, |s| {
18009 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18010 });
18011 });
18012
18013 assert_indent_guides(
18014 0..3,
18015 vec![indent_guide(buffer_id, 1, 1, 0)],
18016 Some(vec![0]),
18017 &mut cx,
18018 );
18019}
18020
18021#[gpui::test]
18022async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
18023 let (buffer_id, mut cx) = setup_indent_guides_editor(
18024 &"
18025 fn main() {
18026 if 1 == 2 {
18027 let a = 1;
18028 }
18029 }"
18030 .unindent(),
18031 cx,
18032 )
18033 .await;
18034
18035 cx.update_editor(|editor, window, cx| {
18036 editor.change_selections(None, window, cx, |s| {
18037 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18038 });
18039 });
18040
18041 assert_indent_guides(
18042 0..4,
18043 vec![
18044 indent_guide(buffer_id, 1, 3, 0),
18045 indent_guide(buffer_id, 2, 2, 1),
18046 ],
18047 Some(vec![1]),
18048 &mut cx,
18049 );
18050
18051 cx.update_editor(|editor, window, cx| {
18052 editor.change_selections(None, window, cx, |s| {
18053 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
18054 });
18055 });
18056
18057 assert_indent_guides(
18058 0..4,
18059 vec![
18060 indent_guide(buffer_id, 1, 3, 0),
18061 indent_guide(buffer_id, 2, 2, 1),
18062 ],
18063 Some(vec![1]),
18064 &mut cx,
18065 );
18066
18067 cx.update_editor(|editor, window, cx| {
18068 editor.change_selections(None, window, cx, |s| {
18069 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
18070 });
18071 });
18072
18073 assert_indent_guides(
18074 0..4,
18075 vec![
18076 indent_guide(buffer_id, 1, 3, 0),
18077 indent_guide(buffer_id, 2, 2, 1),
18078 ],
18079 Some(vec![0]),
18080 &mut cx,
18081 );
18082}
18083
18084#[gpui::test]
18085async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
18086 let (buffer_id, mut cx) = setup_indent_guides_editor(
18087 &"
18088 fn main() {
18089 let a = 1;
18090
18091 let b = 2;
18092 }"
18093 .unindent(),
18094 cx,
18095 )
18096 .await;
18097
18098 cx.update_editor(|editor, window, cx| {
18099 editor.change_selections(None, window, cx, |s| {
18100 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
18101 });
18102 });
18103
18104 assert_indent_guides(
18105 0..5,
18106 vec![indent_guide(buffer_id, 1, 3, 0)],
18107 Some(vec![0]),
18108 &mut cx,
18109 );
18110}
18111
18112#[gpui::test]
18113async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
18114 let (buffer_id, mut cx) = setup_indent_guides_editor(
18115 &"
18116 def m:
18117 a = 1
18118 pass"
18119 .unindent(),
18120 cx,
18121 )
18122 .await;
18123
18124 cx.update_editor(|editor, window, cx| {
18125 editor.change_selections(None, window, cx, |s| {
18126 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18127 });
18128 });
18129
18130 assert_indent_guides(
18131 0..3,
18132 vec![indent_guide(buffer_id, 1, 2, 0)],
18133 Some(vec![0]),
18134 &mut cx,
18135 );
18136}
18137
18138#[gpui::test]
18139async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
18140 init_test(cx, |_| {});
18141 let mut cx = EditorTestContext::new(cx).await;
18142 let text = indoc! {
18143 "
18144 impl A {
18145 fn b() {
18146 0;
18147 3;
18148 5;
18149 6;
18150 7;
18151 }
18152 }
18153 "
18154 };
18155 let base_text = indoc! {
18156 "
18157 impl A {
18158 fn b() {
18159 0;
18160 1;
18161 2;
18162 3;
18163 4;
18164 }
18165 fn c() {
18166 5;
18167 6;
18168 7;
18169 }
18170 }
18171 "
18172 };
18173
18174 cx.update_editor(|editor, window, cx| {
18175 editor.set_text(text, window, cx);
18176
18177 editor.buffer().update(cx, |multibuffer, cx| {
18178 let buffer = multibuffer.as_singleton().unwrap();
18179 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
18180
18181 multibuffer.set_all_diff_hunks_expanded(cx);
18182 multibuffer.add_diff(diff, cx);
18183
18184 buffer.read(cx).remote_id()
18185 })
18186 });
18187 cx.run_until_parked();
18188
18189 cx.assert_state_with_diff(
18190 indoc! { "
18191 impl A {
18192 fn b() {
18193 0;
18194 - 1;
18195 - 2;
18196 3;
18197 - 4;
18198 - }
18199 - fn c() {
18200 5;
18201 6;
18202 7;
18203 }
18204 }
18205 ˇ"
18206 }
18207 .to_string(),
18208 );
18209
18210 let mut actual_guides = cx.update_editor(|editor, window, cx| {
18211 editor
18212 .snapshot(window, cx)
18213 .buffer_snapshot
18214 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
18215 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
18216 .collect::<Vec<_>>()
18217 });
18218 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
18219 assert_eq!(
18220 actual_guides,
18221 vec![
18222 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
18223 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
18224 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
18225 ]
18226 );
18227}
18228
18229#[gpui::test]
18230async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
18231 init_test(cx, |_| {});
18232 let mut cx = EditorTestContext::new(cx).await;
18233
18234 let diff_base = r#"
18235 a
18236 b
18237 c
18238 "#
18239 .unindent();
18240
18241 cx.set_state(
18242 &r#"
18243 ˇA
18244 b
18245 C
18246 "#
18247 .unindent(),
18248 );
18249 cx.set_head_text(&diff_base);
18250 cx.update_editor(|editor, window, cx| {
18251 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18252 });
18253 executor.run_until_parked();
18254
18255 let both_hunks_expanded = r#"
18256 - a
18257 + ˇA
18258 b
18259 - c
18260 + C
18261 "#
18262 .unindent();
18263
18264 cx.assert_state_with_diff(both_hunks_expanded.clone());
18265
18266 let hunk_ranges = cx.update_editor(|editor, window, cx| {
18267 let snapshot = editor.snapshot(window, cx);
18268 let hunks = editor
18269 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18270 .collect::<Vec<_>>();
18271 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
18272 let buffer_id = hunks[0].buffer_id;
18273 hunks
18274 .into_iter()
18275 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
18276 .collect::<Vec<_>>()
18277 });
18278 assert_eq!(hunk_ranges.len(), 2);
18279
18280 cx.update_editor(|editor, _, cx| {
18281 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18282 });
18283 executor.run_until_parked();
18284
18285 let second_hunk_expanded = r#"
18286 ˇA
18287 b
18288 - c
18289 + C
18290 "#
18291 .unindent();
18292
18293 cx.assert_state_with_diff(second_hunk_expanded);
18294
18295 cx.update_editor(|editor, _, cx| {
18296 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18297 });
18298 executor.run_until_parked();
18299
18300 cx.assert_state_with_diff(both_hunks_expanded.clone());
18301
18302 cx.update_editor(|editor, _, cx| {
18303 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
18304 });
18305 executor.run_until_parked();
18306
18307 let first_hunk_expanded = r#"
18308 - a
18309 + ˇA
18310 b
18311 C
18312 "#
18313 .unindent();
18314
18315 cx.assert_state_with_diff(first_hunk_expanded);
18316
18317 cx.update_editor(|editor, _, cx| {
18318 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
18319 });
18320 executor.run_until_parked();
18321
18322 cx.assert_state_with_diff(both_hunks_expanded);
18323
18324 cx.set_state(
18325 &r#"
18326 ˇA
18327 b
18328 "#
18329 .unindent(),
18330 );
18331 cx.run_until_parked();
18332
18333 // TODO this cursor position seems bad
18334 cx.assert_state_with_diff(
18335 r#"
18336 - ˇa
18337 + A
18338 b
18339 "#
18340 .unindent(),
18341 );
18342
18343 cx.update_editor(|editor, window, cx| {
18344 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18345 });
18346
18347 cx.assert_state_with_diff(
18348 r#"
18349 - ˇa
18350 + A
18351 b
18352 - c
18353 "#
18354 .unindent(),
18355 );
18356
18357 let hunk_ranges = cx.update_editor(|editor, window, cx| {
18358 let snapshot = editor.snapshot(window, cx);
18359 let hunks = editor
18360 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18361 .collect::<Vec<_>>();
18362 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
18363 let buffer_id = hunks[0].buffer_id;
18364 hunks
18365 .into_iter()
18366 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
18367 .collect::<Vec<_>>()
18368 });
18369 assert_eq!(hunk_ranges.len(), 2);
18370
18371 cx.update_editor(|editor, _, cx| {
18372 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
18373 });
18374 executor.run_until_parked();
18375
18376 cx.assert_state_with_diff(
18377 r#"
18378 - ˇa
18379 + A
18380 b
18381 "#
18382 .unindent(),
18383 );
18384}
18385
18386#[gpui::test]
18387async fn test_toggle_deletion_hunk_at_start_of_file(
18388 executor: BackgroundExecutor,
18389 cx: &mut TestAppContext,
18390) {
18391 init_test(cx, |_| {});
18392 let mut cx = EditorTestContext::new(cx).await;
18393
18394 let diff_base = r#"
18395 a
18396 b
18397 c
18398 "#
18399 .unindent();
18400
18401 cx.set_state(
18402 &r#"
18403 ˇb
18404 c
18405 "#
18406 .unindent(),
18407 );
18408 cx.set_head_text(&diff_base);
18409 cx.update_editor(|editor, window, cx| {
18410 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18411 });
18412 executor.run_until_parked();
18413
18414 let hunk_expanded = r#"
18415 - a
18416 ˇb
18417 c
18418 "#
18419 .unindent();
18420
18421 cx.assert_state_with_diff(hunk_expanded.clone());
18422
18423 let hunk_ranges = cx.update_editor(|editor, window, cx| {
18424 let snapshot = editor.snapshot(window, cx);
18425 let hunks = editor
18426 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18427 .collect::<Vec<_>>();
18428 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
18429 let buffer_id = hunks[0].buffer_id;
18430 hunks
18431 .into_iter()
18432 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
18433 .collect::<Vec<_>>()
18434 });
18435 assert_eq!(hunk_ranges.len(), 1);
18436
18437 cx.update_editor(|editor, _, cx| {
18438 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18439 });
18440 executor.run_until_parked();
18441
18442 let hunk_collapsed = r#"
18443 ˇb
18444 c
18445 "#
18446 .unindent();
18447
18448 cx.assert_state_with_diff(hunk_collapsed);
18449
18450 cx.update_editor(|editor, _, cx| {
18451 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18452 });
18453 executor.run_until_parked();
18454
18455 cx.assert_state_with_diff(hunk_expanded.clone());
18456}
18457
18458#[gpui::test]
18459async fn test_display_diff_hunks(cx: &mut TestAppContext) {
18460 init_test(cx, |_| {});
18461
18462 let fs = FakeFs::new(cx.executor());
18463 fs.insert_tree(
18464 path!("/test"),
18465 json!({
18466 ".git": {},
18467 "file-1": "ONE\n",
18468 "file-2": "TWO\n",
18469 "file-3": "THREE\n",
18470 }),
18471 )
18472 .await;
18473
18474 fs.set_head_for_repo(
18475 path!("/test/.git").as_ref(),
18476 &[
18477 ("file-1".into(), "one\n".into()),
18478 ("file-2".into(), "two\n".into()),
18479 ("file-3".into(), "three\n".into()),
18480 ],
18481 "deadbeef",
18482 );
18483
18484 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
18485 let mut buffers = vec![];
18486 for i in 1..=3 {
18487 let buffer = project
18488 .update(cx, |project, cx| {
18489 let path = format!(path!("/test/file-{}"), i);
18490 project.open_local_buffer(path, cx)
18491 })
18492 .await
18493 .unwrap();
18494 buffers.push(buffer);
18495 }
18496
18497 let multibuffer = cx.new(|cx| {
18498 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
18499 multibuffer.set_all_diff_hunks_expanded(cx);
18500 for buffer in &buffers {
18501 let snapshot = buffer.read(cx).snapshot();
18502 multibuffer.set_excerpts_for_path(
18503 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
18504 buffer.clone(),
18505 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
18506 DEFAULT_MULTIBUFFER_CONTEXT,
18507 cx,
18508 );
18509 }
18510 multibuffer
18511 });
18512
18513 let editor = cx.add_window(|window, cx| {
18514 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
18515 });
18516 cx.run_until_parked();
18517
18518 let snapshot = editor
18519 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
18520 .unwrap();
18521 let hunks = snapshot
18522 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
18523 .map(|hunk| match hunk {
18524 DisplayDiffHunk::Unfolded {
18525 display_row_range, ..
18526 } => display_row_range,
18527 DisplayDiffHunk::Folded { .. } => unreachable!(),
18528 })
18529 .collect::<Vec<_>>();
18530 assert_eq!(
18531 hunks,
18532 [
18533 DisplayRow(2)..DisplayRow(4),
18534 DisplayRow(7)..DisplayRow(9),
18535 DisplayRow(12)..DisplayRow(14),
18536 ]
18537 );
18538}
18539
18540#[gpui::test]
18541async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
18542 init_test(cx, |_| {});
18543
18544 let mut cx = EditorTestContext::new(cx).await;
18545 cx.set_head_text(indoc! { "
18546 one
18547 two
18548 three
18549 four
18550 five
18551 "
18552 });
18553 cx.set_index_text(indoc! { "
18554 one
18555 two
18556 three
18557 four
18558 five
18559 "
18560 });
18561 cx.set_state(indoc! {"
18562 one
18563 TWO
18564 ˇTHREE
18565 FOUR
18566 five
18567 "});
18568 cx.run_until_parked();
18569 cx.update_editor(|editor, window, cx| {
18570 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18571 });
18572 cx.run_until_parked();
18573 cx.assert_index_text(Some(indoc! {"
18574 one
18575 TWO
18576 THREE
18577 FOUR
18578 five
18579 "}));
18580 cx.set_state(indoc! { "
18581 one
18582 TWO
18583 ˇTHREE-HUNDRED
18584 FOUR
18585 five
18586 "});
18587 cx.run_until_parked();
18588 cx.update_editor(|editor, window, cx| {
18589 let snapshot = editor.snapshot(window, cx);
18590 let hunks = editor
18591 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18592 .collect::<Vec<_>>();
18593 assert_eq!(hunks.len(), 1);
18594 assert_eq!(
18595 hunks[0].status(),
18596 DiffHunkStatus {
18597 kind: DiffHunkStatusKind::Modified,
18598 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
18599 }
18600 );
18601
18602 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18603 });
18604 cx.run_until_parked();
18605 cx.assert_index_text(Some(indoc! {"
18606 one
18607 TWO
18608 THREE-HUNDRED
18609 FOUR
18610 five
18611 "}));
18612}
18613
18614#[gpui::test]
18615fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
18616 init_test(cx, |_| {});
18617
18618 let editor = cx.add_window(|window, cx| {
18619 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
18620 build_editor(buffer, window, cx)
18621 });
18622
18623 let render_args = Arc::new(Mutex::new(None));
18624 let snapshot = editor
18625 .update(cx, |editor, window, cx| {
18626 let snapshot = editor.buffer().read(cx).snapshot(cx);
18627 let range =
18628 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
18629
18630 struct RenderArgs {
18631 row: MultiBufferRow,
18632 folded: bool,
18633 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
18634 }
18635
18636 let crease = Crease::inline(
18637 range,
18638 FoldPlaceholder::test(),
18639 {
18640 let toggle_callback = render_args.clone();
18641 move |row, folded, callback, _window, _cx| {
18642 *toggle_callback.lock() = Some(RenderArgs {
18643 row,
18644 folded,
18645 callback,
18646 });
18647 div()
18648 }
18649 },
18650 |_row, _folded, _window, _cx| div(),
18651 );
18652
18653 editor.insert_creases(Some(crease), cx);
18654 let snapshot = editor.snapshot(window, cx);
18655 let _div = snapshot.render_crease_toggle(
18656 MultiBufferRow(1),
18657 false,
18658 cx.entity().clone(),
18659 window,
18660 cx,
18661 );
18662 snapshot
18663 })
18664 .unwrap();
18665
18666 let render_args = render_args.lock().take().unwrap();
18667 assert_eq!(render_args.row, MultiBufferRow(1));
18668 assert!(!render_args.folded);
18669 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
18670
18671 cx.update_window(*editor, |_, window, cx| {
18672 (render_args.callback)(true, window, cx)
18673 })
18674 .unwrap();
18675 let snapshot = editor
18676 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
18677 .unwrap();
18678 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
18679
18680 cx.update_window(*editor, |_, window, cx| {
18681 (render_args.callback)(false, window, cx)
18682 })
18683 .unwrap();
18684 let snapshot = editor
18685 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
18686 .unwrap();
18687 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
18688}
18689
18690#[gpui::test]
18691async fn test_input_text(cx: &mut TestAppContext) {
18692 init_test(cx, |_| {});
18693 let mut cx = EditorTestContext::new(cx).await;
18694
18695 cx.set_state(
18696 &r#"ˇone
18697 two
18698
18699 three
18700 fourˇ
18701 five
18702
18703 siˇx"#
18704 .unindent(),
18705 );
18706
18707 cx.dispatch_action(HandleInput(String::new()));
18708 cx.assert_editor_state(
18709 &r#"ˇone
18710 two
18711
18712 three
18713 fourˇ
18714 five
18715
18716 siˇx"#
18717 .unindent(),
18718 );
18719
18720 cx.dispatch_action(HandleInput("AAAA".to_string()));
18721 cx.assert_editor_state(
18722 &r#"AAAAˇone
18723 two
18724
18725 three
18726 fourAAAAˇ
18727 five
18728
18729 siAAAAˇx"#
18730 .unindent(),
18731 );
18732}
18733
18734#[gpui::test]
18735async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
18736 init_test(cx, |_| {});
18737
18738 let mut cx = EditorTestContext::new(cx).await;
18739 cx.set_state(
18740 r#"let foo = 1;
18741let foo = 2;
18742let foo = 3;
18743let fooˇ = 4;
18744let foo = 5;
18745let foo = 6;
18746let foo = 7;
18747let foo = 8;
18748let foo = 9;
18749let foo = 10;
18750let foo = 11;
18751let foo = 12;
18752let foo = 13;
18753let foo = 14;
18754let foo = 15;"#,
18755 );
18756
18757 cx.update_editor(|e, window, cx| {
18758 assert_eq!(
18759 e.next_scroll_position,
18760 NextScrollCursorCenterTopBottom::Center,
18761 "Default next scroll direction is center",
18762 );
18763
18764 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
18765 assert_eq!(
18766 e.next_scroll_position,
18767 NextScrollCursorCenterTopBottom::Top,
18768 "After center, next scroll direction should be top",
18769 );
18770
18771 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
18772 assert_eq!(
18773 e.next_scroll_position,
18774 NextScrollCursorCenterTopBottom::Bottom,
18775 "After top, next scroll direction should be bottom",
18776 );
18777
18778 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
18779 assert_eq!(
18780 e.next_scroll_position,
18781 NextScrollCursorCenterTopBottom::Center,
18782 "After bottom, scrolling should start over",
18783 );
18784
18785 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
18786 assert_eq!(
18787 e.next_scroll_position,
18788 NextScrollCursorCenterTopBottom::Top,
18789 "Scrolling continues if retriggered fast enough"
18790 );
18791 });
18792
18793 cx.executor()
18794 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
18795 cx.executor().run_until_parked();
18796 cx.update_editor(|e, _, _| {
18797 assert_eq!(
18798 e.next_scroll_position,
18799 NextScrollCursorCenterTopBottom::Center,
18800 "If scrolling is not triggered fast enough, it should reset"
18801 );
18802 });
18803}
18804
18805#[gpui::test]
18806async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
18807 init_test(cx, |_| {});
18808 let mut cx = EditorLspTestContext::new_rust(
18809 lsp::ServerCapabilities {
18810 definition_provider: Some(lsp::OneOf::Left(true)),
18811 references_provider: Some(lsp::OneOf::Left(true)),
18812 ..lsp::ServerCapabilities::default()
18813 },
18814 cx,
18815 )
18816 .await;
18817
18818 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
18819 let go_to_definition = cx
18820 .lsp
18821 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
18822 move |params, _| async move {
18823 if empty_go_to_definition {
18824 Ok(None)
18825 } else {
18826 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
18827 uri: params.text_document_position_params.text_document.uri,
18828 range: lsp::Range::new(
18829 lsp::Position::new(4, 3),
18830 lsp::Position::new(4, 6),
18831 ),
18832 })))
18833 }
18834 },
18835 );
18836 let references = cx
18837 .lsp
18838 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
18839 Ok(Some(vec![lsp::Location {
18840 uri: params.text_document_position.text_document.uri,
18841 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
18842 }]))
18843 });
18844 (go_to_definition, references)
18845 };
18846
18847 cx.set_state(
18848 &r#"fn one() {
18849 let mut a = ˇtwo();
18850 }
18851
18852 fn two() {}"#
18853 .unindent(),
18854 );
18855 set_up_lsp_handlers(false, &mut cx);
18856 let navigated = cx
18857 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
18858 .await
18859 .expect("Failed to navigate to definition");
18860 assert_eq!(
18861 navigated,
18862 Navigated::Yes,
18863 "Should have navigated to definition from the GetDefinition response"
18864 );
18865 cx.assert_editor_state(
18866 &r#"fn one() {
18867 let mut a = two();
18868 }
18869
18870 fn «twoˇ»() {}"#
18871 .unindent(),
18872 );
18873
18874 let editors = cx.update_workspace(|workspace, _, cx| {
18875 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
18876 });
18877 cx.update_editor(|_, _, test_editor_cx| {
18878 assert_eq!(
18879 editors.len(),
18880 1,
18881 "Initially, only one, test, editor should be open in the workspace"
18882 );
18883 assert_eq!(
18884 test_editor_cx.entity(),
18885 editors.last().expect("Asserted len is 1").clone()
18886 );
18887 });
18888
18889 set_up_lsp_handlers(true, &mut cx);
18890 let navigated = cx
18891 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
18892 .await
18893 .expect("Failed to navigate to lookup references");
18894 assert_eq!(
18895 navigated,
18896 Navigated::Yes,
18897 "Should have navigated to references as a fallback after empty GoToDefinition response"
18898 );
18899 // We should not change the selections in the existing file,
18900 // if opening another milti buffer with the references
18901 cx.assert_editor_state(
18902 &r#"fn one() {
18903 let mut a = two();
18904 }
18905
18906 fn «twoˇ»() {}"#
18907 .unindent(),
18908 );
18909 let editors = cx.update_workspace(|workspace, _, cx| {
18910 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
18911 });
18912 cx.update_editor(|_, _, test_editor_cx| {
18913 assert_eq!(
18914 editors.len(),
18915 2,
18916 "After falling back to references search, we open a new editor with the results"
18917 );
18918 let references_fallback_text = editors
18919 .into_iter()
18920 .find(|new_editor| *new_editor != test_editor_cx.entity())
18921 .expect("Should have one non-test editor now")
18922 .read(test_editor_cx)
18923 .text(test_editor_cx);
18924 assert_eq!(
18925 references_fallback_text, "fn one() {\n let mut a = two();\n}",
18926 "Should use the range from the references response and not the GoToDefinition one"
18927 );
18928 });
18929}
18930
18931#[gpui::test]
18932async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
18933 init_test(cx, |_| {});
18934 cx.update(|cx| {
18935 let mut editor_settings = EditorSettings::get_global(cx).clone();
18936 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
18937 EditorSettings::override_global(editor_settings, cx);
18938 });
18939 let mut cx = EditorLspTestContext::new_rust(
18940 lsp::ServerCapabilities {
18941 definition_provider: Some(lsp::OneOf::Left(true)),
18942 references_provider: Some(lsp::OneOf::Left(true)),
18943 ..lsp::ServerCapabilities::default()
18944 },
18945 cx,
18946 )
18947 .await;
18948 let original_state = r#"fn one() {
18949 let mut a = ˇtwo();
18950 }
18951
18952 fn two() {}"#
18953 .unindent();
18954 cx.set_state(&original_state);
18955
18956 let mut go_to_definition = cx
18957 .lsp
18958 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
18959 move |_, _| async move { Ok(None) },
18960 );
18961 let _references = cx
18962 .lsp
18963 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
18964 panic!("Should not call for references with no go to definition fallback")
18965 });
18966
18967 let navigated = cx
18968 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
18969 .await
18970 .expect("Failed to navigate to lookup references");
18971 go_to_definition
18972 .next()
18973 .await
18974 .expect("Should have called the go_to_definition handler");
18975
18976 assert_eq!(
18977 navigated,
18978 Navigated::No,
18979 "Should have navigated to references as a fallback after empty GoToDefinition response"
18980 );
18981 cx.assert_editor_state(&original_state);
18982 let editors = cx.update_workspace(|workspace, _, cx| {
18983 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
18984 });
18985 cx.update_editor(|_, _, _| {
18986 assert_eq!(
18987 editors.len(),
18988 1,
18989 "After unsuccessful fallback, no other editor should have been opened"
18990 );
18991 });
18992}
18993
18994#[gpui::test]
18995async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
18996 init_test(cx, |_| {});
18997
18998 let language = Arc::new(Language::new(
18999 LanguageConfig::default(),
19000 Some(tree_sitter_rust::LANGUAGE.into()),
19001 ));
19002
19003 let text = r#"
19004 #[cfg(test)]
19005 mod tests() {
19006 #[test]
19007 fn runnable_1() {
19008 let a = 1;
19009 }
19010
19011 #[test]
19012 fn runnable_2() {
19013 let a = 1;
19014 let b = 2;
19015 }
19016 }
19017 "#
19018 .unindent();
19019
19020 let fs = FakeFs::new(cx.executor());
19021 fs.insert_file("/file.rs", Default::default()).await;
19022
19023 let project = Project::test(fs, ["/a".as_ref()], cx).await;
19024 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19025 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19026 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
19027 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
19028
19029 let editor = cx.new_window_entity(|window, cx| {
19030 Editor::new(
19031 EditorMode::full(),
19032 multi_buffer,
19033 Some(project.clone()),
19034 window,
19035 cx,
19036 )
19037 });
19038
19039 editor.update_in(cx, |editor, window, cx| {
19040 let snapshot = editor.buffer().read(cx).snapshot(cx);
19041 editor.tasks.insert(
19042 (buffer.read(cx).remote_id(), 3),
19043 RunnableTasks {
19044 templates: vec![],
19045 offset: snapshot.anchor_before(43),
19046 column: 0,
19047 extra_variables: HashMap::default(),
19048 context_range: BufferOffset(43)..BufferOffset(85),
19049 },
19050 );
19051 editor.tasks.insert(
19052 (buffer.read(cx).remote_id(), 8),
19053 RunnableTasks {
19054 templates: vec![],
19055 offset: snapshot.anchor_before(86),
19056 column: 0,
19057 extra_variables: HashMap::default(),
19058 context_range: BufferOffset(86)..BufferOffset(191),
19059 },
19060 );
19061
19062 // Test finding task when cursor is inside function body
19063 editor.change_selections(None, window, cx, |s| {
19064 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
19065 });
19066 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
19067 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
19068
19069 // Test finding task when cursor is on function name
19070 editor.change_selections(None, window, cx, |s| {
19071 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
19072 });
19073 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
19074 assert_eq!(row, 8, "Should find task when cursor is on function name");
19075 });
19076}
19077
19078#[gpui::test]
19079async fn test_folding_buffers(cx: &mut TestAppContext) {
19080 init_test(cx, |_| {});
19081
19082 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
19083 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
19084 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
19085
19086 let fs = FakeFs::new(cx.executor());
19087 fs.insert_tree(
19088 path!("/a"),
19089 json!({
19090 "first.rs": sample_text_1,
19091 "second.rs": sample_text_2,
19092 "third.rs": sample_text_3,
19093 }),
19094 )
19095 .await;
19096 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19097 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19098 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19099 let worktree = project.update(cx, |project, cx| {
19100 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
19101 assert_eq!(worktrees.len(), 1);
19102 worktrees.pop().unwrap()
19103 });
19104 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
19105
19106 let buffer_1 = project
19107 .update(cx, |project, cx| {
19108 project.open_buffer((worktree_id, "first.rs"), cx)
19109 })
19110 .await
19111 .unwrap();
19112 let buffer_2 = project
19113 .update(cx, |project, cx| {
19114 project.open_buffer((worktree_id, "second.rs"), cx)
19115 })
19116 .await
19117 .unwrap();
19118 let buffer_3 = project
19119 .update(cx, |project, cx| {
19120 project.open_buffer((worktree_id, "third.rs"), cx)
19121 })
19122 .await
19123 .unwrap();
19124
19125 let multi_buffer = cx.new(|cx| {
19126 let mut multi_buffer = MultiBuffer::new(ReadWrite);
19127 multi_buffer.push_excerpts(
19128 buffer_1.clone(),
19129 [
19130 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19131 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19132 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19133 ],
19134 cx,
19135 );
19136 multi_buffer.push_excerpts(
19137 buffer_2.clone(),
19138 [
19139 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19140 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19141 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19142 ],
19143 cx,
19144 );
19145 multi_buffer.push_excerpts(
19146 buffer_3.clone(),
19147 [
19148 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19149 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19150 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19151 ],
19152 cx,
19153 );
19154 multi_buffer
19155 });
19156 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19157 Editor::new(
19158 EditorMode::full(),
19159 multi_buffer.clone(),
19160 Some(project.clone()),
19161 window,
19162 cx,
19163 )
19164 });
19165
19166 assert_eq!(
19167 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19168 "\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",
19169 );
19170
19171 multi_buffer_editor.update(cx, |editor, cx| {
19172 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
19173 });
19174 assert_eq!(
19175 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19176 "\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",
19177 "After folding the first buffer, its text should not be displayed"
19178 );
19179
19180 multi_buffer_editor.update(cx, |editor, cx| {
19181 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
19182 });
19183 assert_eq!(
19184 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19185 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
19186 "After folding the second buffer, its text should not be displayed"
19187 );
19188
19189 multi_buffer_editor.update(cx, |editor, cx| {
19190 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
19191 });
19192 assert_eq!(
19193 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19194 "\n\n\n\n\n",
19195 "After folding the third buffer, its text should not be displayed"
19196 );
19197
19198 // Emulate selection inside the fold logic, that should work
19199 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19200 editor
19201 .snapshot(window, cx)
19202 .next_line_boundary(Point::new(0, 4));
19203 });
19204
19205 multi_buffer_editor.update(cx, |editor, cx| {
19206 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
19207 });
19208 assert_eq!(
19209 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19210 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
19211 "After unfolding the second buffer, its text should be displayed"
19212 );
19213
19214 // Typing inside of buffer 1 causes that buffer to be unfolded.
19215 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19216 assert_eq!(
19217 multi_buffer
19218 .read(cx)
19219 .snapshot(cx)
19220 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
19221 .collect::<String>(),
19222 "bbbb"
19223 );
19224 editor.change_selections(None, window, cx, |selections| {
19225 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
19226 });
19227 editor.handle_input("B", window, cx);
19228 });
19229
19230 assert_eq!(
19231 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19232 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
19233 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
19234 );
19235
19236 multi_buffer_editor.update(cx, |editor, cx| {
19237 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
19238 });
19239 assert_eq!(
19240 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19241 "\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",
19242 "After unfolding the all buffers, all original text should be displayed"
19243 );
19244}
19245
19246#[gpui::test]
19247async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
19248 init_test(cx, |_| {});
19249
19250 let sample_text_1 = "1111\n2222\n3333".to_string();
19251 let sample_text_2 = "4444\n5555\n6666".to_string();
19252 let sample_text_3 = "7777\n8888\n9999".to_string();
19253
19254 let fs = FakeFs::new(cx.executor());
19255 fs.insert_tree(
19256 path!("/a"),
19257 json!({
19258 "first.rs": sample_text_1,
19259 "second.rs": sample_text_2,
19260 "third.rs": sample_text_3,
19261 }),
19262 )
19263 .await;
19264 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19265 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19266 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19267 let worktree = project.update(cx, |project, cx| {
19268 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
19269 assert_eq!(worktrees.len(), 1);
19270 worktrees.pop().unwrap()
19271 });
19272 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
19273
19274 let buffer_1 = project
19275 .update(cx, |project, cx| {
19276 project.open_buffer((worktree_id, "first.rs"), cx)
19277 })
19278 .await
19279 .unwrap();
19280 let buffer_2 = project
19281 .update(cx, |project, cx| {
19282 project.open_buffer((worktree_id, "second.rs"), cx)
19283 })
19284 .await
19285 .unwrap();
19286 let buffer_3 = project
19287 .update(cx, |project, cx| {
19288 project.open_buffer((worktree_id, "third.rs"), cx)
19289 })
19290 .await
19291 .unwrap();
19292
19293 let multi_buffer = cx.new(|cx| {
19294 let mut multi_buffer = MultiBuffer::new(ReadWrite);
19295 multi_buffer.push_excerpts(
19296 buffer_1.clone(),
19297 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19298 cx,
19299 );
19300 multi_buffer.push_excerpts(
19301 buffer_2.clone(),
19302 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19303 cx,
19304 );
19305 multi_buffer.push_excerpts(
19306 buffer_3.clone(),
19307 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19308 cx,
19309 );
19310 multi_buffer
19311 });
19312
19313 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19314 Editor::new(
19315 EditorMode::full(),
19316 multi_buffer,
19317 Some(project.clone()),
19318 window,
19319 cx,
19320 )
19321 });
19322
19323 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
19324 assert_eq!(
19325 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19326 full_text,
19327 );
19328
19329 multi_buffer_editor.update(cx, |editor, cx| {
19330 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
19331 });
19332 assert_eq!(
19333 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19334 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
19335 "After folding the first buffer, its text should not be displayed"
19336 );
19337
19338 multi_buffer_editor.update(cx, |editor, cx| {
19339 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
19340 });
19341
19342 assert_eq!(
19343 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19344 "\n\n\n\n\n\n7777\n8888\n9999",
19345 "After folding the second buffer, its text should not be displayed"
19346 );
19347
19348 multi_buffer_editor.update(cx, |editor, cx| {
19349 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
19350 });
19351 assert_eq!(
19352 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19353 "\n\n\n\n\n",
19354 "After folding the third buffer, its text should not be displayed"
19355 );
19356
19357 multi_buffer_editor.update(cx, |editor, cx| {
19358 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
19359 });
19360 assert_eq!(
19361 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19362 "\n\n\n\n4444\n5555\n6666\n\n",
19363 "After unfolding the second buffer, its text should be displayed"
19364 );
19365
19366 multi_buffer_editor.update(cx, |editor, cx| {
19367 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
19368 });
19369 assert_eq!(
19370 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19371 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
19372 "After unfolding the first buffer, its text should be displayed"
19373 );
19374
19375 multi_buffer_editor.update(cx, |editor, cx| {
19376 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
19377 });
19378 assert_eq!(
19379 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19380 full_text,
19381 "After unfolding all buffers, all original text should be displayed"
19382 );
19383}
19384
19385#[gpui::test]
19386async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
19387 init_test(cx, |_| {});
19388
19389 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
19390
19391 let fs = FakeFs::new(cx.executor());
19392 fs.insert_tree(
19393 path!("/a"),
19394 json!({
19395 "main.rs": sample_text,
19396 }),
19397 )
19398 .await;
19399 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19400 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19401 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19402 let worktree = project.update(cx, |project, cx| {
19403 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
19404 assert_eq!(worktrees.len(), 1);
19405 worktrees.pop().unwrap()
19406 });
19407 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
19408
19409 let buffer_1 = project
19410 .update(cx, |project, cx| {
19411 project.open_buffer((worktree_id, "main.rs"), cx)
19412 })
19413 .await
19414 .unwrap();
19415
19416 let multi_buffer = cx.new(|cx| {
19417 let mut multi_buffer = MultiBuffer::new(ReadWrite);
19418 multi_buffer.push_excerpts(
19419 buffer_1.clone(),
19420 [ExcerptRange::new(
19421 Point::new(0, 0)
19422 ..Point::new(
19423 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
19424 0,
19425 ),
19426 )],
19427 cx,
19428 );
19429 multi_buffer
19430 });
19431 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19432 Editor::new(
19433 EditorMode::full(),
19434 multi_buffer,
19435 Some(project.clone()),
19436 window,
19437 cx,
19438 )
19439 });
19440
19441 let selection_range = Point::new(1, 0)..Point::new(2, 0);
19442 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19443 enum TestHighlight {}
19444 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
19445 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
19446 editor.highlight_text::<TestHighlight>(
19447 vec![highlight_range.clone()],
19448 HighlightStyle::color(Hsla::green()),
19449 cx,
19450 );
19451 editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
19452 });
19453
19454 let full_text = format!("\n\n{sample_text}");
19455 assert_eq!(
19456 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19457 full_text,
19458 );
19459}
19460
19461#[gpui::test]
19462async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
19463 init_test(cx, |_| {});
19464 cx.update(|cx| {
19465 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
19466 "keymaps/default-linux.json",
19467 cx,
19468 )
19469 .unwrap();
19470 cx.bind_keys(default_key_bindings);
19471 });
19472
19473 let (editor, cx) = cx.add_window_view(|window, cx| {
19474 let multi_buffer = MultiBuffer::build_multi(
19475 [
19476 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
19477 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
19478 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
19479 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
19480 ],
19481 cx,
19482 );
19483 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
19484
19485 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
19486 // fold all but the second buffer, so that we test navigating between two
19487 // adjacent folded buffers, as well as folded buffers at the start and
19488 // end the multibuffer
19489 editor.fold_buffer(buffer_ids[0], cx);
19490 editor.fold_buffer(buffer_ids[2], cx);
19491 editor.fold_buffer(buffer_ids[3], cx);
19492
19493 editor
19494 });
19495 cx.simulate_resize(size(px(1000.), px(1000.)));
19496
19497 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
19498 cx.assert_excerpts_with_selections(indoc! {"
19499 [EXCERPT]
19500 ˇ[FOLDED]
19501 [EXCERPT]
19502 a1
19503 b1
19504 [EXCERPT]
19505 [FOLDED]
19506 [EXCERPT]
19507 [FOLDED]
19508 "
19509 });
19510 cx.simulate_keystroke("down");
19511 cx.assert_excerpts_with_selections(indoc! {"
19512 [EXCERPT]
19513 [FOLDED]
19514 [EXCERPT]
19515 ˇa1
19516 b1
19517 [EXCERPT]
19518 [FOLDED]
19519 [EXCERPT]
19520 [FOLDED]
19521 "
19522 });
19523 cx.simulate_keystroke("down");
19524 cx.assert_excerpts_with_selections(indoc! {"
19525 [EXCERPT]
19526 [FOLDED]
19527 [EXCERPT]
19528 a1
19529 ˇb1
19530 [EXCERPT]
19531 [FOLDED]
19532 [EXCERPT]
19533 [FOLDED]
19534 "
19535 });
19536 cx.simulate_keystroke("down");
19537 cx.assert_excerpts_with_selections(indoc! {"
19538 [EXCERPT]
19539 [FOLDED]
19540 [EXCERPT]
19541 a1
19542 b1
19543 ˇ[EXCERPT]
19544 [FOLDED]
19545 [EXCERPT]
19546 [FOLDED]
19547 "
19548 });
19549 cx.simulate_keystroke("down");
19550 cx.assert_excerpts_with_selections(indoc! {"
19551 [EXCERPT]
19552 [FOLDED]
19553 [EXCERPT]
19554 a1
19555 b1
19556 [EXCERPT]
19557 ˇ[FOLDED]
19558 [EXCERPT]
19559 [FOLDED]
19560 "
19561 });
19562 for _ in 0..5 {
19563 cx.simulate_keystroke("down");
19564 cx.assert_excerpts_with_selections(indoc! {"
19565 [EXCERPT]
19566 [FOLDED]
19567 [EXCERPT]
19568 a1
19569 b1
19570 [EXCERPT]
19571 [FOLDED]
19572 [EXCERPT]
19573 ˇ[FOLDED]
19574 "
19575 });
19576 }
19577
19578 cx.simulate_keystroke("up");
19579 cx.assert_excerpts_with_selections(indoc! {"
19580 [EXCERPT]
19581 [FOLDED]
19582 [EXCERPT]
19583 a1
19584 b1
19585 [EXCERPT]
19586 ˇ[FOLDED]
19587 [EXCERPT]
19588 [FOLDED]
19589 "
19590 });
19591 cx.simulate_keystroke("up");
19592 cx.assert_excerpts_with_selections(indoc! {"
19593 [EXCERPT]
19594 [FOLDED]
19595 [EXCERPT]
19596 a1
19597 b1
19598 ˇ[EXCERPT]
19599 [FOLDED]
19600 [EXCERPT]
19601 [FOLDED]
19602 "
19603 });
19604 cx.simulate_keystroke("up");
19605 cx.assert_excerpts_with_selections(indoc! {"
19606 [EXCERPT]
19607 [FOLDED]
19608 [EXCERPT]
19609 a1
19610 ˇb1
19611 [EXCERPT]
19612 [FOLDED]
19613 [EXCERPT]
19614 [FOLDED]
19615 "
19616 });
19617 cx.simulate_keystroke("up");
19618 cx.assert_excerpts_with_selections(indoc! {"
19619 [EXCERPT]
19620 [FOLDED]
19621 [EXCERPT]
19622 ˇa1
19623 b1
19624 [EXCERPT]
19625 [FOLDED]
19626 [EXCERPT]
19627 [FOLDED]
19628 "
19629 });
19630 for _ in 0..5 {
19631 cx.simulate_keystroke("up");
19632 cx.assert_excerpts_with_selections(indoc! {"
19633 [EXCERPT]
19634 ˇ[FOLDED]
19635 [EXCERPT]
19636 a1
19637 b1
19638 [EXCERPT]
19639 [FOLDED]
19640 [EXCERPT]
19641 [FOLDED]
19642 "
19643 });
19644 }
19645}
19646
19647#[gpui::test]
19648async fn test_inline_completion_text(cx: &mut TestAppContext) {
19649 init_test(cx, |_| {});
19650
19651 // Simple insertion
19652 assert_highlighted_edits(
19653 "Hello, world!",
19654 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
19655 true,
19656 cx,
19657 |highlighted_edits, cx| {
19658 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
19659 assert_eq!(highlighted_edits.highlights.len(), 1);
19660 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
19661 assert_eq!(
19662 highlighted_edits.highlights[0].1.background_color,
19663 Some(cx.theme().status().created_background)
19664 );
19665 },
19666 )
19667 .await;
19668
19669 // Replacement
19670 assert_highlighted_edits(
19671 "This is a test.",
19672 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
19673 false,
19674 cx,
19675 |highlighted_edits, cx| {
19676 assert_eq!(highlighted_edits.text, "That is a test.");
19677 assert_eq!(highlighted_edits.highlights.len(), 1);
19678 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
19679 assert_eq!(
19680 highlighted_edits.highlights[0].1.background_color,
19681 Some(cx.theme().status().created_background)
19682 );
19683 },
19684 )
19685 .await;
19686
19687 // Multiple edits
19688 assert_highlighted_edits(
19689 "Hello, world!",
19690 vec![
19691 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
19692 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
19693 ],
19694 false,
19695 cx,
19696 |highlighted_edits, cx| {
19697 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
19698 assert_eq!(highlighted_edits.highlights.len(), 2);
19699 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
19700 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
19701 assert_eq!(
19702 highlighted_edits.highlights[0].1.background_color,
19703 Some(cx.theme().status().created_background)
19704 );
19705 assert_eq!(
19706 highlighted_edits.highlights[1].1.background_color,
19707 Some(cx.theme().status().created_background)
19708 );
19709 },
19710 )
19711 .await;
19712
19713 // Multiple lines with edits
19714 assert_highlighted_edits(
19715 "First line\nSecond line\nThird line\nFourth line",
19716 vec![
19717 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
19718 (
19719 Point::new(2, 0)..Point::new(2, 10),
19720 "New third line".to_string(),
19721 ),
19722 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
19723 ],
19724 false,
19725 cx,
19726 |highlighted_edits, cx| {
19727 assert_eq!(
19728 highlighted_edits.text,
19729 "Second modified\nNew third line\nFourth updated line"
19730 );
19731 assert_eq!(highlighted_edits.highlights.len(), 3);
19732 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
19733 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
19734 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
19735 for highlight in &highlighted_edits.highlights {
19736 assert_eq!(
19737 highlight.1.background_color,
19738 Some(cx.theme().status().created_background)
19739 );
19740 }
19741 },
19742 )
19743 .await;
19744}
19745
19746#[gpui::test]
19747async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
19748 init_test(cx, |_| {});
19749
19750 // Deletion
19751 assert_highlighted_edits(
19752 "Hello, world!",
19753 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
19754 true,
19755 cx,
19756 |highlighted_edits, cx| {
19757 assert_eq!(highlighted_edits.text, "Hello, world!");
19758 assert_eq!(highlighted_edits.highlights.len(), 1);
19759 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
19760 assert_eq!(
19761 highlighted_edits.highlights[0].1.background_color,
19762 Some(cx.theme().status().deleted_background)
19763 );
19764 },
19765 )
19766 .await;
19767
19768 // Insertion
19769 assert_highlighted_edits(
19770 "Hello, world!",
19771 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
19772 true,
19773 cx,
19774 |highlighted_edits, cx| {
19775 assert_eq!(highlighted_edits.highlights.len(), 1);
19776 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
19777 assert_eq!(
19778 highlighted_edits.highlights[0].1.background_color,
19779 Some(cx.theme().status().created_background)
19780 );
19781 },
19782 )
19783 .await;
19784}
19785
19786async fn assert_highlighted_edits(
19787 text: &str,
19788 edits: Vec<(Range<Point>, String)>,
19789 include_deletions: bool,
19790 cx: &mut TestAppContext,
19791 assertion_fn: impl Fn(HighlightedText, &App),
19792) {
19793 let window = cx.add_window(|window, cx| {
19794 let buffer = MultiBuffer::build_simple(text, cx);
19795 Editor::new(EditorMode::full(), buffer, None, window, cx)
19796 });
19797 let cx = &mut VisualTestContext::from_window(*window, cx);
19798
19799 let (buffer, snapshot) = window
19800 .update(cx, |editor, _window, cx| {
19801 (
19802 editor.buffer().clone(),
19803 editor.buffer().read(cx).snapshot(cx),
19804 )
19805 })
19806 .unwrap();
19807
19808 let edits = edits
19809 .into_iter()
19810 .map(|(range, edit)| {
19811 (
19812 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
19813 edit,
19814 )
19815 })
19816 .collect::<Vec<_>>();
19817
19818 let text_anchor_edits = edits
19819 .clone()
19820 .into_iter()
19821 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
19822 .collect::<Vec<_>>();
19823
19824 let edit_preview = window
19825 .update(cx, |_, _window, cx| {
19826 buffer
19827 .read(cx)
19828 .as_singleton()
19829 .unwrap()
19830 .read(cx)
19831 .preview_edits(text_anchor_edits.into(), cx)
19832 })
19833 .unwrap()
19834 .await;
19835
19836 cx.update(|_window, cx| {
19837 let highlighted_edits = inline_completion_edit_text(
19838 &snapshot.as_singleton().unwrap().2,
19839 &edits,
19840 &edit_preview,
19841 include_deletions,
19842 cx,
19843 );
19844 assertion_fn(highlighted_edits, cx)
19845 });
19846}
19847
19848#[track_caller]
19849fn assert_breakpoint(
19850 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
19851 path: &Arc<Path>,
19852 expected: Vec<(u32, Breakpoint)>,
19853) {
19854 if expected.len() == 0usize {
19855 assert!(!breakpoints.contains_key(path), "{}", path.display());
19856 } else {
19857 let mut breakpoint = breakpoints
19858 .get(path)
19859 .unwrap()
19860 .into_iter()
19861 .map(|breakpoint| {
19862 (
19863 breakpoint.row,
19864 Breakpoint {
19865 message: breakpoint.message.clone(),
19866 state: breakpoint.state,
19867 condition: breakpoint.condition.clone(),
19868 hit_condition: breakpoint.hit_condition.clone(),
19869 },
19870 )
19871 })
19872 .collect::<Vec<_>>();
19873
19874 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
19875
19876 assert_eq!(expected, breakpoint);
19877 }
19878}
19879
19880fn add_log_breakpoint_at_cursor(
19881 editor: &mut Editor,
19882 log_message: &str,
19883 window: &mut Window,
19884 cx: &mut Context<Editor>,
19885) {
19886 let (anchor, bp) = editor
19887 .breakpoints_at_cursors(window, cx)
19888 .first()
19889 .and_then(|(anchor, bp)| {
19890 if let Some(bp) = bp {
19891 Some((*anchor, bp.clone()))
19892 } else {
19893 None
19894 }
19895 })
19896 .unwrap_or_else(|| {
19897 let cursor_position: Point = editor.selections.newest(cx).head();
19898
19899 let breakpoint_position = editor
19900 .snapshot(window, cx)
19901 .display_snapshot
19902 .buffer_snapshot
19903 .anchor_before(Point::new(cursor_position.row, 0));
19904
19905 (breakpoint_position, Breakpoint::new_log(&log_message))
19906 });
19907
19908 editor.edit_breakpoint_at_anchor(
19909 anchor,
19910 bp,
19911 BreakpointEditAction::EditLogMessage(log_message.into()),
19912 cx,
19913 );
19914}
19915
19916#[gpui::test]
19917async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
19918 init_test(cx, |_| {});
19919
19920 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
19921 let fs = FakeFs::new(cx.executor());
19922 fs.insert_tree(
19923 path!("/a"),
19924 json!({
19925 "main.rs": sample_text,
19926 }),
19927 )
19928 .await;
19929 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19930 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19931 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19932
19933 let fs = FakeFs::new(cx.executor());
19934 fs.insert_tree(
19935 path!("/a"),
19936 json!({
19937 "main.rs": sample_text,
19938 }),
19939 )
19940 .await;
19941 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19942 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19943 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19944 let worktree_id = workspace
19945 .update(cx, |workspace, _window, cx| {
19946 workspace.project().update(cx, |project, cx| {
19947 project.worktrees(cx).next().unwrap().read(cx).id()
19948 })
19949 })
19950 .unwrap();
19951
19952 let buffer = project
19953 .update(cx, |project, cx| {
19954 project.open_buffer((worktree_id, "main.rs"), cx)
19955 })
19956 .await
19957 .unwrap();
19958
19959 let (editor, cx) = cx.add_window_view(|window, cx| {
19960 Editor::new(
19961 EditorMode::full(),
19962 MultiBuffer::build_from_buffer(buffer, cx),
19963 Some(project.clone()),
19964 window,
19965 cx,
19966 )
19967 });
19968
19969 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
19970 let abs_path = project.read_with(cx, |project, cx| {
19971 project
19972 .absolute_path(&project_path, cx)
19973 .map(|path_buf| Arc::from(path_buf.to_owned()))
19974 .unwrap()
19975 });
19976
19977 // assert we can add breakpoint on the first line
19978 editor.update_in(cx, |editor, window, cx| {
19979 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19980 editor.move_to_end(&MoveToEnd, window, cx);
19981 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19982 });
19983
19984 let breakpoints = editor.update(cx, |editor, cx| {
19985 editor
19986 .breakpoint_store()
19987 .as_ref()
19988 .unwrap()
19989 .read(cx)
19990 .all_source_breakpoints(cx)
19991 .clone()
19992 });
19993
19994 assert_eq!(1, breakpoints.len());
19995 assert_breakpoint(
19996 &breakpoints,
19997 &abs_path,
19998 vec![
19999 (0, Breakpoint::new_standard()),
20000 (3, Breakpoint::new_standard()),
20001 ],
20002 );
20003
20004 editor.update_in(cx, |editor, window, cx| {
20005 editor.move_to_beginning(&MoveToBeginning, window, cx);
20006 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20007 });
20008
20009 let breakpoints = editor.update(cx, |editor, cx| {
20010 editor
20011 .breakpoint_store()
20012 .as_ref()
20013 .unwrap()
20014 .read(cx)
20015 .all_source_breakpoints(cx)
20016 .clone()
20017 });
20018
20019 assert_eq!(1, breakpoints.len());
20020 assert_breakpoint(
20021 &breakpoints,
20022 &abs_path,
20023 vec![(3, Breakpoint::new_standard())],
20024 );
20025
20026 editor.update_in(cx, |editor, window, cx| {
20027 editor.move_to_end(&MoveToEnd, window, cx);
20028 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20029 });
20030
20031 let breakpoints = editor.update(cx, |editor, cx| {
20032 editor
20033 .breakpoint_store()
20034 .as_ref()
20035 .unwrap()
20036 .read(cx)
20037 .all_source_breakpoints(cx)
20038 .clone()
20039 });
20040
20041 assert_eq!(0, breakpoints.len());
20042 assert_breakpoint(&breakpoints, &abs_path, vec![]);
20043}
20044
20045#[gpui::test]
20046async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
20047 init_test(cx, |_| {});
20048
20049 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20050
20051 let fs = FakeFs::new(cx.executor());
20052 fs.insert_tree(
20053 path!("/a"),
20054 json!({
20055 "main.rs": sample_text,
20056 }),
20057 )
20058 .await;
20059 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20060 let (workspace, cx) =
20061 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20062
20063 let worktree_id = workspace.update(cx, |workspace, cx| {
20064 workspace.project().update(cx, |project, cx| {
20065 project.worktrees(cx).next().unwrap().read(cx).id()
20066 })
20067 });
20068
20069 let buffer = project
20070 .update(cx, |project, cx| {
20071 project.open_buffer((worktree_id, "main.rs"), cx)
20072 })
20073 .await
20074 .unwrap();
20075
20076 let (editor, cx) = cx.add_window_view(|window, cx| {
20077 Editor::new(
20078 EditorMode::full(),
20079 MultiBuffer::build_from_buffer(buffer, cx),
20080 Some(project.clone()),
20081 window,
20082 cx,
20083 )
20084 });
20085
20086 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20087 let abs_path = project.read_with(cx, |project, cx| {
20088 project
20089 .absolute_path(&project_path, cx)
20090 .map(|path_buf| Arc::from(path_buf.to_owned()))
20091 .unwrap()
20092 });
20093
20094 editor.update_in(cx, |editor, window, cx| {
20095 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
20096 });
20097
20098 let breakpoints = editor.update(cx, |editor, cx| {
20099 editor
20100 .breakpoint_store()
20101 .as_ref()
20102 .unwrap()
20103 .read(cx)
20104 .all_source_breakpoints(cx)
20105 .clone()
20106 });
20107
20108 assert_breakpoint(
20109 &breakpoints,
20110 &abs_path,
20111 vec![(0, Breakpoint::new_log("hello world"))],
20112 );
20113
20114 // Removing a log message from a log breakpoint should remove it
20115 editor.update_in(cx, |editor, window, cx| {
20116 add_log_breakpoint_at_cursor(editor, "", window, cx);
20117 });
20118
20119 let breakpoints = editor.update(cx, |editor, cx| {
20120 editor
20121 .breakpoint_store()
20122 .as_ref()
20123 .unwrap()
20124 .read(cx)
20125 .all_source_breakpoints(cx)
20126 .clone()
20127 });
20128
20129 assert_breakpoint(&breakpoints, &abs_path, vec![]);
20130
20131 editor.update_in(cx, |editor, window, cx| {
20132 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20133 editor.move_to_end(&MoveToEnd, window, cx);
20134 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20135 // Not adding a log message to a standard breakpoint shouldn't remove it
20136 add_log_breakpoint_at_cursor(editor, "", window, cx);
20137 });
20138
20139 let breakpoints = editor.update(cx, |editor, cx| {
20140 editor
20141 .breakpoint_store()
20142 .as_ref()
20143 .unwrap()
20144 .read(cx)
20145 .all_source_breakpoints(cx)
20146 .clone()
20147 });
20148
20149 assert_breakpoint(
20150 &breakpoints,
20151 &abs_path,
20152 vec![
20153 (0, Breakpoint::new_standard()),
20154 (3, Breakpoint::new_standard()),
20155 ],
20156 );
20157
20158 editor.update_in(cx, |editor, window, cx| {
20159 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
20160 });
20161
20162 let breakpoints = editor.update(cx, |editor, cx| {
20163 editor
20164 .breakpoint_store()
20165 .as_ref()
20166 .unwrap()
20167 .read(cx)
20168 .all_source_breakpoints(cx)
20169 .clone()
20170 });
20171
20172 assert_breakpoint(
20173 &breakpoints,
20174 &abs_path,
20175 vec![
20176 (0, Breakpoint::new_standard()),
20177 (3, Breakpoint::new_log("hello world")),
20178 ],
20179 );
20180
20181 editor.update_in(cx, |editor, window, cx| {
20182 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
20183 });
20184
20185 let breakpoints = editor.update(cx, |editor, cx| {
20186 editor
20187 .breakpoint_store()
20188 .as_ref()
20189 .unwrap()
20190 .read(cx)
20191 .all_source_breakpoints(cx)
20192 .clone()
20193 });
20194
20195 assert_breakpoint(
20196 &breakpoints,
20197 &abs_path,
20198 vec![
20199 (0, Breakpoint::new_standard()),
20200 (3, Breakpoint::new_log("hello Earth!!")),
20201 ],
20202 );
20203}
20204
20205/// This also tests that Editor::breakpoint_at_cursor_head is working properly
20206/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
20207/// or when breakpoints were placed out of order. This tests for a regression too
20208#[gpui::test]
20209async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
20210 init_test(cx, |_| {});
20211
20212 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20213 let fs = FakeFs::new(cx.executor());
20214 fs.insert_tree(
20215 path!("/a"),
20216 json!({
20217 "main.rs": sample_text,
20218 }),
20219 )
20220 .await;
20221 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20222 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20223 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20224
20225 let fs = FakeFs::new(cx.executor());
20226 fs.insert_tree(
20227 path!("/a"),
20228 json!({
20229 "main.rs": sample_text,
20230 }),
20231 )
20232 .await;
20233 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20234 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20235 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20236 let worktree_id = workspace
20237 .update(cx, |workspace, _window, cx| {
20238 workspace.project().update(cx, |project, cx| {
20239 project.worktrees(cx).next().unwrap().read(cx).id()
20240 })
20241 })
20242 .unwrap();
20243
20244 let buffer = project
20245 .update(cx, |project, cx| {
20246 project.open_buffer((worktree_id, "main.rs"), cx)
20247 })
20248 .await
20249 .unwrap();
20250
20251 let (editor, cx) = cx.add_window_view(|window, cx| {
20252 Editor::new(
20253 EditorMode::full(),
20254 MultiBuffer::build_from_buffer(buffer, cx),
20255 Some(project.clone()),
20256 window,
20257 cx,
20258 )
20259 });
20260
20261 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20262 let abs_path = project.read_with(cx, |project, cx| {
20263 project
20264 .absolute_path(&project_path, cx)
20265 .map(|path_buf| Arc::from(path_buf.to_owned()))
20266 .unwrap()
20267 });
20268
20269 // assert we can add breakpoint on the first line
20270 editor.update_in(cx, |editor, window, cx| {
20271 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20272 editor.move_to_end(&MoveToEnd, window, cx);
20273 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20274 editor.move_up(&MoveUp, window, cx);
20275 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20276 });
20277
20278 let breakpoints = editor.update(cx, |editor, cx| {
20279 editor
20280 .breakpoint_store()
20281 .as_ref()
20282 .unwrap()
20283 .read(cx)
20284 .all_source_breakpoints(cx)
20285 .clone()
20286 });
20287
20288 assert_eq!(1, breakpoints.len());
20289 assert_breakpoint(
20290 &breakpoints,
20291 &abs_path,
20292 vec![
20293 (0, Breakpoint::new_standard()),
20294 (2, Breakpoint::new_standard()),
20295 (3, Breakpoint::new_standard()),
20296 ],
20297 );
20298
20299 editor.update_in(cx, |editor, window, cx| {
20300 editor.move_to_beginning(&MoveToBeginning, window, cx);
20301 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20302 editor.move_to_end(&MoveToEnd, window, cx);
20303 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20304 // Disabling a breakpoint that doesn't exist should do nothing
20305 editor.move_up(&MoveUp, window, cx);
20306 editor.move_up(&MoveUp, window, cx);
20307 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20308 });
20309
20310 let breakpoints = editor.update(cx, |editor, cx| {
20311 editor
20312 .breakpoint_store()
20313 .as_ref()
20314 .unwrap()
20315 .read(cx)
20316 .all_source_breakpoints(cx)
20317 .clone()
20318 });
20319
20320 let disable_breakpoint = {
20321 let mut bp = Breakpoint::new_standard();
20322 bp.state = BreakpointState::Disabled;
20323 bp
20324 };
20325
20326 assert_eq!(1, breakpoints.len());
20327 assert_breakpoint(
20328 &breakpoints,
20329 &abs_path,
20330 vec![
20331 (0, disable_breakpoint.clone()),
20332 (2, Breakpoint::new_standard()),
20333 (3, disable_breakpoint.clone()),
20334 ],
20335 );
20336
20337 editor.update_in(cx, |editor, window, cx| {
20338 editor.move_to_beginning(&MoveToBeginning, window, cx);
20339 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
20340 editor.move_to_end(&MoveToEnd, window, cx);
20341 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
20342 editor.move_up(&MoveUp, window, cx);
20343 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20344 });
20345
20346 let breakpoints = editor.update(cx, |editor, cx| {
20347 editor
20348 .breakpoint_store()
20349 .as_ref()
20350 .unwrap()
20351 .read(cx)
20352 .all_source_breakpoints(cx)
20353 .clone()
20354 });
20355
20356 assert_eq!(1, breakpoints.len());
20357 assert_breakpoint(
20358 &breakpoints,
20359 &abs_path,
20360 vec![
20361 (0, Breakpoint::new_standard()),
20362 (2, disable_breakpoint),
20363 (3, Breakpoint::new_standard()),
20364 ],
20365 );
20366}
20367
20368#[gpui::test]
20369async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
20370 init_test(cx, |_| {});
20371 let capabilities = lsp::ServerCapabilities {
20372 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
20373 prepare_provider: Some(true),
20374 work_done_progress_options: Default::default(),
20375 })),
20376 ..Default::default()
20377 };
20378 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
20379
20380 cx.set_state(indoc! {"
20381 struct Fˇoo {}
20382 "});
20383
20384 cx.update_editor(|editor, _, cx| {
20385 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
20386 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
20387 editor.highlight_background::<DocumentHighlightRead>(
20388 &[highlight_range],
20389 |theme| theme.colors().editor_document_highlight_read_background,
20390 cx,
20391 );
20392 });
20393
20394 let mut prepare_rename_handler = cx
20395 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
20396 move |_, _, _| async move {
20397 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
20398 start: lsp::Position {
20399 line: 0,
20400 character: 7,
20401 },
20402 end: lsp::Position {
20403 line: 0,
20404 character: 10,
20405 },
20406 })))
20407 },
20408 );
20409 let prepare_rename_task = cx
20410 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
20411 .expect("Prepare rename was not started");
20412 prepare_rename_handler.next().await.unwrap();
20413 prepare_rename_task.await.expect("Prepare rename failed");
20414
20415 let mut rename_handler =
20416 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
20417 let edit = lsp::TextEdit {
20418 range: lsp::Range {
20419 start: lsp::Position {
20420 line: 0,
20421 character: 7,
20422 },
20423 end: lsp::Position {
20424 line: 0,
20425 character: 10,
20426 },
20427 },
20428 new_text: "FooRenamed".to_string(),
20429 };
20430 Ok(Some(lsp::WorkspaceEdit::new(
20431 // Specify the same edit twice
20432 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
20433 )))
20434 });
20435 let rename_task = cx
20436 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
20437 .expect("Confirm rename was not started");
20438 rename_handler.next().await.unwrap();
20439 rename_task.await.expect("Confirm rename failed");
20440 cx.run_until_parked();
20441
20442 // Despite two edits, only one is actually applied as those are identical
20443 cx.assert_editor_state(indoc! {"
20444 struct FooRenamedˇ {}
20445 "});
20446}
20447
20448#[gpui::test]
20449async fn test_rename_without_prepare(cx: &mut TestAppContext) {
20450 init_test(cx, |_| {});
20451 // These capabilities indicate that the server does not support prepare rename.
20452 let capabilities = lsp::ServerCapabilities {
20453 rename_provider: Some(lsp::OneOf::Left(true)),
20454 ..Default::default()
20455 };
20456 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
20457
20458 cx.set_state(indoc! {"
20459 struct Fˇoo {}
20460 "});
20461
20462 cx.update_editor(|editor, _window, cx| {
20463 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
20464 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
20465 editor.highlight_background::<DocumentHighlightRead>(
20466 &[highlight_range],
20467 |theme| theme.colors().editor_document_highlight_read_background,
20468 cx,
20469 );
20470 });
20471
20472 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
20473 .expect("Prepare rename was not started")
20474 .await
20475 .expect("Prepare rename failed");
20476
20477 let mut rename_handler =
20478 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
20479 let edit = lsp::TextEdit {
20480 range: lsp::Range {
20481 start: lsp::Position {
20482 line: 0,
20483 character: 7,
20484 },
20485 end: lsp::Position {
20486 line: 0,
20487 character: 10,
20488 },
20489 },
20490 new_text: "FooRenamed".to_string(),
20491 };
20492 Ok(Some(lsp::WorkspaceEdit::new(
20493 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
20494 )))
20495 });
20496 let rename_task = cx
20497 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
20498 .expect("Confirm rename was not started");
20499 rename_handler.next().await.unwrap();
20500 rename_task.await.expect("Confirm rename failed");
20501 cx.run_until_parked();
20502
20503 // Correct range is renamed, as `surrounding_word` is used to find it.
20504 cx.assert_editor_state(indoc! {"
20505 struct FooRenamedˇ {}
20506 "});
20507}
20508
20509#[gpui::test]
20510async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
20511 init_test(cx, |_| {});
20512 let mut cx = EditorTestContext::new(cx).await;
20513
20514 let language = Arc::new(
20515 Language::new(
20516 LanguageConfig::default(),
20517 Some(tree_sitter_html::LANGUAGE.into()),
20518 )
20519 .with_brackets_query(
20520 r#"
20521 ("<" @open "/>" @close)
20522 ("</" @open ">" @close)
20523 ("<" @open ">" @close)
20524 ("\"" @open "\"" @close)
20525 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
20526 "#,
20527 )
20528 .unwrap(),
20529 );
20530 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
20531
20532 cx.set_state(indoc! {"
20533 <span>ˇ</span>
20534 "});
20535 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
20536 cx.assert_editor_state(indoc! {"
20537 <span>
20538 ˇ
20539 </span>
20540 "});
20541
20542 cx.set_state(indoc! {"
20543 <span><span></span>ˇ</span>
20544 "});
20545 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
20546 cx.assert_editor_state(indoc! {"
20547 <span><span></span>
20548 ˇ</span>
20549 "});
20550
20551 cx.set_state(indoc! {"
20552 <span>ˇ
20553 </span>
20554 "});
20555 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
20556 cx.assert_editor_state(indoc! {"
20557 <span>
20558 ˇ
20559 </span>
20560 "});
20561}
20562
20563#[gpui::test(iterations = 10)]
20564async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
20565 init_test(cx, |_| {});
20566
20567 let fs = FakeFs::new(cx.executor());
20568 fs.insert_tree(
20569 path!("/dir"),
20570 json!({
20571 "a.ts": "a",
20572 }),
20573 )
20574 .await;
20575
20576 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
20577 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20578 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20579
20580 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
20581 language_registry.add(Arc::new(Language::new(
20582 LanguageConfig {
20583 name: "TypeScript".into(),
20584 matcher: LanguageMatcher {
20585 path_suffixes: vec!["ts".to_string()],
20586 ..Default::default()
20587 },
20588 ..Default::default()
20589 },
20590 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
20591 )));
20592 let mut fake_language_servers = language_registry.register_fake_lsp(
20593 "TypeScript",
20594 FakeLspAdapter {
20595 capabilities: lsp::ServerCapabilities {
20596 code_lens_provider: Some(lsp::CodeLensOptions {
20597 resolve_provider: Some(true),
20598 }),
20599 execute_command_provider: Some(lsp::ExecuteCommandOptions {
20600 commands: vec!["_the/command".to_string()],
20601 ..lsp::ExecuteCommandOptions::default()
20602 }),
20603 ..lsp::ServerCapabilities::default()
20604 },
20605 ..FakeLspAdapter::default()
20606 },
20607 );
20608
20609 let (buffer, _handle) = project
20610 .update(cx, |p, cx| {
20611 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
20612 })
20613 .await
20614 .unwrap();
20615 cx.executor().run_until_parked();
20616
20617 let fake_server = fake_language_servers.next().await.unwrap();
20618
20619 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
20620 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
20621 drop(buffer_snapshot);
20622 let actions = cx
20623 .update_window(*workspace, |_, window, cx| {
20624 project.code_actions(&buffer, anchor..anchor, window, cx)
20625 })
20626 .unwrap();
20627
20628 fake_server
20629 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
20630 Ok(Some(vec![
20631 lsp::CodeLens {
20632 range: lsp::Range::default(),
20633 command: Some(lsp::Command {
20634 title: "Code lens command".to_owned(),
20635 command: "_the/command".to_owned(),
20636 arguments: None,
20637 }),
20638 data: None,
20639 },
20640 lsp::CodeLens {
20641 range: lsp::Range::default(),
20642 command: Some(lsp::Command {
20643 title: "Command not in capabilities".to_owned(),
20644 command: "not in capabilities".to_owned(),
20645 arguments: None,
20646 }),
20647 data: None,
20648 },
20649 lsp::CodeLens {
20650 range: lsp::Range {
20651 start: lsp::Position {
20652 line: 1,
20653 character: 1,
20654 },
20655 end: lsp::Position {
20656 line: 1,
20657 character: 1,
20658 },
20659 },
20660 command: Some(lsp::Command {
20661 title: "Command not in range".to_owned(),
20662 command: "_the/command".to_owned(),
20663 arguments: None,
20664 }),
20665 data: None,
20666 },
20667 ]))
20668 })
20669 .next()
20670 .await;
20671
20672 let actions = actions.await.unwrap();
20673 assert_eq!(
20674 actions.len(),
20675 1,
20676 "Should have only one valid action for the 0..0 range"
20677 );
20678 let action = actions[0].clone();
20679 let apply = project.update(cx, |project, cx| {
20680 project.apply_code_action(buffer.clone(), action, true, cx)
20681 });
20682
20683 // Resolving the code action does not populate its edits. In absence of
20684 // edits, we must execute the given command.
20685 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
20686 |mut lens, _| async move {
20687 let lens_command = lens.command.as_mut().expect("should have a command");
20688 assert_eq!(lens_command.title, "Code lens command");
20689 lens_command.arguments = Some(vec![json!("the-argument")]);
20690 Ok(lens)
20691 },
20692 );
20693
20694 // While executing the command, the language server sends the editor
20695 // a `workspaceEdit` request.
20696 fake_server
20697 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
20698 let fake = fake_server.clone();
20699 move |params, _| {
20700 assert_eq!(params.command, "_the/command");
20701 let fake = fake.clone();
20702 async move {
20703 fake.server
20704 .request::<lsp::request::ApplyWorkspaceEdit>(
20705 lsp::ApplyWorkspaceEditParams {
20706 label: None,
20707 edit: lsp::WorkspaceEdit {
20708 changes: Some(
20709 [(
20710 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
20711 vec![lsp::TextEdit {
20712 range: lsp::Range::new(
20713 lsp::Position::new(0, 0),
20714 lsp::Position::new(0, 0),
20715 ),
20716 new_text: "X".into(),
20717 }],
20718 )]
20719 .into_iter()
20720 .collect(),
20721 ),
20722 ..Default::default()
20723 },
20724 },
20725 )
20726 .await
20727 .into_response()
20728 .unwrap();
20729 Ok(Some(json!(null)))
20730 }
20731 }
20732 })
20733 .next()
20734 .await;
20735
20736 // Applying the code lens command returns a project transaction containing the edits
20737 // sent by the language server in its `workspaceEdit` request.
20738 let transaction = apply.await.unwrap();
20739 assert!(transaction.0.contains_key(&buffer));
20740 buffer.update(cx, |buffer, cx| {
20741 assert_eq!(buffer.text(), "Xa");
20742 buffer.undo(cx);
20743 assert_eq!(buffer.text(), "a");
20744 });
20745}
20746
20747#[gpui::test]
20748async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
20749 init_test(cx, |_| {});
20750
20751 let fs = FakeFs::new(cx.executor());
20752 let main_text = r#"fn main() {
20753println!("1");
20754println!("2");
20755println!("3");
20756println!("4");
20757println!("5");
20758}"#;
20759 let lib_text = "mod foo {}";
20760 fs.insert_tree(
20761 path!("/a"),
20762 json!({
20763 "lib.rs": lib_text,
20764 "main.rs": main_text,
20765 }),
20766 )
20767 .await;
20768
20769 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20770 let (workspace, cx) =
20771 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20772 let worktree_id = workspace.update(cx, |workspace, cx| {
20773 workspace.project().update(cx, |project, cx| {
20774 project.worktrees(cx).next().unwrap().read(cx).id()
20775 })
20776 });
20777
20778 let expected_ranges = vec![
20779 Point::new(0, 0)..Point::new(0, 0),
20780 Point::new(1, 0)..Point::new(1, 1),
20781 Point::new(2, 0)..Point::new(2, 2),
20782 Point::new(3, 0)..Point::new(3, 3),
20783 ];
20784
20785 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
20786 let editor_1 = workspace
20787 .update_in(cx, |workspace, window, cx| {
20788 workspace.open_path(
20789 (worktree_id, "main.rs"),
20790 Some(pane_1.downgrade()),
20791 true,
20792 window,
20793 cx,
20794 )
20795 })
20796 .unwrap()
20797 .await
20798 .downcast::<Editor>()
20799 .unwrap();
20800 pane_1.update(cx, |pane, cx| {
20801 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20802 open_editor.update(cx, |editor, cx| {
20803 assert_eq!(
20804 editor.display_text(cx),
20805 main_text,
20806 "Original main.rs text on initial open",
20807 );
20808 assert_eq!(
20809 editor
20810 .selections
20811 .all::<Point>(cx)
20812 .into_iter()
20813 .map(|s| s.range())
20814 .collect::<Vec<_>>(),
20815 vec![Point::zero()..Point::zero()],
20816 "Default selections on initial open",
20817 );
20818 })
20819 });
20820 editor_1.update_in(cx, |editor, window, cx| {
20821 editor.change_selections(None, window, cx, |s| {
20822 s.select_ranges(expected_ranges.clone());
20823 });
20824 });
20825
20826 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
20827 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
20828 });
20829 let editor_2 = workspace
20830 .update_in(cx, |workspace, window, cx| {
20831 workspace.open_path(
20832 (worktree_id, "main.rs"),
20833 Some(pane_2.downgrade()),
20834 true,
20835 window,
20836 cx,
20837 )
20838 })
20839 .unwrap()
20840 .await
20841 .downcast::<Editor>()
20842 .unwrap();
20843 pane_2.update(cx, |pane, cx| {
20844 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20845 open_editor.update(cx, |editor, cx| {
20846 assert_eq!(
20847 editor.display_text(cx),
20848 main_text,
20849 "Original main.rs text on initial open in another panel",
20850 );
20851 assert_eq!(
20852 editor
20853 .selections
20854 .all::<Point>(cx)
20855 .into_iter()
20856 .map(|s| s.range())
20857 .collect::<Vec<_>>(),
20858 vec![Point::zero()..Point::zero()],
20859 "Default selections on initial open in another panel",
20860 );
20861 })
20862 });
20863
20864 editor_2.update_in(cx, |editor, window, cx| {
20865 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
20866 });
20867
20868 let _other_editor_1 = workspace
20869 .update_in(cx, |workspace, window, cx| {
20870 workspace.open_path(
20871 (worktree_id, "lib.rs"),
20872 Some(pane_1.downgrade()),
20873 true,
20874 window,
20875 cx,
20876 )
20877 })
20878 .unwrap()
20879 .await
20880 .downcast::<Editor>()
20881 .unwrap();
20882 pane_1
20883 .update_in(cx, |pane, window, cx| {
20884 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
20885 })
20886 .await
20887 .unwrap();
20888 drop(editor_1);
20889 pane_1.update(cx, |pane, cx| {
20890 pane.active_item()
20891 .unwrap()
20892 .downcast::<Editor>()
20893 .unwrap()
20894 .update(cx, |editor, cx| {
20895 assert_eq!(
20896 editor.display_text(cx),
20897 lib_text,
20898 "Other file should be open and active",
20899 );
20900 });
20901 assert_eq!(pane.items().count(), 1, "No other editors should be open");
20902 });
20903
20904 let _other_editor_2 = workspace
20905 .update_in(cx, |workspace, window, cx| {
20906 workspace.open_path(
20907 (worktree_id, "lib.rs"),
20908 Some(pane_2.downgrade()),
20909 true,
20910 window,
20911 cx,
20912 )
20913 })
20914 .unwrap()
20915 .await
20916 .downcast::<Editor>()
20917 .unwrap();
20918 pane_2
20919 .update_in(cx, |pane, window, cx| {
20920 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
20921 })
20922 .await
20923 .unwrap();
20924 drop(editor_2);
20925 pane_2.update(cx, |pane, cx| {
20926 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20927 open_editor.update(cx, |editor, cx| {
20928 assert_eq!(
20929 editor.display_text(cx),
20930 lib_text,
20931 "Other file should be open and active in another panel too",
20932 );
20933 });
20934 assert_eq!(
20935 pane.items().count(),
20936 1,
20937 "No other editors should be open in another pane",
20938 );
20939 });
20940
20941 let _editor_1_reopened = workspace
20942 .update_in(cx, |workspace, window, cx| {
20943 workspace.open_path(
20944 (worktree_id, "main.rs"),
20945 Some(pane_1.downgrade()),
20946 true,
20947 window,
20948 cx,
20949 )
20950 })
20951 .unwrap()
20952 .await
20953 .downcast::<Editor>()
20954 .unwrap();
20955 let _editor_2_reopened = workspace
20956 .update_in(cx, |workspace, window, cx| {
20957 workspace.open_path(
20958 (worktree_id, "main.rs"),
20959 Some(pane_2.downgrade()),
20960 true,
20961 window,
20962 cx,
20963 )
20964 })
20965 .unwrap()
20966 .await
20967 .downcast::<Editor>()
20968 .unwrap();
20969 pane_1.update(cx, |pane, cx| {
20970 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20971 open_editor.update(cx, |editor, cx| {
20972 assert_eq!(
20973 editor.display_text(cx),
20974 main_text,
20975 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
20976 );
20977 assert_eq!(
20978 editor
20979 .selections
20980 .all::<Point>(cx)
20981 .into_iter()
20982 .map(|s| s.range())
20983 .collect::<Vec<_>>(),
20984 expected_ranges,
20985 "Previous editor in the 1st panel had selections and should get them restored on reopen",
20986 );
20987 })
20988 });
20989 pane_2.update(cx, |pane, cx| {
20990 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20991 open_editor.update(cx, |editor, cx| {
20992 assert_eq!(
20993 editor.display_text(cx),
20994 r#"fn main() {
20995⋯rintln!("1");
20996⋯intln!("2");
20997⋯ntln!("3");
20998println!("4");
20999println!("5");
21000}"#,
21001 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
21002 );
21003 assert_eq!(
21004 editor
21005 .selections
21006 .all::<Point>(cx)
21007 .into_iter()
21008 .map(|s| s.range())
21009 .collect::<Vec<_>>(),
21010 vec![Point::zero()..Point::zero()],
21011 "Previous editor in the 2nd pane had no selections changed hence should restore none",
21012 );
21013 })
21014 });
21015}
21016
21017#[gpui::test]
21018async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
21019 init_test(cx, |_| {});
21020
21021 let fs = FakeFs::new(cx.executor());
21022 let main_text = r#"fn main() {
21023println!("1");
21024println!("2");
21025println!("3");
21026println!("4");
21027println!("5");
21028}"#;
21029 let lib_text = "mod foo {}";
21030 fs.insert_tree(
21031 path!("/a"),
21032 json!({
21033 "lib.rs": lib_text,
21034 "main.rs": main_text,
21035 }),
21036 )
21037 .await;
21038
21039 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21040 let (workspace, cx) =
21041 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21042 let worktree_id = workspace.update(cx, |workspace, cx| {
21043 workspace.project().update(cx, |project, cx| {
21044 project.worktrees(cx).next().unwrap().read(cx).id()
21045 })
21046 });
21047
21048 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21049 let editor = workspace
21050 .update_in(cx, |workspace, window, cx| {
21051 workspace.open_path(
21052 (worktree_id, "main.rs"),
21053 Some(pane.downgrade()),
21054 true,
21055 window,
21056 cx,
21057 )
21058 })
21059 .unwrap()
21060 .await
21061 .downcast::<Editor>()
21062 .unwrap();
21063 pane.update(cx, |pane, cx| {
21064 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21065 open_editor.update(cx, |editor, cx| {
21066 assert_eq!(
21067 editor.display_text(cx),
21068 main_text,
21069 "Original main.rs text on initial open",
21070 );
21071 })
21072 });
21073 editor.update_in(cx, |editor, window, cx| {
21074 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
21075 });
21076
21077 cx.update_global(|store: &mut SettingsStore, cx| {
21078 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
21079 s.restore_on_file_reopen = Some(false);
21080 });
21081 });
21082 editor.update_in(cx, |editor, window, cx| {
21083 editor.fold_ranges(
21084 vec![
21085 Point::new(1, 0)..Point::new(1, 1),
21086 Point::new(2, 0)..Point::new(2, 2),
21087 Point::new(3, 0)..Point::new(3, 3),
21088 ],
21089 false,
21090 window,
21091 cx,
21092 );
21093 });
21094 pane.update_in(cx, |pane, window, cx| {
21095 pane.close_all_items(&CloseAllItems::default(), window, cx)
21096 })
21097 .await
21098 .unwrap();
21099 pane.update(cx, |pane, _| {
21100 assert!(pane.active_item().is_none());
21101 });
21102 cx.update_global(|store: &mut SettingsStore, cx| {
21103 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
21104 s.restore_on_file_reopen = Some(true);
21105 });
21106 });
21107
21108 let _editor_reopened = workspace
21109 .update_in(cx, |workspace, window, cx| {
21110 workspace.open_path(
21111 (worktree_id, "main.rs"),
21112 Some(pane.downgrade()),
21113 true,
21114 window,
21115 cx,
21116 )
21117 })
21118 .unwrap()
21119 .await
21120 .downcast::<Editor>()
21121 .unwrap();
21122 pane.update(cx, |pane, cx| {
21123 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21124 open_editor.update(cx, |editor, cx| {
21125 assert_eq!(
21126 editor.display_text(cx),
21127 main_text,
21128 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
21129 );
21130 })
21131 });
21132}
21133
21134#[gpui::test]
21135async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
21136 struct EmptyModalView {
21137 focus_handle: gpui::FocusHandle,
21138 }
21139 impl EventEmitter<DismissEvent> for EmptyModalView {}
21140 impl Render for EmptyModalView {
21141 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
21142 div()
21143 }
21144 }
21145 impl Focusable for EmptyModalView {
21146 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
21147 self.focus_handle.clone()
21148 }
21149 }
21150 impl workspace::ModalView for EmptyModalView {}
21151 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
21152 EmptyModalView {
21153 focus_handle: cx.focus_handle(),
21154 }
21155 }
21156
21157 init_test(cx, |_| {});
21158
21159 let fs = FakeFs::new(cx.executor());
21160 let project = Project::test(fs, [], cx).await;
21161 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21162 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
21163 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21164 let editor = cx.new_window_entity(|window, cx| {
21165 Editor::new(
21166 EditorMode::full(),
21167 buffer,
21168 Some(project.clone()),
21169 window,
21170 cx,
21171 )
21172 });
21173 workspace
21174 .update(cx, |workspace, window, cx| {
21175 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
21176 })
21177 .unwrap();
21178 editor.update_in(cx, |editor, window, cx| {
21179 editor.open_context_menu(&OpenContextMenu, window, cx);
21180 assert!(editor.mouse_context_menu.is_some());
21181 });
21182 workspace
21183 .update(cx, |workspace, window, cx| {
21184 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
21185 })
21186 .unwrap();
21187 cx.read(|cx| {
21188 assert!(editor.read(cx).mouse_context_menu.is_none());
21189 });
21190}
21191
21192#[gpui::test]
21193async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
21194 init_test(cx, |_| {});
21195
21196 let fs = FakeFs::new(cx.executor());
21197 fs.insert_file(path!("/file.html"), Default::default())
21198 .await;
21199
21200 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
21201
21202 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21203 let html_language = Arc::new(Language::new(
21204 LanguageConfig {
21205 name: "HTML".into(),
21206 matcher: LanguageMatcher {
21207 path_suffixes: vec!["html".to_string()],
21208 ..LanguageMatcher::default()
21209 },
21210 brackets: BracketPairConfig {
21211 pairs: vec![BracketPair {
21212 start: "<".into(),
21213 end: ">".into(),
21214 close: true,
21215 ..Default::default()
21216 }],
21217 ..Default::default()
21218 },
21219 ..Default::default()
21220 },
21221 Some(tree_sitter_html::LANGUAGE.into()),
21222 ));
21223 language_registry.add(html_language);
21224 let mut fake_servers = language_registry.register_fake_lsp(
21225 "HTML",
21226 FakeLspAdapter {
21227 capabilities: lsp::ServerCapabilities {
21228 completion_provider: Some(lsp::CompletionOptions {
21229 resolve_provider: Some(true),
21230 ..Default::default()
21231 }),
21232 ..Default::default()
21233 },
21234 ..Default::default()
21235 },
21236 );
21237
21238 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21239 let cx = &mut VisualTestContext::from_window(*workspace, cx);
21240
21241 let worktree_id = workspace
21242 .update(cx, |workspace, _window, cx| {
21243 workspace.project().update(cx, |project, cx| {
21244 project.worktrees(cx).next().unwrap().read(cx).id()
21245 })
21246 })
21247 .unwrap();
21248 project
21249 .update(cx, |project, cx| {
21250 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
21251 })
21252 .await
21253 .unwrap();
21254 let editor = workspace
21255 .update(cx, |workspace, window, cx| {
21256 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
21257 })
21258 .unwrap()
21259 .await
21260 .unwrap()
21261 .downcast::<Editor>()
21262 .unwrap();
21263
21264 let fake_server = fake_servers.next().await.unwrap();
21265 editor.update_in(cx, |editor, window, cx| {
21266 editor.set_text("<ad></ad>", window, cx);
21267 editor.change_selections(None, window, cx, |selections| {
21268 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
21269 });
21270 let Some((buffer, _)) = editor
21271 .buffer
21272 .read(cx)
21273 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
21274 else {
21275 panic!("Failed to get buffer for selection position");
21276 };
21277 let buffer = buffer.read(cx);
21278 let buffer_id = buffer.remote_id();
21279 let opening_range =
21280 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
21281 let closing_range =
21282 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
21283 let mut linked_ranges = HashMap::default();
21284 linked_ranges.insert(
21285 buffer_id,
21286 vec![(opening_range.clone(), vec![closing_range.clone()])],
21287 );
21288 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
21289 });
21290 let mut completion_handle =
21291 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
21292 Ok(Some(lsp::CompletionResponse::Array(vec![
21293 lsp::CompletionItem {
21294 label: "head".to_string(),
21295 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
21296 lsp::InsertReplaceEdit {
21297 new_text: "head".to_string(),
21298 insert: lsp::Range::new(
21299 lsp::Position::new(0, 1),
21300 lsp::Position::new(0, 3),
21301 ),
21302 replace: lsp::Range::new(
21303 lsp::Position::new(0, 1),
21304 lsp::Position::new(0, 3),
21305 ),
21306 },
21307 )),
21308 ..Default::default()
21309 },
21310 ])))
21311 });
21312 editor.update_in(cx, |editor, window, cx| {
21313 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
21314 });
21315 cx.run_until_parked();
21316 completion_handle.next().await.unwrap();
21317 editor.update(cx, |editor, _| {
21318 assert!(
21319 editor.context_menu_visible(),
21320 "Completion menu should be visible"
21321 );
21322 });
21323 editor.update_in(cx, |editor, window, cx| {
21324 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
21325 });
21326 cx.executor().run_until_parked();
21327 editor.update(cx, |editor, cx| {
21328 assert_eq!(editor.text(cx), "<head></head>");
21329 });
21330}
21331
21332#[gpui::test]
21333async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
21334 init_test(cx, |_| {});
21335
21336 let fs = FakeFs::new(cx.executor());
21337 fs.insert_tree(
21338 path!("/root"),
21339 json!({
21340 "a": {
21341 "main.rs": "fn main() {}",
21342 },
21343 "foo": {
21344 "bar": {
21345 "external_file.rs": "pub mod external {}",
21346 }
21347 }
21348 }),
21349 )
21350 .await;
21351
21352 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
21353 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21354 language_registry.add(rust_lang());
21355 let _fake_servers = language_registry.register_fake_lsp(
21356 "Rust",
21357 FakeLspAdapter {
21358 ..FakeLspAdapter::default()
21359 },
21360 );
21361 let (workspace, cx) =
21362 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21363 let worktree_id = workspace.update(cx, |workspace, cx| {
21364 workspace.project().update(cx, |project, cx| {
21365 project.worktrees(cx).next().unwrap().read(cx).id()
21366 })
21367 });
21368
21369 let assert_language_servers_count =
21370 |expected: usize, context: &str, cx: &mut VisualTestContext| {
21371 project.update(cx, |project, cx| {
21372 let current = project
21373 .lsp_store()
21374 .read(cx)
21375 .as_local()
21376 .unwrap()
21377 .language_servers
21378 .len();
21379 assert_eq!(expected, current, "{context}");
21380 });
21381 };
21382
21383 assert_language_servers_count(
21384 0,
21385 "No servers should be running before any file is open",
21386 cx,
21387 );
21388 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21389 let main_editor = workspace
21390 .update_in(cx, |workspace, window, cx| {
21391 workspace.open_path(
21392 (worktree_id, "main.rs"),
21393 Some(pane.downgrade()),
21394 true,
21395 window,
21396 cx,
21397 )
21398 })
21399 .unwrap()
21400 .await
21401 .downcast::<Editor>()
21402 .unwrap();
21403 pane.update(cx, |pane, cx| {
21404 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21405 open_editor.update(cx, |editor, cx| {
21406 assert_eq!(
21407 editor.display_text(cx),
21408 "fn main() {}",
21409 "Original main.rs text on initial open",
21410 );
21411 });
21412 assert_eq!(open_editor, main_editor);
21413 });
21414 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
21415
21416 let external_editor = workspace
21417 .update_in(cx, |workspace, window, cx| {
21418 workspace.open_abs_path(
21419 PathBuf::from("/root/foo/bar/external_file.rs"),
21420 OpenOptions::default(),
21421 window,
21422 cx,
21423 )
21424 })
21425 .await
21426 .expect("opening external file")
21427 .downcast::<Editor>()
21428 .expect("downcasted external file's open element to editor");
21429 pane.update(cx, |pane, cx| {
21430 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21431 open_editor.update(cx, |editor, cx| {
21432 assert_eq!(
21433 editor.display_text(cx),
21434 "pub mod external {}",
21435 "External file is open now",
21436 );
21437 });
21438 assert_eq!(open_editor, external_editor);
21439 });
21440 assert_language_servers_count(
21441 1,
21442 "Second, external, *.rs file should join the existing server",
21443 cx,
21444 );
21445
21446 pane.update_in(cx, |pane, window, cx| {
21447 pane.close_active_item(&CloseActiveItem::default(), window, cx)
21448 })
21449 .await
21450 .unwrap();
21451 pane.update_in(cx, |pane, window, cx| {
21452 pane.navigate_backward(window, cx);
21453 });
21454 cx.run_until_parked();
21455 pane.update(cx, |pane, cx| {
21456 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21457 open_editor.update(cx, |editor, cx| {
21458 assert_eq!(
21459 editor.display_text(cx),
21460 "pub mod external {}",
21461 "External file is open now",
21462 );
21463 });
21464 });
21465 assert_language_servers_count(
21466 1,
21467 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
21468 cx,
21469 );
21470
21471 cx.update(|_, cx| {
21472 workspace::reload(&workspace::Reload::default(), cx);
21473 });
21474 assert_language_servers_count(
21475 1,
21476 "After reloading the worktree with local and external files opened, only one project should be started",
21477 cx,
21478 );
21479}
21480
21481#[gpui::test]
21482async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
21483 init_test(cx, |_| {});
21484
21485 let mut cx = EditorTestContext::new(cx).await;
21486 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
21487 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
21488
21489 // test cursor move to start of each line on tab
21490 // for `if`, `elif`, `else`, `while`, `with` and `for`
21491 cx.set_state(indoc! {"
21492 def main():
21493 ˇ for item in items:
21494 ˇ while item.active:
21495 ˇ if item.value > 10:
21496 ˇ continue
21497 ˇ elif item.value < 0:
21498 ˇ break
21499 ˇ else:
21500 ˇ with item.context() as ctx:
21501 ˇ yield count
21502 ˇ else:
21503 ˇ log('while else')
21504 ˇ else:
21505 ˇ log('for else')
21506 "});
21507 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
21508 cx.assert_editor_state(indoc! {"
21509 def main():
21510 ˇfor item in items:
21511 ˇwhile item.active:
21512 ˇif item.value > 10:
21513 ˇcontinue
21514 ˇelif item.value < 0:
21515 ˇbreak
21516 ˇelse:
21517 ˇwith item.context() as ctx:
21518 ˇyield count
21519 ˇelse:
21520 ˇlog('while else')
21521 ˇelse:
21522 ˇlog('for else')
21523 "});
21524 // test relative indent is preserved when tab
21525 // for `if`, `elif`, `else`, `while`, `with` and `for`
21526 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
21527 cx.assert_editor_state(indoc! {"
21528 def main():
21529 ˇfor item in items:
21530 ˇwhile item.active:
21531 ˇif item.value > 10:
21532 ˇcontinue
21533 ˇelif item.value < 0:
21534 ˇbreak
21535 ˇelse:
21536 ˇwith item.context() as ctx:
21537 ˇyield count
21538 ˇelse:
21539 ˇlog('while else')
21540 ˇelse:
21541 ˇlog('for else')
21542 "});
21543
21544 // test cursor move to start of each line on tab
21545 // for `try`, `except`, `else`, `finally`, `match` and `def`
21546 cx.set_state(indoc! {"
21547 def main():
21548 ˇ try:
21549 ˇ fetch()
21550 ˇ except ValueError:
21551 ˇ handle_error()
21552 ˇ else:
21553 ˇ match value:
21554 ˇ case _:
21555 ˇ finally:
21556 ˇ def status():
21557 ˇ return 0
21558 "});
21559 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
21560 cx.assert_editor_state(indoc! {"
21561 def main():
21562 ˇtry:
21563 ˇfetch()
21564 ˇexcept ValueError:
21565 ˇhandle_error()
21566 ˇelse:
21567 ˇmatch value:
21568 ˇcase _:
21569 ˇfinally:
21570 ˇdef status():
21571 ˇreturn 0
21572 "});
21573 // test relative indent is preserved when tab
21574 // for `try`, `except`, `else`, `finally`, `match` and `def`
21575 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
21576 cx.assert_editor_state(indoc! {"
21577 def main():
21578 ˇtry:
21579 ˇfetch()
21580 ˇexcept ValueError:
21581 ˇhandle_error()
21582 ˇelse:
21583 ˇmatch value:
21584 ˇcase _:
21585 ˇfinally:
21586 ˇdef status():
21587 ˇreturn 0
21588 "});
21589}
21590
21591#[gpui::test]
21592async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
21593 init_test(cx, |_| {});
21594
21595 let mut cx = EditorTestContext::new(cx).await;
21596 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
21597 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
21598
21599 // test `else` auto outdents when typed inside `if` block
21600 cx.set_state(indoc! {"
21601 def main():
21602 if i == 2:
21603 return
21604 ˇ
21605 "});
21606 cx.update_editor(|editor, window, cx| {
21607 editor.handle_input("else:", window, cx);
21608 });
21609 cx.assert_editor_state(indoc! {"
21610 def main():
21611 if i == 2:
21612 return
21613 else:ˇ
21614 "});
21615
21616 // test `except` auto outdents when typed inside `try` block
21617 cx.set_state(indoc! {"
21618 def main():
21619 try:
21620 i = 2
21621 ˇ
21622 "});
21623 cx.update_editor(|editor, window, cx| {
21624 editor.handle_input("except:", window, cx);
21625 });
21626 cx.assert_editor_state(indoc! {"
21627 def main():
21628 try:
21629 i = 2
21630 except:ˇ
21631 "});
21632
21633 // test `else` auto outdents when typed inside `except` block
21634 cx.set_state(indoc! {"
21635 def main():
21636 try:
21637 i = 2
21638 except:
21639 j = 2
21640 ˇ
21641 "});
21642 cx.update_editor(|editor, window, cx| {
21643 editor.handle_input("else:", window, cx);
21644 });
21645 cx.assert_editor_state(indoc! {"
21646 def main():
21647 try:
21648 i = 2
21649 except:
21650 j = 2
21651 else:ˇ
21652 "});
21653
21654 // test `finally` auto outdents when typed inside `else` block
21655 cx.set_state(indoc! {"
21656 def main():
21657 try:
21658 i = 2
21659 except:
21660 j = 2
21661 else:
21662 k = 2
21663 ˇ
21664 "});
21665 cx.update_editor(|editor, window, cx| {
21666 editor.handle_input("finally:", window, cx);
21667 });
21668 cx.assert_editor_state(indoc! {"
21669 def main():
21670 try:
21671 i = 2
21672 except:
21673 j = 2
21674 else:
21675 k = 2
21676 finally:ˇ
21677 "});
21678
21679 // TODO: test `except` auto outdents when typed inside `try` block right after for block
21680 // cx.set_state(indoc! {"
21681 // def main():
21682 // try:
21683 // for i in range(n):
21684 // pass
21685 // ˇ
21686 // "});
21687 // cx.update_editor(|editor, window, cx| {
21688 // editor.handle_input("except:", window, cx);
21689 // });
21690 // cx.assert_editor_state(indoc! {"
21691 // def main():
21692 // try:
21693 // for i in range(n):
21694 // pass
21695 // except:ˇ
21696 // "});
21697
21698 // TODO: test `else` auto outdents when typed inside `except` block right after for block
21699 // cx.set_state(indoc! {"
21700 // def main():
21701 // try:
21702 // i = 2
21703 // except:
21704 // for i in range(n):
21705 // pass
21706 // ˇ
21707 // "});
21708 // cx.update_editor(|editor, window, cx| {
21709 // editor.handle_input("else:", window, cx);
21710 // });
21711 // cx.assert_editor_state(indoc! {"
21712 // def main():
21713 // try:
21714 // i = 2
21715 // except:
21716 // for i in range(n):
21717 // pass
21718 // else:ˇ
21719 // "});
21720
21721 // TODO: test `finally` auto outdents when typed inside `else` block right after for block
21722 // cx.set_state(indoc! {"
21723 // def main():
21724 // try:
21725 // i = 2
21726 // except:
21727 // j = 2
21728 // else:
21729 // for i in range(n):
21730 // pass
21731 // ˇ
21732 // "});
21733 // cx.update_editor(|editor, window, cx| {
21734 // editor.handle_input("finally:", window, cx);
21735 // });
21736 // cx.assert_editor_state(indoc! {"
21737 // def main():
21738 // try:
21739 // i = 2
21740 // except:
21741 // j = 2
21742 // else:
21743 // for i in range(n):
21744 // pass
21745 // finally:ˇ
21746 // "});
21747
21748 // test `else` stays at correct indent when typed after `for` block
21749 cx.set_state(indoc! {"
21750 def main():
21751 for i in range(10):
21752 if i == 3:
21753 break
21754 ˇ
21755 "});
21756 cx.update_editor(|editor, window, cx| {
21757 editor.handle_input("else:", window, cx);
21758 });
21759 cx.assert_editor_state(indoc! {"
21760 def main():
21761 for i in range(10):
21762 if i == 3:
21763 break
21764 else:ˇ
21765 "});
21766
21767 // test does not outdent on typing after line with square brackets
21768 cx.set_state(indoc! {"
21769 def f() -> list[str]:
21770 ˇ
21771 "});
21772 cx.update_editor(|editor, window, cx| {
21773 editor.handle_input("a", window, cx);
21774 });
21775 cx.assert_editor_state(indoc! {"
21776 def f() -> list[str]:
21777 aˇ
21778 "});
21779}
21780
21781#[gpui::test]
21782async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
21783 init_test(cx, |_| {});
21784 update_test_language_settings(cx, |settings| {
21785 settings.defaults.extend_comment_on_newline = Some(false);
21786 });
21787 let mut cx = EditorTestContext::new(cx).await;
21788 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
21789 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
21790
21791 // test correct indent after newline on comment
21792 cx.set_state(indoc! {"
21793 # COMMENT:ˇ
21794 "});
21795 cx.update_editor(|editor, window, cx| {
21796 editor.newline(&Newline, window, cx);
21797 });
21798 cx.assert_editor_state(indoc! {"
21799 # COMMENT:
21800 ˇ
21801 "});
21802
21803 // test correct indent after newline in brackets
21804 cx.set_state(indoc! {"
21805 {ˇ}
21806 "});
21807 cx.update_editor(|editor, window, cx| {
21808 editor.newline(&Newline, window, cx);
21809 });
21810 cx.run_until_parked();
21811 cx.assert_editor_state(indoc! {"
21812 {
21813 ˇ
21814 }
21815 "});
21816
21817 cx.set_state(indoc! {"
21818 (ˇ)
21819 "});
21820 cx.update_editor(|editor, window, cx| {
21821 editor.newline(&Newline, window, cx);
21822 });
21823 cx.run_until_parked();
21824 cx.assert_editor_state(indoc! {"
21825 (
21826 ˇ
21827 )
21828 "});
21829
21830 // do not indent after empty lists or dictionaries
21831 cx.set_state(indoc! {"
21832 a = []ˇ
21833 "});
21834 cx.update_editor(|editor, window, cx| {
21835 editor.newline(&Newline, window, cx);
21836 });
21837 cx.run_until_parked();
21838 cx.assert_editor_state(indoc! {"
21839 a = []
21840 ˇ
21841 "});
21842}
21843
21844fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
21845 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
21846 point..point
21847}
21848
21849#[track_caller]
21850fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
21851 let (text, ranges) = marked_text_ranges(marked_text, true);
21852 assert_eq!(editor.text(cx), text);
21853 assert_eq!(
21854 editor.selections.ranges(cx),
21855 ranges,
21856 "Assert selections are {}",
21857 marked_text
21858 );
21859}
21860
21861pub fn handle_signature_help_request(
21862 cx: &mut EditorLspTestContext,
21863 mocked_response: lsp::SignatureHelp,
21864) -> impl Future<Output = ()> + use<> {
21865 let mut request =
21866 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
21867 let mocked_response = mocked_response.clone();
21868 async move { Ok(Some(mocked_response)) }
21869 });
21870
21871 async move {
21872 request.next().await;
21873 }
21874}
21875
21876#[track_caller]
21877pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
21878 cx.update_editor(|editor, _, _| {
21879 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
21880 let entries = menu.entries.borrow();
21881 let entries = entries
21882 .iter()
21883 .map(|entry| entry.string.as_str())
21884 .collect::<Vec<_>>();
21885 assert_eq!(entries, expected);
21886 } else {
21887 panic!("Expected completions menu");
21888 }
21889 });
21890}
21891
21892/// Handle completion request passing a marked string specifying where the completion
21893/// should be triggered from using '|' character, what range should be replaced, and what completions
21894/// should be returned using '<' and '>' to delimit the range.
21895///
21896/// Also see `handle_completion_request_with_insert_and_replace`.
21897#[track_caller]
21898pub fn handle_completion_request(
21899 marked_string: &str,
21900 completions: Vec<&'static str>,
21901 is_incomplete: bool,
21902 counter: Arc<AtomicUsize>,
21903 cx: &mut EditorLspTestContext,
21904) -> impl Future<Output = ()> {
21905 let complete_from_marker: TextRangeMarker = '|'.into();
21906 let replace_range_marker: TextRangeMarker = ('<', '>').into();
21907 let (_, mut marked_ranges) = marked_text_ranges_by(
21908 marked_string,
21909 vec![complete_from_marker.clone(), replace_range_marker.clone()],
21910 );
21911
21912 let complete_from_position =
21913 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
21914 let replace_range =
21915 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
21916
21917 let mut request =
21918 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
21919 let completions = completions.clone();
21920 counter.fetch_add(1, atomic::Ordering::Release);
21921 async move {
21922 assert_eq!(params.text_document_position.text_document.uri, url.clone());
21923 assert_eq!(
21924 params.text_document_position.position,
21925 complete_from_position
21926 );
21927 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
21928 is_incomplete: is_incomplete,
21929 item_defaults: None,
21930 items: completions
21931 .iter()
21932 .map(|completion_text| lsp::CompletionItem {
21933 label: completion_text.to_string(),
21934 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
21935 range: replace_range,
21936 new_text: completion_text.to_string(),
21937 })),
21938 ..Default::default()
21939 })
21940 .collect(),
21941 })))
21942 }
21943 });
21944
21945 async move {
21946 request.next().await;
21947 }
21948}
21949
21950/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
21951/// given instead, which also contains an `insert` range.
21952///
21953/// This function uses markers to define ranges:
21954/// - `|` marks the cursor position
21955/// - `<>` marks the replace range
21956/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
21957pub fn handle_completion_request_with_insert_and_replace(
21958 cx: &mut EditorLspTestContext,
21959 marked_string: &str,
21960 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
21961 counter: Arc<AtomicUsize>,
21962) -> impl Future<Output = ()> {
21963 let complete_from_marker: TextRangeMarker = '|'.into();
21964 let replace_range_marker: TextRangeMarker = ('<', '>').into();
21965 let insert_range_marker: TextRangeMarker = ('{', '}').into();
21966
21967 let (_, mut marked_ranges) = marked_text_ranges_by(
21968 marked_string,
21969 vec![
21970 complete_from_marker.clone(),
21971 replace_range_marker.clone(),
21972 insert_range_marker.clone(),
21973 ],
21974 );
21975
21976 let complete_from_position =
21977 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
21978 let replace_range =
21979 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
21980
21981 let insert_range = match marked_ranges.remove(&insert_range_marker) {
21982 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
21983 _ => lsp::Range {
21984 start: replace_range.start,
21985 end: complete_from_position,
21986 },
21987 };
21988
21989 let mut request =
21990 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
21991 let completions = completions.clone();
21992 counter.fetch_add(1, atomic::Ordering::Release);
21993 async move {
21994 assert_eq!(params.text_document_position.text_document.uri, url.clone());
21995 assert_eq!(
21996 params.text_document_position.position, complete_from_position,
21997 "marker `|` position doesn't match",
21998 );
21999 Ok(Some(lsp::CompletionResponse::Array(
22000 completions
22001 .iter()
22002 .map(|(label, new_text)| lsp::CompletionItem {
22003 label: label.to_string(),
22004 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
22005 lsp::InsertReplaceEdit {
22006 insert: insert_range,
22007 replace: replace_range,
22008 new_text: new_text.to_string(),
22009 },
22010 )),
22011 ..Default::default()
22012 })
22013 .collect(),
22014 )))
22015 }
22016 });
22017
22018 async move {
22019 request.next().await;
22020 }
22021}
22022
22023fn handle_resolve_completion_request(
22024 cx: &mut EditorLspTestContext,
22025 edits: Option<Vec<(&'static str, &'static str)>>,
22026) -> impl Future<Output = ()> {
22027 let edits = edits.map(|edits| {
22028 edits
22029 .iter()
22030 .map(|(marked_string, new_text)| {
22031 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
22032 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
22033 lsp::TextEdit::new(replace_range, new_text.to_string())
22034 })
22035 .collect::<Vec<_>>()
22036 });
22037
22038 let mut request =
22039 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
22040 let edits = edits.clone();
22041 async move {
22042 Ok(lsp::CompletionItem {
22043 additional_text_edits: edits,
22044 ..Default::default()
22045 })
22046 }
22047 });
22048
22049 async move {
22050 request.next().await;
22051 }
22052}
22053
22054pub(crate) fn update_test_language_settings(
22055 cx: &mut TestAppContext,
22056 f: impl Fn(&mut AllLanguageSettingsContent),
22057) {
22058 cx.update(|cx| {
22059 SettingsStore::update_global(cx, |store, cx| {
22060 store.update_user_settings::<AllLanguageSettings>(cx, f);
22061 });
22062 });
22063}
22064
22065pub(crate) fn update_test_project_settings(
22066 cx: &mut TestAppContext,
22067 f: impl Fn(&mut ProjectSettings),
22068) {
22069 cx.update(|cx| {
22070 SettingsStore::update_global(cx, |store, cx| {
22071 store.update_user_settings::<ProjectSettings>(cx, f);
22072 });
22073 });
22074}
22075
22076pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
22077 cx.update(|cx| {
22078 assets::Assets.load_test_fonts(cx);
22079 let store = SettingsStore::test(cx);
22080 cx.set_global(store);
22081 theme::init(theme::LoadThemes::JustBase, cx);
22082 release_channel::init(SemanticVersion::default(), cx);
22083 client::init_settings(cx);
22084 language::init(cx);
22085 Project::init_settings(cx);
22086 workspace::init_settings(cx);
22087 crate::init(cx);
22088 });
22089
22090 update_test_language_settings(cx, f);
22091}
22092
22093#[track_caller]
22094fn assert_hunk_revert(
22095 not_reverted_text_with_selections: &str,
22096 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
22097 expected_reverted_text_with_selections: &str,
22098 base_text: &str,
22099 cx: &mut EditorLspTestContext,
22100) {
22101 cx.set_state(not_reverted_text_with_selections);
22102 cx.set_head_text(base_text);
22103 cx.executor().run_until_parked();
22104
22105 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
22106 let snapshot = editor.snapshot(window, cx);
22107 let reverted_hunk_statuses = snapshot
22108 .buffer_snapshot
22109 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
22110 .map(|hunk| hunk.status().kind)
22111 .collect::<Vec<_>>();
22112
22113 editor.git_restore(&Default::default(), window, cx);
22114 reverted_hunk_statuses
22115 });
22116 cx.executor().run_until_parked();
22117 cx.assert_editor_state(expected_reverted_text_with_selections);
22118 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
22119}
22120
22121#[gpui::test(iterations = 10)]
22122async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
22123 init_test(cx, |_| {});
22124
22125 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
22126 let counter = diagnostic_requests.clone();
22127
22128 let fs = FakeFs::new(cx.executor());
22129 fs.insert_tree(
22130 path!("/a"),
22131 json!({
22132 "first.rs": "fn main() { let a = 5; }",
22133 "second.rs": "// Test file",
22134 }),
22135 )
22136 .await;
22137
22138 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22139 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22140 let cx = &mut VisualTestContext::from_window(*workspace, cx);
22141
22142 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22143 language_registry.add(rust_lang());
22144 let mut fake_servers = language_registry.register_fake_lsp(
22145 "Rust",
22146 FakeLspAdapter {
22147 capabilities: lsp::ServerCapabilities {
22148 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
22149 lsp::DiagnosticOptions {
22150 identifier: None,
22151 inter_file_dependencies: true,
22152 workspace_diagnostics: true,
22153 work_done_progress_options: Default::default(),
22154 },
22155 )),
22156 ..Default::default()
22157 },
22158 ..Default::default()
22159 },
22160 );
22161
22162 let editor = workspace
22163 .update(cx, |workspace, window, cx| {
22164 workspace.open_abs_path(
22165 PathBuf::from(path!("/a/first.rs")),
22166 OpenOptions::default(),
22167 window,
22168 cx,
22169 )
22170 })
22171 .unwrap()
22172 .await
22173 .unwrap()
22174 .downcast::<Editor>()
22175 .unwrap();
22176 let fake_server = fake_servers.next().await.unwrap();
22177 let server_id = fake_server.server.server_id();
22178 let mut first_request = fake_server
22179 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
22180 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
22181 let result_id = Some(new_result_id.to_string());
22182 assert_eq!(
22183 params.text_document.uri,
22184 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
22185 );
22186 async move {
22187 Ok(lsp::DocumentDiagnosticReportResult::Report(
22188 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
22189 related_documents: None,
22190 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
22191 items: Vec::new(),
22192 result_id,
22193 },
22194 }),
22195 ))
22196 }
22197 });
22198
22199 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
22200 project.update(cx, |project, cx| {
22201 let buffer_id = editor
22202 .read(cx)
22203 .buffer()
22204 .read(cx)
22205 .as_singleton()
22206 .expect("created a singleton buffer")
22207 .read(cx)
22208 .remote_id();
22209 let buffer_result_id = project
22210 .lsp_store()
22211 .read(cx)
22212 .result_id(server_id, buffer_id, cx);
22213 assert_eq!(expected, buffer_result_id);
22214 });
22215 };
22216
22217 ensure_result_id(None, cx);
22218 cx.executor().advance_clock(Duration::from_millis(60));
22219 cx.executor().run_until_parked();
22220 assert_eq!(
22221 diagnostic_requests.load(atomic::Ordering::Acquire),
22222 1,
22223 "Opening file should trigger diagnostic request"
22224 );
22225 first_request
22226 .next()
22227 .await
22228 .expect("should have sent the first diagnostics pull request");
22229 ensure_result_id(Some("1".to_string()), cx);
22230
22231 // Editing should trigger diagnostics
22232 editor.update_in(cx, |editor, window, cx| {
22233 editor.handle_input("2", window, cx)
22234 });
22235 cx.executor().advance_clock(Duration::from_millis(60));
22236 cx.executor().run_until_parked();
22237 assert_eq!(
22238 diagnostic_requests.load(atomic::Ordering::Acquire),
22239 2,
22240 "Editing should trigger diagnostic request"
22241 );
22242 ensure_result_id(Some("2".to_string()), cx);
22243
22244 // Moving cursor should not trigger diagnostic request
22245 editor.update_in(cx, |editor, window, cx| {
22246 editor.change_selections(None, window, cx, |s| {
22247 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
22248 });
22249 });
22250 cx.executor().advance_clock(Duration::from_millis(60));
22251 cx.executor().run_until_parked();
22252 assert_eq!(
22253 diagnostic_requests.load(atomic::Ordering::Acquire),
22254 2,
22255 "Cursor movement should not trigger diagnostic request"
22256 );
22257 ensure_result_id(Some("2".to_string()), cx);
22258 // Multiple rapid edits should be debounced
22259 for _ in 0..5 {
22260 editor.update_in(cx, |editor, window, cx| {
22261 editor.handle_input("x", window, cx)
22262 });
22263 }
22264 cx.executor().advance_clock(Duration::from_millis(60));
22265 cx.executor().run_until_parked();
22266
22267 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
22268 assert!(
22269 final_requests <= 4,
22270 "Multiple rapid edits should be debounced (got {final_requests} requests)",
22271 );
22272 ensure_result_id(Some(final_requests.to_string()), cx);
22273}
22274
22275#[gpui::test]
22276async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
22277 // Regression test for issue #11671
22278 // Previously, adding a cursor after moving multiple cursors would reset
22279 // the cursor count instead of adding to the existing cursors.
22280 init_test(cx, |_| {});
22281 let mut cx = EditorTestContext::new(cx).await;
22282
22283 // Create a simple buffer with cursor at start
22284 cx.set_state(indoc! {"
22285 ˇaaaa
22286 bbbb
22287 cccc
22288 dddd
22289 eeee
22290 ffff
22291 gggg
22292 hhhh"});
22293
22294 // Add 2 cursors below (so we have 3 total)
22295 cx.update_editor(|editor, window, cx| {
22296 editor.add_selection_below(&Default::default(), window, cx);
22297 editor.add_selection_below(&Default::default(), window, cx);
22298 });
22299
22300 // Verify we have 3 cursors
22301 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
22302 assert_eq!(
22303 initial_count, 3,
22304 "Should have 3 cursors after adding 2 below"
22305 );
22306
22307 // Move down one line
22308 cx.update_editor(|editor, window, cx| {
22309 editor.move_down(&MoveDown, window, cx);
22310 });
22311
22312 // Add another cursor below
22313 cx.update_editor(|editor, window, cx| {
22314 editor.add_selection_below(&Default::default(), window, cx);
22315 });
22316
22317 // Should now have 4 cursors (3 original + 1 new)
22318 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
22319 assert_eq!(
22320 final_count, 4,
22321 "Should have 4 cursors after moving and adding another"
22322 );
22323}
22324
22325#[gpui::test]
22326async fn test_mtime_and_document_colors(cx: &mut TestAppContext) {
22327 let expected_color = Rgba {
22328 r: 0.33,
22329 g: 0.33,
22330 b: 0.33,
22331 a: 0.33,
22332 };
22333
22334 init_test(cx, |_| {});
22335
22336 let fs = FakeFs::new(cx.executor());
22337 fs.insert_tree(
22338 path!("/a"),
22339 json!({
22340 "first.rs": "fn main() { let a = 5; }",
22341 }),
22342 )
22343 .await;
22344
22345 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22346 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22347 let cx = &mut VisualTestContext::from_window(*workspace, cx);
22348
22349 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22350 language_registry.add(rust_lang());
22351 let mut fake_servers = language_registry.register_fake_lsp(
22352 "Rust",
22353 FakeLspAdapter {
22354 capabilities: lsp::ServerCapabilities {
22355 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
22356 ..lsp::ServerCapabilities::default()
22357 },
22358 ..FakeLspAdapter::default()
22359 },
22360 );
22361
22362 let editor = workspace
22363 .update(cx, |workspace, window, cx| {
22364 workspace.open_abs_path(
22365 PathBuf::from(path!("/a/first.rs")),
22366 OpenOptions::default(),
22367 window,
22368 cx,
22369 )
22370 })
22371 .unwrap()
22372 .await
22373 .unwrap()
22374 .downcast::<Editor>()
22375 .unwrap();
22376 let fake_language_server = fake_servers.next().await.unwrap();
22377 let requests_made = Arc::new(AtomicUsize::new(0));
22378 let closure_requests_made = Arc::clone(&requests_made);
22379 let mut color_request_handle = fake_language_server
22380 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
22381 let requests_made = Arc::clone(&closure_requests_made);
22382 async move {
22383 assert_eq!(
22384 params.text_document.uri,
22385 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
22386 );
22387 requests_made.fetch_add(1, atomic::Ordering::Release);
22388 Ok(vec![lsp::ColorInformation {
22389 range: lsp::Range {
22390 start: lsp::Position {
22391 line: 0,
22392 character: 0,
22393 },
22394 end: lsp::Position {
22395 line: 0,
22396 character: 1,
22397 },
22398 },
22399 color: lsp::Color {
22400 red: 0.33,
22401 green: 0.33,
22402 blue: 0.33,
22403 alpha: 0.33,
22404 },
22405 }])
22406 }
22407 });
22408 color_request_handle.next().await.unwrap();
22409 cx.run_until_parked();
22410 color_request_handle.next().await.unwrap();
22411 cx.run_until_parked();
22412 assert_eq!(
22413 2,
22414 requests_made.load(atomic::Ordering::Acquire),
22415 "Should query for colors once per editor open and once after the language server startup"
22416 );
22417
22418 cx.executor().advance_clock(Duration::from_millis(500));
22419 let save = editor.update_in(cx, |editor, window, cx| {
22420 assert_eq!(
22421 vec![expected_color],
22422 extract_color_inlays(editor, cx),
22423 "Should have an initial inlay"
22424 );
22425
22426 editor.move_to_end(&MoveToEnd, window, cx);
22427 editor.handle_input("dirty", window, cx);
22428 editor.save(
22429 SaveOptions {
22430 format: true,
22431 autosave: true,
22432 },
22433 project.clone(),
22434 window,
22435 cx,
22436 )
22437 });
22438 save.await.unwrap();
22439
22440 color_request_handle.next().await.unwrap();
22441 cx.run_until_parked();
22442 color_request_handle.next().await.unwrap();
22443 cx.run_until_parked();
22444 assert_eq!(
22445 4,
22446 requests_made.load(atomic::Ordering::Acquire),
22447 "Should query for colors once per save and once per formatting after save"
22448 );
22449
22450 drop(editor);
22451 let close = workspace
22452 .update(cx, |workspace, window, cx| {
22453 workspace.active_pane().update(cx, |pane, cx| {
22454 pane.close_active_item(&CloseActiveItem::default(), window, cx)
22455 })
22456 })
22457 .unwrap();
22458 close.await.unwrap();
22459 assert_eq!(
22460 4,
22461 requests_made.load(atomic::Ordering::Acquire),
22462 "After saving and closing the editor, no extra requests should be made"
22463 );
22464
22465 workspace
22466 .update(cx, |workspace, window, cx| {
22467 workspace.active_pane().update(cx, |pane, cx| {
22468 pane.navigate_backward(window, cx);
22469 })
22470 })
22471 .unwrap();
22472 color_request_handle.next().await.unwrap();
22473 cx.run_until_parked();
22474 assert_eq!(
22475 5,
22476 requests_made.load(atomic::Ordering::Acquire),
22477 "After navigating back to an editor and reopening it, another color request should be made"
22478 );
22479 let editor = workspace
22480 .update(cx, |workspace, _, cx| {
22481 workspace
22482 .active_item(cx)
22483 .expect("Should have reopened the editor again after navigating back")
22484 .downcast::<Editor>()
22485 .expect("Should be an editor")
22486 })
22487 .unwrap();
22488 editor.update(cx, |editor, cx| {
22489 assert_eq!(
22490 vec![expected_color],
22491 extract_color_inlays(editor, cx),
22492 "Should have an initial inlay"
22493 );
22494 });
22495}
22496
22497#[track_caller]
22498fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
22499 editor
22500 .all_inlays(cx)
22501 .into_iter()
22502 .filter_map(|inlay| inlay.get_color())
22503 .map(Rgba::from)
22504 .collect()
22505}