1use super::*;
2use crate::{
3 JoinLines,
4 code_context_menus::CodeContextMenu,
5 edit_prediction_tests::FakeEditPredictionProvider,
6 element::StickyHeader,
7 linked_editing_ranges::LinkedEditingRanges,
8 scroll::scroll_amount::ScrollAmount,
9 test::{
10 assert_text_with_selections, build_editor,
11 editor_lsp_test_context::{EditorLspTestContext, git_commit_lang},
12 editor_test_context::EditorTestContext,
13 select_ranges,
14 },
15};
16use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
17use collections::HashMap;
18use futures::{StreamExt, channel::oneshot};
19use gpui::{
20 BackgroundExecutor, DismissEvent, Rgba, SemanticVersion, TestAppContext, UpdateGlobal,
21 VisualTestContext, WindowBounds, WindowOptions, div,
22};
23use indoc::indoc;
24use language::{
25 BracketPairConfig,
26 Capability::ReadWrite,
27 DiagnosticSourceKind, FakeLspAdapter, IndentGuideSettings, LanguageConfig,
28 LanguageConfigOverride, LanguageMatcher, LanguageName, Override, Point,
29 language_settings::{
30 CompletionSettingsContent, FormatterList, LanguageSettingsContent, LspInsertMode,
31 },
32 tree_sitter_python,
33};
34use language_settings::Formatter;
35use languages::rust_lang;
36use lsp::CompletionParams;
37use multi_buffer::{IndentGuide, PathKey};
38use parking_lot::Mutex;
39use pretty_assertions::{assert_eq, assert_ne};
40use project::{
41 FakeFs,
42 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
43 project_settings::LspSettings,
44};
45use serde_json::{self, json};
46use settings::{
47 AllLanguageSettingsContent, IndentGuideBackgroundColoring, IndentGuideColoring,
48 ProjectSettingsContent,
49};
50use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
51use std::{
52 iter,
53 sync::atomic::{self, AtomicUsize},
54};
55use test::build_editor_with_project;
56use text::ToPoint as _;
57use unindent::Unindent;
58use util::{
59 assert_set_eq, path,
60 rel_path::rel_path,
61 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
62 uri,
63};
64use workspace::{
65 CloseActiveItem, CloseAllItems, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry,
66 OpenOptions, ViewId,
67 invalid_item_view::InvalidItemView,
68 item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
69 register_project_item,
70};
71
72fn display_ranges(editor: &Editor, cx: &mut Context<'_, Editor>) -> Vec<Range<DisplayPoint>> {
73 editor
74 .selections
75 .display_ranges(&editor.display_snapshot(cx))
76}
77
78#[gpui::test]
79fn test_edit_events(cx: &mut TestAppContext) {
80 init_test(cx, |_| {});
81
82 let buffer = cx.new(|cx| {
83 let mut buffer = language::Buffer::local("123456", cx);
84 buffer.set_group_interval(Duration::from_secs(1));
85 buffer
86 });
87
88 let events = Rc::new(RefCell::new(Vec::new()));
89 let editor1 = cx.add_window({
90 let events = events.clone();
91 |window, cx| {
92 let entity = cx.entity();
93 cx.subscribe_in(
94 &entity,
95 window,
96 move |_, _, event: &EditorEvent, _, _| match event {
97 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
98 EditorEvent::BufferEdited => {
99 events.borrow_mut().push(("editor1", "buffer edited"))
100 }
101 _ => {}
102 },
103 )
104 .detach();
105 Editor::for_buffer(buffer.clone(), None, window, cx)
106 }
107 });
108
109 let editor2 = cx.add_window({
110 let events = events.clone();
111 |window, cx| {
112 cx.subscribe_in(
113 &cx.entity(),
114 window,
115 move |_, _, event: &EditorEvent, _, _| match event {
116 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
117 EditorEvent::BufferEdited => {
118 events.borrow_mut().push(("editor2", "buffer edited"))
119 }
120 _ => {}
121 },
122 )
123 .detach();
124 Editor::for_buffer(buffer.clone(), None, window, cx)
125 }
126 });
127
128 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
129
130 // Mutating editor 1 will emit an `Edited` event only for that editor.
131 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
132 assert_eq!(
133 mem::take(&mut *events.borrow_mut()),
134 [
135 ("editor1", "edited"),
136 ("editor1", "buffer edited"),
137 ("editor2", "buffer edited"),
138 ]
139 );
140
141 // Mutating editor 2 will emit an `Edited` event only for that editor.
142 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
143 assert_eq!(
144 mem::take(&mut *events.borrow_mut()),
145 [
146 ("editor2", "edited"),
147 ("editor1", "buffer edited"),
148 ("editor2", "buffer edited"),
149 ]
150 );
151
152 // Undoing on editor 1 will emit an `Edited` event only for that editor.
153 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
154 assert_eq!(
155 mem::take(&mut *events.borrow_mut()),
156 [
157 ("editor1", "edited"),
158 ("editor1", "buffer edited"),
159 ("editor2", "buffer edited"),
160 ]
161 );
162
163 // Redoing on editor 1 will emit an `Edited` event only for that editor.
164 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
165 assert_eq!(
166 mem::take(&mut *events.borrow_mut()),
167 [
168 ("editor1", "edited"),
169 ("editor1", "buffer edited"),
170 ("editor2", "buffer edited"),
171 ]
172 );
173
174 // Undoing on editor 2 will emit an `Edited` event only for that editor.
175 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
176 assert_eq!(
177 mem::take(&mut *events.borrow_mut()),
178 [
179 ("editor2", "edited"),
180 ("editor1", "buffer edited"),
181 ("editor2", "buffer edited"),
182 ]
183 );
184
185 // Redoing on editor 2 will emit an `Edited` event only for that editor.
186 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
187 assert_eq!(
188 mem::take(&mut *events.borrow_mut()),
189 [
190 ("editor2", "edited"),
191 ("editor1", "buffer edited"),
192 ("editor2", "buffer edited"),
193 ]
194 );
195
196 // No event is emitted when the mutation is a no-op.
197 _ = editor2.update(cx, |editor, window, cx| {
198 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
199 s.select_ranges([0..0])
200 });
201
202 editor.backspace(&Backspace, window, cx);
203 });
204 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
205}
206
207#[gpui::test]
208fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
209 init_test(cx, |_| {});
210
211 let mut now = Instant::now();
212 let group_interval = Duration::from_millis(1);
213 let buffer = cx.new(|cx| {
214 let mut buf = language::Buffer::local("123456", cx);
215 buf.set_group_interval(group_interval);
216 buf
217 });
218 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
219 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
220
221 _ = editor.update(cx, |editor, window, cx| {
222 editor.start_transaction_at(now, window, cx);
223 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
224 s.select_ranges([2..4])
225 });
226
227 editor.insert("cd", window, cx);
228 editor.end_transaction_at(now, cx);
229 assert_eq!(editor.text(cx), "12cd56");
230 assert_eq!(
231 editor.selections.ranges(&editor.display_snapshot(cx)),
232 vec![4..4]
233 );
234
235 editor.start_transaction_at(now, window, cx);
236 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
237 s.select_ranges([4..5])
238 });
239 editor.insert("e", window, cx);
240 editor.end_transaction_at(now, cx);
241 assert_eq!(editor.text(cx), "12cde6");
242 assert_eq!(
243 editor.selections.ranges(&editor.display_snapshot(cx)),
244 vec![5..5]
245 );
246
247 now += group_interval + Duration::from_millis(1);
248 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
249 s.select_ranges([2..2])
250 });
251
252 // Simulate an edit in another editor
253 buffer.update(cx, |buffer, cx| {
254 buffer.start_transaction_at(now, cx);
255 buffer.edit([(0..1, "a")], None, cx);
256 buffer.edit([(1..1, "b")], None, cx);
257 buffer.end_transaction_at(now, cx);
258 });
259
260 assert_eq!(editor.text(cx), "ab2cde6");
261 assert_eq!(
262 editor.selections.ranges(&editor.display_snapshot(cx)),
263 vec![3..3]
264 );
265
266 // Last transaction happened past the group interval in a different editor.
267 // Undo it individually and don't restore selections.
268 editor.undo(&Undo, window, cx);
269 assert_eq!(editor.text(cx), "12cde6");
270 assert_eq!(
271 editor.selections.ranges(&editor.display_snapshot(cx)),
272 vec![2..2]
273 );
274
275 // First two transactions happened within the group interval in this editor.
276 // Undo them together and restore selections.
277 editor.undo(&Undo, window, cx);
278 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
279 assert_eq!(editor.text(cx), "123456");
280 assert_eq!(
281 editor.selections.ranges(&editor.display_snapshot(cx)),
282 vec![0..0]
283 );
284
285 // Redo the first two transactions together.
286 editor.redo(&Redo, window, cx);
287 assert_eq!(editor.text(cx), "12cde6");
288 assert_eq!(
289 editor.selections.ranges(&editor.display_snapshot(cx)),
290 vec![5..5]
291 );
292
293 // Redo the last transaction on its own.
294 editor.redo(&Redo, window, cx);
295 assert_eq!(editor.text(cx), "ab2cde6");
296 assert_eq!(
297 editor.selections.ranges(&editor.display_snapshot(cx)),
298 vec![6..6]
299 );
300
301 // Test empty transactions.
302 editor.start_transaction_at(now, window, cx);
303 editor.end_transaction_at(now, cx);
304 editor.undo(&Undo, window, cx);
305 assert_eq!(editor.text(cx), "12cde6");
306 });
307}
308
309#[gpui::test]
310fn test_ime_composition(cx: &mut TestAppContext) {
311 init_test(cx, |_| {});
312
313 let buffer = cx.new(|cx| {
314 let mut buffer = language::Buffer::local("abcde", cx);
315 // Ensure automatic grouping doesn't occur.
316 buffer.set_group_interval(Duration::ZERO);
317 buffer
318 });
319
320 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
321 cx.add_window(|window, cx| {
322 let mut editor = build_editor(buffer.clone(), window, cx);
323
324 // Start a new IME composition.
325 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
326 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
327 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
328 assert_eq!(editor.text(cx), "äbcde");
329 assert_eq!(
330 editor.marked_text_ranges(cx),
331 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
332 );
333
334 // Finalize IME composition.
335 editor.replace_text_in_range(None, "ā", window, cx);
336 assert_eq!(editor.text(cx), "ābcde");
337 assert_eq!(editor.marked_text_ranges(cx), None);
338
339 // IME composition edits are grouped and are undone/redone at once.
340 editor.undo(&Default::default(), window, cx);
341 assert_eq!(editor.text(cx), "abcde");
342 assert_eq!(editor.marked_text_ranges(cx), None);
343 editor.redo(&Default::default(), window, cx);
344 assert_eq!(editor.text(cx), "ābcde");
345 assert_eq!(editor.marked_text_ranges(cx), None);
346
347 // Start a new IME composition.
348 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
349 assert_eq!(
350 editor.marked_text_ranges(cx),
351 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
352 );
353
354 // Undoing during an IME composition cancels it.
355 editor.undo(&Default::default(), window, cx);
356 assert_eq!(editor.text(cx), "ābcde");
357 assert_eq!(editor.marked_text_ranges(cx), None);
358
359 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
360 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
361 assert_eq!(editor.text(cx), "ābcdè");
362 assert_eq!(
363 editor.marked_text_ranges(cx),
364 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
365 );
366
367 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
368 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
369 assert_eq!(editor.text(cx), "ābcdę");
370 assert_eq!(editor.marked_text_ranges(cx), None);
371
372 // Start a new IME composition with multiple cursors.
373 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
374 s.select_ranges([
375 OffsetUtf16(1)..OffsetUtf16(1),
376 OffsetUtf16(3)..OffsetUtf16(3),
377 OffsetUtf16(5)..OffsetUtf16(5),
378 ])
379 });
380 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
381 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
382 assert_eq!(
383 editor.marked_text_ranges(cx),
384 Some(vec![
385 OffsetUtf16(0)..OffsetUtf16(3),
386 OffsetUtf16(4)..OffsetUtf16(7),
387 OffsetUtf16(8)..OffsetUtf16(11)
388 ])
389 );
390
391 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
392 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
393 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
394 assert_eq!(
395 editor.marked_text_ranges(cx),
396 Some(vec![
397 OffsetUtf16(1)..OffsetUtf16(2),
398 OffsetUtf16(5)..OffsetUtf16(6),
399 OffsetUtf16(9)..OffsetUtf16(10)
400 ])
401 );
402
403 // Finalize IME composition with multiple cursors.
404 editor.replace_text_in_range(Some(9..10), "2", window, cx);
405 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
406 assert_eq!(editor.marked_text_ranges(cx), None);
407
408 editor
409 });
410}
411
412#[gpui::test]
413fn test_selection_with_mouse(cx: &mut TestAppContext) {
414 init_test(cx, |_| {});
415
416 let editor = cx.add_window(|window, cx| {
417 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
418 build_editor(buffer, window, cx)
419 });
420
421 _ = editor.update(cx, |editor, window, cx| {
422 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
423 });
424 assert_eq!(
425 editor
426 .update(cx, |editor, _, cx| display_ranges(editor, cx))
427 .unwrap(),
428 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
429 );
430
431 _ = editor.update(cx, |editor, window, cx| {
432 editor.update_selection(
433 DisplayPoint::new(DisplayRow(3), 3),
434 0,
435 gpui::Point::<f32>::default(),
436 window,
437 cx,
438 );
439 });
440
441 assert_eq!(
442 editor
443 .update(cx, |editor, _, cx| display_ranges(editor, cx))
444 .unwrap(),
445 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
446 );
447
448 _ = editor.update(cx, |editor, window, cx| {
449 editor.update_selection(
450 DisplayPoint::new(DisplayRow(1), 1),
451 0,
452 gpui::Point::<f32>::default(),
453 window,
454 cx,
455 );
456 });
457
458 assert_eq!(
459 editor
460 .update(cx, |editor, _, cx| display_ranges(editor, cx))
461 .unwrap(),
462 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
463 );
464
465 _ = editor.update(cx, |editor, window, cx| {
466 editor.end_selection(window, cx);
467 editor.update_selection(
468 DisplayPoint::new(DisplayRow(3), 3),
469 0,
470 gpui::Point::<f32>::default(),
471 window,
472 cx,
473 );
474 });
475
476 assert_eq!(
477 editor
478 .update(cx, |editor, _, cx| display_ranges(editor, cx))
479 .unwrap(),
480 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
481 );
482
483 _ = editor.update(cx, |editor, window, cx| {
484 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
485 editor.update_selection(
486 DisplayPoint::new(DisplayRow(0), 0),
487 0,
488 gpui::Point::<f32>::default(),
489 window,
490 cx,
491 );
492 });
493
494 assert_eq!(
495 editor
496 .update(cx, |editor, _, cx| display_ranges(editor, cx))
497 .unwrap(),
498 [
499 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
500 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
501 ]
502 );
503
504 _ = editor.update(cx, |editor, window, cx| {
505 editor.end_selection(window, cx);
506 });
507
508 assert_eq!(
509 editor
510 .update(cx, |editor, _, cx| display_ranges(editor, cx))
511 .unwrap(),
512 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
513 );
514}
515
516#[gpui::test]
517fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
518 init_test(cx, |_| {});
519
520 let editor = cx.add_window(|window, cx| {
521 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
522 build_editor(buffer, window, cx)
523 });
524
525 _ = editor.update(cx, |editor, window, cx| {
526 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
527 });
528
529 _ = editor.update(cx, |editor, window, cx| {
530 editor.end_selection(window, cx);
531 });
532
533 _ = editor.update(cx, |editor, window, cx| {
534 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
535 });
536
537 _ = editor.update(cx, |editor, window, cx| {
538 editor.end_selection(window, cx);
539 });
540
541 assert_eq!(
542 editor
543 .update(cx, |editor, _, cx| display_ranges(editor, cx))
544 .unwrap(),
545 [
546 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
547 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
548 ]
549 );
550
551 _ = editor.update(cx, |editor, window, cx| {
552 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
553 });
554
555 _ = editor.update(cx, |editor, window, cx| {
556 editor.end_selection(window, cx);
557 });
558
559 assert_eq!(
560 editor
561 .update(cx, |editor, _, cx| display_ranges(editor, cx))
562 .unwrap(),
563 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
564 );
565}
566
567#[gpui::test]
568fn test_canceling_pending_selection(cx: &mut TestAppContext) {
569 init_test(cx, |_| {});
570
571 let editor = cx.add_window(|window, cx| {
572 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
573 build_editor(buffer, window, cx)
574 });
575
576 _ = editor.update(cx, |editor, window, cx| {
577 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
578 assert_eq!(
579 display_ranges(editor, cx),
580 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
581 );
582 });
583
584 _ = editor.update(cx, |editor, window, cx| {
585 editor.update_selection(
586 DisplayPoint::new(DisplayRow(3), 3),
587 0,
588 gpui::Point::<f32>::default(),
589 window,
590 cx,
591 );
592 assert_eq!(
593 display_ranges(editor, cx),
594 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
595 );
596 });
597
598 _ = editor.update(cx, |editor, window, cx| {
599 editor.cancel(&Cancel, window, cx);
600 editor.update_selection(
601 DisplayPoint::new(DisplayRow(1), 1),
602 0,
603 gpui::Point::<f32>::default(),
604 window,
605 cx,
606 );
607 assert_eq!(
608 display_ranges(editor, cx),
609 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
610 );
611 });
612}
613
614#[gpui::test]
615fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
616 init_test(cx, |_| {});
617
618 let editor = cx.add_window(|window, cx| {
619 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
620 build_editor(buffer, window, cx)
621 });
622
623 _ = editor.update(cx, |editor, window, cx| {
624 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
625 assert_eq!(
626 display_ranges(editor, cx),
627 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
628 );
629
630 editor.move_down(&Default::default(), window, cx);
631 assert_eq!(
632 display_ranges(editor, cx),
633 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
634 );
635
636 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
637 assert_eq!(
638 display_ranges(editor, cx),
639 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
640 );
641
642 editor.move_up(&Default::default(), window, cx);
643 assert_eq!(
644 display_ranges(editor, cx),
645 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
646 );
647 });
648}
649
650#[gpui::test]
651fn test_extending_selection(cx: &mut TestAppContext) {
652 init_test(cx, |_| {});
653
654 let editor = cx.add_window(|window, cx| {
655 let buffer = MultiBuffer::build_simple("aaa bbb ccc ddd eee", cx);
656 build_editor(buffer, window, cx)
657 });
658
659 _ = editor.update(cx, |editor, window, cx| {
660 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), false, 1, window, cx);
661 editor.end_selection(window, cx);
662 assert_eq!(
663 display_ranges(editor, cx),
664 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)]
665 );
666
667 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
668 editor.end_selection(window, cx);
669 assert_eq!(
670 display_ranges(editor, cx),
671 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 10)]
672 );
673
674 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
675 editor.end_selection(window, cx);
676 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 2, window, cx);
677 assert_eq!(
678 display_ranges(editor, cx),
679 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 11)]
680 );
681
682 editor.update_selection(
683 DisplayPoint::new(DisplayRow(0), 1),
684 0,
685 gpui::Point::<f32>::default(),
686 window,
687 cx,
688 );
689 editor.end_selection(window, cx);
690 assert_eq!(
691 display_ranges(editor, cx),
692 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 0)]
693 );
694
695 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), true, 1, window, cx);
696 editor.end_selection(window, cx);
697 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), true, 2, window, cx);
698 editor.end_selection(window, cx);
699 assert_eq!(
700 display_ranges(editor, cx),
701 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 7)]
702 );
703
704 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
705 assert_eq!(
706 display_ranges(editor, cx),
707 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 11)]
708 );
709
710 editor.update_selection(
711 DisplayPoint::new(DisplayRow(0), 6),
712 0,
713 gpui::Point::<f32>::default(),
714 window,
715 cx,
716 );
717 assert_eq!(
718 display_ranges(editor, cx),
719 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 7)]
720 );
721
722 editor.update_selection(
723 DisplayPoint::new(DisplayRow(0), 1),
724 0,
725 gpui::Point::<f32>::default(),
726 window,
727 cx,
728 );
729 editor.end_selection(window, cx);
730 assert_eq!(
731 display_ranges(editor, cx),
732 [DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 0)]
733 );
734 });
735}
736
737#[gpui::test]
738fn test_clone(cx: &mut TestAppContext) {
739 init_test(cx, |_| {});
740
741 let (text, selection_ranges) = marked_text_ranges(
742 indoc! {"
743 one
744 two
745 threeˇ
746 four
747 fiveˇ
748 "},
749 true,
750 );
751
752 let editor = cx.add_window(|window, cx| {
753 let buffer = MultiBuffer::build_simple(&text, cx);
754 build_editor(buffer, window, cx)
755 });
756
757 _ = editor.update(cx, |editor, window, cx| {
758 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
759 s.select_ranges(selection_ranges.clone())
760 });
761 editor.fold_creases(
762 vec![
763 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
764 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
765 ],
766 true,
767 window,
768 cx,
769 );
770 });
771
772 let cloned_editor = editor
773 .update(cx, |editor, _, cx| {
774 cx.open_window(Default::default(), |window, cx| {
775 cx.new(|cx| editor.clone(window, cx))
776 })
777 })
778 .unwrap()
779 .unwrap();
780
781 let snapshot = editor
782 .update(cx, |e, window, cx| e.snapshot(window, cx))
783 .unwrap();
784 let cloned_snapshot = cloned_editor
785 .update(cx, |e, window, cx| e.snapshot(window, cx))
786 .unwrap();
787
788 assert_eq!(
789 cloned_editor
790 .update(cx, |e, _, cx| e.display_text(cx))
791 .unwrap(),
792 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
793 );
794 assert_eq!(
795 cloned_snapshot
796 .folds_in_range(0..text.len())
797 .collect::<Vec<_>>(),
798 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
799 );
800 assert_set_eq!(
801 cloned_editor
802 .update(cx, |editor, _, cx| editor
803 .selections
804 .ranges::<Point>(&editor.display_snapshot(cx)))
805 .unwrap(),
806 editor
807 .update(cx, |editor, _, cx| editor
808 .selections
809 .ranges(&editor.display_snapshot(cx)))
810 .unwrap()
811 );
812 assert_set_eq!(
813 cloned_editor
814 .update(cx, |e, _window, cx| e
815 .selections
816 .display_ranges(&e.display_snapshot(cx)))
817 .unwrap(),
818 editor
819 .update(cx, |e, _, cx| e
820 .selections
821 .display_ranges(&e.display_snapshot(cx)))
822 .unwrap()
823 );
824}
825
826#[gpui::test]
827async fn test_navigation_history(cx: &mut TestAppContext) {
828 init_test(cx, |_| {});
829
830 use workspace::item::Item;
831
832 let fs = FakeFs::new(cx.executor());
833 let project = Project::test(fs, [], cx).await;
834 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
835 let pane = workspace
836 .update(cx, |workspace, _, _| workspace.active_pane().clone())
837 .unwrap();
838
839 _ = workspace.update(cx, |_v, window, cx| {
840 cx.new(|cx| {
841 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
842 let mut editor = build_editor(buffer, window, cx);
843 let handle = cx.entity();
844 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
845
846 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
847 editor.nav_history.as_mut().unwrap().pop_backward(cx)
848 }
849
850 // Move the cursor a small distance.
851 // Nothing is added to the navigation history.
852 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
853 s.select_display_ranges([
854 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
855 ])
856 });
857 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
858 s.select_display_ranges([
859 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
860 ])
861 });
862 assert!(pop_history(&mut editor, cx).is_none());
863
864 // Move the cursor a large distance.
865 // The history can jump back to the previous position.
866 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
867 s.select_display_ranges([
868 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
869 ])
870 });
871 let nav_entry = pop_history(&mut editor, cx).unwrap();
872 editor.navigate(nav_entry.data.unwrap(), window, cx);
873 assert_eq!(nav_entry.item.id(), cx.entity_id());
874 assert_eq!(
875 editor
876 .selections
877 .display_ranges(&editor.display_snapshot(cx)),
878 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
879 );
880 assert!(pop_history(&mut editor, cx).is_none());
881
882 // Move the cursor a small distance via the mouse.
883 // Nothing is added to the navigation history.
884 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
885 editor.end_selection(window, cx);
886 assert_eq!(
887 editor
888 .selections
889 .display_ranges(&editor.display_snapshot(cx)),
890 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
891 );
892 assert!(pop_history(&mut editor, cx).is_none());
893
894 // Move the cursor a large distance via the mouse.
895 // The history can jump back to the previous position.
896 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
897 editor.end_selection(window, cx);
898 assert_eq!(
899 editor
900 .selections
901 .display_ranges(&editor.display_snapshot(cx)),
902 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
903 );
904 let nav_entry = pop_history(&mut editor, cx).unwrap();
905 editor.navigate(nav_entry.data.unwrap(), window, cx);
906 assert_eq!(nav_entry.item.id(), cx.entity_id());
907 assert_eq!(
908 editor
909 .selections
910 .display_ranges(&editor.display_snapshot(cx)),
911 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
912 );
913 assert!(pop_history(&mut editor, cx).is_none());
914
915 // Set scroll position to check later
916 editor.set_scroll_position(gpui::Point::<f64>::new(5.5, 5.5), window, cx);
917 let original_scroll_position = editor.scroll_manager.anchor();
918
919 // Jump to the end of the document and adjust scroll
920 editor.move_to_end(&MoveToEnd, window, cx);
921 editor.set_scroll_position(gpui::Point::<f64>::new(-2.5, -0.5), window, cx);
922 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
923
924 let nav_entry = pop_history(&mut editor, cx).unwrap();
925 editor.navigate(nav_entry.data.unwrap(), window, cx);
926 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
927
928 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
929 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
930 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
931 let invalid_point = Point::new(9999, 0);
932 editor.navigate(
933 Box::new(NavigationData {
934 cursor_anchor: invalid_anchor,
935 cursor_position: invalid_point,
936 scroll_anchor: ScrollAnchor {
937 anchor: invalid_anchor,
938 offset: Default::default(),
939 },
940 scroll_top_row: invalid_point.row,
941 }),
942 window,
943 cx,
944 );
945 assert_eq!(
946 editor
947 .selections
948 .display_ranges(&editor.display_snapshot(cx)),
949 &[editor.max_point(cx)..editor.max_point(cx)]
950 );
951 assert_eq!(
952 editor.scroll_position(cx),
953 gpui::Point::new(0., editor.max_point(cx).row().as_f64())
954 );
955
956 editor
957 })
958 });
959}
960
961#[gpui::test]
962fn test_cancel(cx: &mut TestAppContext) {
963 init_test(cx, |_| {});
964
965 let editor = cx.add_window(|window, cx| {
966 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
967 build_editor(buffer, window, cx)
968 });
969
970 _ = editor.update(cx, |editor, window, cx| {
971 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
972 editor.update_selection(
973 DisplayPoint::new(DisplayRow(1), 1),
974 0,
975 gpui::Point::<f32>::default(),
976 window,
977 cx,
978 );
979 editor.end_selection(window, cx);
980
981 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
982 editor.update_selection(
983 DisplayPoint::new(DisplayRow(0), 3),
984 0,
985 gpui::Point::<f32>::default(),
986 window,
987 cx,
988 );
989 editor.end_selection(window, cx);
990 assert_eq!(
991 display_ranges(editor, cx),
992 [
993 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
994 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
995 ]
996 );
997 });
998
999 _ = editor.update(cx, |editor, window, cx| {
1000 editor.cancel(&Cancel, window, cx);
1001 assert_eq!(
1002 display_ranges(editor, cx),
1003 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
1004 );
1005 });
1006
1007 _ = editor.update(cx, |editor, window, cx| {
1008 editor.cancel(&Cancel, window, cx);
1009 assert_eq!(
1010 display_ranges(editor, cx),
1011 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
1012 );
1013 });
1014}
1015
1016#[gpui::test]
1017fn test_fold_action(cx: &mut TestAppContext) {
1018 init_test(cx, |_| {});
1019
1020 let editor = cx.add_window(|window, cx| {
1021 let buffer = MultiBuffer::build_simple(
1022 &"
1023 impl Foo {
1024 // Hello!
1025
1026 fn a() {
1027 1
1028 }
1029
1030 fn b() {
1031 2
1032 }
1033
1034 fn c() {
1035 3
1036 }
1037 }
1038 "
1039 .unindent(),
1040 cx,
1041 );
1042 build_editor(buffer, window, cx)
1043 });
1044
1045 _ = editor.update(cx, |editor, window, cx| {
1046 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1047 s.select_display_ranges([
1048 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
1049 ]);
1050 });
1051 editor.fold(&Fold, window, cx);
1052 assert_eq!(
1053 editor.display_text(cx),
1054 "
1055 impl Foo {
1056 // Hello!
1057
1058 fn a() {
1059 1
1060 }
1061
1062 fn b() {⋯
1063 }
1064
1065 fn c() {⋯
1066 }
1067 }
1068 "
1069 .unindent(),
1070 );
1071
1072 editor.fold(&Fold, window, cx);
1073 assert_eq!(
1074 editor.display_text(cx),
1075 "
1076 impl Foo {⋯
1077 }
1078 "
1079 .unindent(),
1080 );
1081
1082 editor.unfold_lines(&UnfoldLines, window, cx);
1083 assert_eq!(
1084 editor.display_text(cx),
1085 "
1086 impl Foo {
1087 // Hello!
1088
1089 fn a() {
1090 1
1091 }
1092
1093 fn b() {⋯
1094 }
1095
1096 fn c() {⋯
1097 }
1098 }
1099 "
1100 .unindent(),
1101 );
1102
1103 editor.unfold_lines(&UnfoldLines, window, cx);
1104 assert_eq!(
1105 editor.display_text(cx),
1106 editor.buffer.read(cx).read(cx).text()
1107 );
1108 });
1109}
1110
1111#[gpui::test]
1112fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
1113 init_test(cx, |_| {});
1114
1115 let editor = cx.add_window(|window, cx| {
1116 let buffer = MultiBuffer::build_simple(
1117 &"
1118 class Foo:
1119 # Hello!
1120
1121 def a():
1122 print(1)
1123
1124 def b():
1125 print(2)
1126
1127 def c():
1128 print(3)
1129 "
1130 .unindent(),
1131 cx,
1132 );
1133 build_editor(buffer, window, cx)
1134 });
1135
1136 _ = editor.update(cx, |editor, window, cx| {
1137 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1138 s.select_display_ranges([
1139 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
1140 ]);
1141 });
1142 editor.fold(&Fold, window, cx);
1143 assert_eq!(
1144 editor.display_text(cx),
1145 "
1146 class Foo:
1147 # Hello!
1148
1149 def a():
1150 print(1)
1151
1152 def b():⋯
1153
1154 def c():⋯
1155 "
1156 .unindent(),
1157 );
1158
1159 editor.fold(&Fold, window, cx);
1160 assert_eq!(
1161 editor.display_text(cx),
1162 "
1163 class Foo:⋯
1164 "
1165 .unindent(),
1166 );
1167
1168 editor.unfold_lines(&UnfoldLines, window, cx);
1169 assert_eq!(
1170 editor.display_text(cx),
1171 "
1172 class Foo:
1173 # Hello!
1174
1175 def a():
1176 print(1)
1177
1178 def b():⋯
1179
1180 def c():⋯
1181 "
1182 .unindent(),
1183 );
1184
1185 editor.unfold_lines(&UnfoldLines, window, cx);
1186 assert_eq!(
1187 editor.display_text(cx),
1188 editor.buffer.read(cx).read(cx).text()
1189 );
1190 });
1191}
1192
1193#[gpui::test]
1194fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1195 init_test(cx, |_| {});
1196
1197 let editor = cx.add_window(|window, cx| {
1198 let buffer = MultiBuffer::build_simple(
1199 &"
1200 class Foo:
1201 # Hello!
1202
1203 def a():
1204 print(1)
1205
1206 def b():
1207 print(2)
1208
1209
1210 def c():
1211 print(3)
1212
1213
1214 "
1215 .unindent(),
1216 cx,
1217 );
1218 build_editor(buffer, window, cx)
1219 });
1220
1221 _ = editor.update(cx, |editor, window, cx| {
1222 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1223 s.select_display_ranges([
1224 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1225 ]);
1226 });
1227 editor.fold(&Fold, window, cx);
1228 assert_eq!(
1229 editor.display_text(cx),
1230 "
1231 class Foo:
1232 # Hello!
1233
1234 def a():
1235 print(1)
1236
1237 def b():⋯
1238
1239
1240 def c():⋯
1241
1242
1243 "
1244 .unindent(),
1245 );
1246
1247 editor.fold(&Fold, window, cx);
1248 assert_eq!(
1249 editor.display_text(cx),
1250 "
1251 class Foo:⋯
1252
1253
1254 "
1255 .unindent(),
1256 );
1257
1258 editor.unfold_lines(&UnfoldLines, window, cx);
1259 assert_eq!(
1260 editor.display_text(cx),
1261 "
1262 class Foo:
1263 # Hello!
1264
1265 def a():
1266 print(1)
1267
1268 def b():⋯
1269
1270
1271 def c():⋯
1272
1273
1274 "
1275 .unindent(),
1276 );
1277
1278 editor.unfold_lines(&UnfoldLines, window, cx);
1279 assert_eq!(
1280 editor.display_text(cx),
1281 editor.buffer.read(cx).read(cx).text()
1282 );
1283 });
1284}
1285
1286#[gpui::test]
1287fn test_fold_at_level(cx: &mut TestAppContext) {
1288 init_test(cx, |_| {});
1289
1290 let editor = cx.add_window(|window, cx| {
1291 let buffer = MultiBuffer::build_simple(
1292 &"
1293 class Foo:
1294 # Hello!
1295
1296 def a():
1297 print(1)
1298
1299 def b():
1300 print(2)
1301
1302
1303 class Bar:
1304 # World!
1305
1306 def a():
1307 print(1)
1308
1309 def b():
1310 print(2)
1311
1312
1313 "
1314 .unindent(),
1315 cx,
1316 );
1317 build_editor(buffer, window, cx)
1318 });
1319
1320 _ = editor.update(cx, |editor, window, cx| {
1321 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1322 assert_eq!(
1323 editor.display_text(cx),
1324 "
1325 class Foo:
1326 # Hello!
1327
1328 def a():⋯
1329
1330 def b():⋯
1331
1332
1333 class Bar:
1334 # World!
1335
1336 def a():⋯
1337
1338 def b():⋯
1339
1340
1341 "
1342 .unindent(),
1343 );
1344
1345 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1346 assert_eq!(
1347 editor.display_text(cx),
1348 "
1349 class Foo:⋯
1350
1351
1352 class Bar:⋯
1353
1354
1355 "
1356 .unindent(),
1357 );
1358
1359 editor.unfold_all(&UnfoldAll, window, cx);
1360 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1361 assert_eq!(
1362 editor.display_text(cx),
1363 "
1364 class Foo:
1365 # Hello!
1366
1367 def a():
1368 print(1)
1369
1370 def b():
1371 print(2)
1372
1373
1374 class Bar:
1375 # World!
1376
1377 def a():
1378 print(1)
1379
1380 def b():
1381 print(2)
1382
1383
1384 "
1385 .unindent(),
1386 );
1387
1388 assert_eq!(
1389 editor.display_text(cx),
1390 editor.buffer.read(cx).read(cx).text()
1391 );
1392 let (_, positions) = marked_text_ranges(
1393 &"
1394 class Foo:
1395 # Hello!
1396
1397 def a():
1398 print(1)
1399
1400 def b():
1401 p«riˇ»nt(2)
1402
1403
1404 class Bar:
1405 # World!
1406
1407 def a():
1408 «ˇprint(1)
1409
1410 def b():
1411 print(2)»
1412
1413
1414 "
1415 .unindent(),
1416 true,
1417 );
1418
1419 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
1420 s.select_ranges(positions)
1421 });
1422
1423 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1424 assert_eq!(
1425 editor.display_text(cx),
1426 "
1427 class Foo:
1428 # Hello!
1429
1430 def a():⋯
1431
1432 def b():
1433 print(2)
1434
1435
1436 class Bar:
1437 # World!
1438
1439 def a():
1440 print(1)
1441
1442 def b():
1443 print(2)
1444
1445
1446 "
1447 .unindent(),
1448 );
1449 });
1450}
1451
1452#[gpui::test]
1453fn test_move_cursor(cx: &mut TestAppContext) {
1454 init_test(cx, |_| {});
1455
1456 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1457 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1458
1459 buffer.update(cx, |buffer, cx| {
1460 buffer.edit(
1461 vec![
1462 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1463 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1464 ],
1465 None,
1466 cx,
1467 );
1468 });
1469 _ = editor.update(cx, |editor, window, cx| {
1470 assert_eq!(
1471 display_ranges(editor, cx),
1472 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1473 );
1474
1475 editor.move_down(&MoveDown, window, cx);
1476 assert_eq!(
1477 display_ranges(editor, cx),
1478 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1479 );
1480
1481 editor.move_right(&MoveRight, window, cx);
1482 assert_eq!(
1483 display_ranges(editor, cx),
1484 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1485 );
1486
1487 editor.move_left(&MoveLeft, window, cx);
1488 assert_eq!(
1489 display_ranges(editor, cx),
1490 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1491 );
1492
1493 editor.move_up(&MoveUp, window, cx);
1494 assert_eq!(
1495 display_ranges(editor, cx),
1496 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1497 );
1498
1499 editor.move_to_end(&MoveToEnd, window, cx);
1500 assert_eq!(
1501 display_ranges(editor, cx),
1502 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1503 );
1504
1505 editor.move_to_beginning(&MoveToBeginning, window, cx);
1506 assert_eq!(
1507 display_ranges(editor, cx),
1508 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1509 );
1510
1511 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1512 s.select_display_ranges([
1513 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1514 ]);
1515 });
1516 editor.select_to_beginning(&SelectToBeginning, window, cx);
1517 assert_eq!(
1518 display_ranges(editor, cx),
1519 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1520 );
1521
1522 editor.select_to_end(&SelectToEnd, window, cx);
1523 assert_eq!(
1524 display_ranges(editor, cx),
1525 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1526 );
1527 });
1528}
1529
1530#[gpui::test]
1531fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1532 init_test(cx, |_| {});
1533
1534 let editor = cx.add_window(|window, cx| {
1535 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1536 build_editor(buffer, window, cx)
1537 });
1538
1539 assert_eq!('🟥'.len_utf8(), 4);
1540 assert_eq!('α'.len_utf8(), 2);
1541
1542 _ = editor.update(cx, |editor, window, cx| {
1543 editor.fold_creases(
1544 vec![
1545 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1546 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1547 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1548 ],
1549 true,
1550 window,
1551 cx,
1552 );
1553 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1554
1555 editor.move_right(&MoveRight, window, cx);
1556 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥".len())]);
1557 editor.move_right(&MoveRight, window, cx);
1558 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥🟧".len())]);
1559 editor.move_right(&MoveRight, window, cx);
1560 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥🟧⋯".len())]);
1561
1562 editor.move_down(&MoveDown, window, cx);
1563 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯e".len())]);
1564 editor.move_left(&MoveLeft, window, cx);
1565 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯".len())]);
1566 editor.move_left(&MoveLeft, window, cx);
1567 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab".len())]);
1568 editor.move_left(&MoveLeft, window, cx);
1569 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "a".len())]);
1570
1571 editor.move_down(&MoveDown, window, cx);
1572 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "α".len())]);
1573 editor.move_right(&MoveRight, window, cx);
1574 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ".len())]);
1575 editor.move_right(&MoveRight, window, cx);
1576 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ⋯".len())]);
1577 editor.move_right(&MoveRight, window, cx);
1578 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ⋯ε".len())]);
1579
1580 editor.move_up(&MoveUp, window, cx);
1581 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯e".len())]);
1582 editor.move_down(&MoveDown, window, cx);
1583 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ⋯ε".len())]);
1584 editor.move_up(&MoveUp, window, cx);
1585 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯e".len())]);
1586
1587 editor.move_up(&MoveUp, window, cx);
1588 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥🟧".len())]);
1589 editor.move_left(&MoveLeft, window, cx);
1590 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥".len())]);
1591 editor.move_left(&MoveLeft, window, cx);
1592 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "".len())]);
1593 });
1594}
1595
1596#[gpui::test]
1597fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1598 init_test(cx, |_| {});
1599
1600 let editor = cx.add_window(|window, cx| {
1601 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1602 build_editor(buffer, window, cx)
1603 });
1604 _ = editor.update(cx, |editor, window, cx| {
1605 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1606 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1607 });
1608
1609 // moving above start of document should move selection to start of document,
1610 // but the next move down should still be at the original goal_x
1611 editor.move_up(&MoveUp, window, cx);
1612 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "".len())]);
1613
1614 editor.move_down(&MoveDown, window, cx);
1615 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "abcd".len())]);
1616
1617 editor.move_down(&MoveDown, window, cx);
1618 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβγ".len())]);
1619
1620 editor.move_down(&MoveDown, window, cx);
1621 assert_eq!(display_ranges(editor, cx), &[empty_range(3, "abcd".len())]);
1622
1623 editor.move_down(&MoveDown, window, cx);
1624 assert_eq!(display_ranges(editor, cx), &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]);
1625
1626 // moving past end of document should not change goal_x
1627 editor.move_down(&MoveDown, window, cx);
1628 assert_eq!(display_ranges(editor, cx), &[empty_range(5, "".len())]);
1629
1630 editor.move_down(&MoveDown, window, cx);
1631 assert_eq!(display_ranges(editor, cx), &[empty_range(5, "".len())]);
1632
1633 editor.move_up(&MoveUp, window, cx);
1634 assert_eq!(display_ranges(editor, cx), &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]);
1635
1636 editor.move_up(&MoveUp, window, cx);
1637 assert_eq!(display_ranges(editor, cx), &[empty_range(3, "abcd".len())]);
1638
1639 editor.move_up(&MoveUp, window, cx);
1640 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβγ".len())]);
1641 });
1642}
1643
1644#[gpui::test]
1645fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1646 init_test(cx, |_| {});
1647 let move_to_beg = MoveToBeginningOfLine {
1648 stop_at_soft_wraps: true,
1649 stop_at_indent: true,
1650 };
1651
1652 let delete_to_beg = DeleteToBeginningOfLine {
1653 stop_at_indent: false,
1654 };
1655
1656 let move_to_end = MoveToEndOfLine {
1657 stop_at_soft_wraps: true,
1658 };
1659
1660 let editor = cx.add_window(|window, cx| {
1661 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1662 build_editor(buffer, window, cx)
1663 });
1664 _ = editor.update(cx, |editor, window, cx| {
1665 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1666 s.select_display_ranges([
1667 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1668 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1669 ]);
1670 });
1671 });
1672
1673 _ = editor.update(cx, |editor, window, cx| {
1674 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1675 assert_eq!(
1676 display_ranges(editor, cx),
1677 &[
1678 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1679 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1680 ]
1681 );
1682 });
1683
1684 _ = editor.update(cx, |editor, window, cx| {
1685 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1686 assert_eq!(
1687 display_ranges(editor, cx),
1688 &[
1689 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1690 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1691 ]
1692 );
1693 });
1694
1695 _ = editor.update(cx, |editor, window, cx| {
1696 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1697 assert_eq!(
1698 display_ranges(editor, cx),
1699 &[
1700 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1701 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1702 ]
1703 );
1704 });
1705
1706 _ = editor.update(cx, |editor, window, cx| {
1707 editor.move_to_end_of_line(&move_to_end, window, cx);
1708 assert_eq!(
1709 display_ranges(editor, cx),
1710 &[
1711 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1712 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1713 ]
1714 );
1715 });
1716
1717 // Moving to the end of line again is a no-op.
1718 _ = editor.update(cx, |editor, window, cx| {
1719 editor.move_to_end_of_line(&move_to_end, window, cx);
1720 assert_eq!(
1721 display_ranges(editor, cx),
1722 &[
1723 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1724 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1725 ]
1726 );
1727 });
1728
1729 _ = editor.update(cx, |editor, window, cx| {
1730 editor.move_left(&MoveLeft, window, cx);
1731 editor.select_to_beginning_of_line(
1732 &SelectToBeginningOfLine {
1733 stop_at_soft_wraps: true,
1734 stop_at_indent: true,
1735 },
1736 window,
1737 cx,
1738 );
1739 assert_eq!(
1740 display_ranges(editor, cx),
1741 &[
1742 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1743 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1744 ]
1745 );
1746 });
1747
1748 _ = editor.update(cx, |editor, window, cx| {
1749 editor.select_to_beginning_of_line(
1750 &SelectToBeginningOfLine {
1751 stop_at_soft_wraps: true,
1752 stop_at_indent: true,
1753 },
1754 window,
1755 cx,
1756 );
1757 assert_eq!(
1758 display_ranges(editor, cx),
1759 &[
1760 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1761 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1762 ]
1763 );
1764 });
1765
1766 _ = editor.update(cx, |editor, window, cx| {
1767 editor.select_to_beginning_of_line(
1768 &SelectToBeginningOfLine {
1769 stop_at_soft_wraps: true,
1770 stop_at_indent: true,
1771 },
1772 window,
1773 cx,
1774 );
1775 assert_eq!(
1776 display_ranges(editor, cx),
1777 &[
1778 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1779 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1780 ]
1781 );
1782 });
1783
1784 _ = editor.update(cx, |editor, window, cx| {
1785 editor.select_to_end_of_line(
1786 &SelectToEndOfLine {
1787 stop_at_soft_wraps: true,
1788 },
1789 window,
1790 cx,
1791 );
1792 assert_eq!(
1793 display_ranges(editor, cx),
1794 &[
1795 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1796 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1797 ]
1798 );
1799 });
1800
1801 _ = editor.update(cx, |editor, window, cx| {
1802 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1803 assert_eq!(editor.display_text(cx), "ab\n de");
1804 assert_eq!(
1805 display_ranges(editor, cx),
1806 &[
1807 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1808 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1809 ]
1810 );
1811 });
1812
1813 _ = editor.update(cx, |editor, window, cx| {
1814 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1815 assert_eq!(editor.display_text(cx), "\n");
1816 assert_eq!(
1817 display_ranges(editor, cx),
1818 &[
1819 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1820 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1821 ]
1822 );
1823 });
1824}
1825
1826#[gpui::test]
1827fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1828 init_test(cx, |_| {});
1829 let move_to_beg = MoveToBeginningOfLine {
1830 stop_at_soft_wraps: false,
1831 stop_at_indent: false,
1832 };
1833
1834 let move_to_end = MoveToEndOfLine {
1835 stop_at_soft_wraps: false,
1836 };
1837
1838 let editor = cx.add_window(|window, cx| {
1839 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1840 build_editor(buffer, window, cx)
1841 });
1842
1843 _ = editor.update(cx, |editor, window, cx| {
1844 editor.set_wrap_width(Some(140.0.into()), cx);
1845
1846 // We expect the following lines after wrapping
1847 // ```
1848 // thequickbrownfox
1849 // jumpedoverthelazydo
1850 // gs
1851 // ```
1852 // The final `gs` was soft-wrapped onto a new line.
1853 assert_eq!(
1854 "thequickbrownfox\njumpedoverthelaz\nydogs",
1855 editor.display_text(cx),
1856 );
1857
1858 // First, let's assert behavior on the first line, that was not soft-wrapped.
1859 // Start the cursor at the `k` on the first line
1860 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1861 s.select_display_ranges([
1862 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1863 ]);
1864 });
1865
1866 // Moving to the beginning of the line should put us at the beginning of the line.
1867 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1868 assert_eq!(
1869 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1870 display_ranges(editor, cx)
1871 );
1872
1873 // Moving to the end of the line should put us at the end of the line.
1874 editor.move_to_end_of_line(&move_to_end, window, cx);
1875 assert_eq!(
1876 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1877 display_ranges(editor, cx)
1878 );
1879
1880 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1881 // Start the cursor at the last line (`y` that was wrapped to a new line)
1882 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1883 s.select_display_ranges([
1884 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1885 ]);
1886 });
1887
1888 // Moving to the beginning of the line should put us at the start of the second line of
1889 // display text, i.e., the `j`.
1890 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1891 assert_eq!(
1892 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1893 display_ranges(editor, cx)
1894 );
1895
1896 // Moving to the beginning of the line again should be a no-op.
1897 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1898 assert_eq!(
1899 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1900 display_ranges(editor, cx)
1901 );
1902
1903 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1904 // next display line.
1905 editor.move_to_end_of_line(&move_to_end, window, cx);
1906 assert_eq!(
1907 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1908 display_ranges(editor, cx)
1909 );
1910
1911 // Moving to the end of the line again should be a no-op.
1912 editor.move_to_end_of_line(&move_to_end, window, cx);
1913 assert_eq!(
1914 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1915 display_ranges(editor, cx)
1916 );
1917 });
1918}
1919
1920#[gpui::test]
1921fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1922 init_test(cx, |_| {});
1923
1924 let move_to_beg = MoveToBeginningOfLine {
1925 stop_at_soft_wraps: true,
1926 stop_at_indent: true,
1927 };
1928
1929 let select_to_beg = SelectToBeginningOfLine {
1930 stop_at_soft_wraps: true,
1931 stop_at_indent: true,
1932 };
1933
1934 let delete_to_beg = DeleteToBeginningOfLine {
1935 stop_at_indent: true,
1936 };
1937
1938 let move_to_end = MoveToEndOfLine {
1939 stop_at_soft_wraps: false,
1940 };
1941
1942 let editor = cx.add_window(|window, cx| {
1943 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1944 build_editor(buffer, window, cx)
1945 });
1946
1947 _ = editor.update(cx, |editor, window, cx| {
1948 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1949 s.select_display_ranges([
1950 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1951 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1952 ]);
1953 });
1954
1955 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1956 // and the second cursor at the first non-whitespace character in the line.
1957 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1958 assert_eq!(
1959 display_ranges(editor, cx),
1960 &[
1961 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1962 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1963 ]
1964 );
1965
1966 // Moving to the beginning of the line again should be a no-op for the first cursor,
1967 // and should move the second cursor to the beginning of the line.
1968 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1969 assert_eq!(
1970 display_ranges(editor, cx),
1971 &[
1972 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1973 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1974 ]
1975 );
1976
1977 // Moving to the beginning of the line again should still be a no-op for the first cursor,
1978 // and should move the second cursor back to the first non-whitespace character in the line.
1979 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1980 assert_eq!(
1981 display_ranges(editor, cx),
1982 &[
1983 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1984 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1985 ]
1986 );
1987
1988 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
1989 // and to the first non-whitespace character in the line for the second cursor.
1990 editor.move_to_end_of_line(&move_to_end, window, cx);
1991 editor.move_left(&MoveLeft, window, cx);
1992 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1993 assert_eq!(
1994 display_ranges(editor, cx),
1995 &[
1996 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1997 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1998 ]
1999 );
2000
2001 // Selecting to the beginning of the line again should be a no-op for the first cursor,
2002 // and should select to the beginning of the line for the second cursor.
2003 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
2004 assert_eq!(
2005 display_ranges(editor, cx),
2006 &[
2007 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
2008 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
2009 ]
2010 );
2011
2012 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
2013 // and should delete to the first non-whitespace character in the line for the second cursor.
2014 editor.move_to_end_of_line(&move_to_end, window, cx);
2015 editor.move_left(&MoveLeft, window, cx);
2016 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
2017 assert_eq!(editor.text(cx), "c\n f");
2018 });
2019}
2020
2021#[gpui::test]
2022fn test_beginning_of_line_with_cursor_between_line_start_and_indent(cx: &mut TestAppContext) {
2023 init_test(cx, |_| {});
2024
2025 let move_to_beg = MoveToBeginningOfLine {
2026 stop_at_soft_wraps: true,
2027 stop_at_indent: true,
2028 };
2029
2030 let editor = cx.add_window(|window, cx| {
2031 let buffer = MultiBuffer::build_simple(" hello\nworld", cx);
2032 build_editor(buffer, window, cx)
2033 });
2034
2035 _ = editor.update(cx, |editor, window, cx| {
2036 // test cursor between line_start and indent_start
2037 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2038 s.select_display_ranges([
2039 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3)
2040 ]);
2041 });
2042
2043 // cursor should move to line_start
2044 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2045 assert_eq!(
2046 display_ranges(editor, cx),
2047 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
2048 );
2049
2050 // cursor should move to indent_start
2051 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2052 assert_eq!(
2053 display_ranges(editor, cx),
2054 &[DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 4)]
2055 );
2056
2057 // cursor should move to back to line_start
2058 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2059 assert_eq!(
2060 display_ranges(editor, cx),
2061 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
2062 );
2063 });
2064}
2065
2066#[gpui::test]
2067fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
2068 init_test(cx, |_| {});
2069
2070 let editor = cx.add_window(|window, cx| {
2071 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
2072 build_editor(buffer, window, cx)
2073 });
2074 _ = editor.update(cx, |editor, window, cx| {
2075 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2076 s.select_display_ranges([
2077 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
2078 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
2079 ])
2080 });
2081 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2082 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
2083
2084 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2085 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
2086
2087 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2088 assert_selection_ranges("use ˇstd::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
2089
2090 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2091 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
2092
2093 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2094 assert_selection_ranges("ˇuse std::str::{foo, ˇbar}\n\n {baz.qux()}", editor, cx);
2095
2096 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2097 assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
2098
2099 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2100 assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
2101
2102 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2103 assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
2104
2105 editor.move_right(&MoveRight, window, cx);
2106 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2107 assert_selection_ranges(
2108 "use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}",
2109 editor,
2110 cx,
2111 );
2112
2113 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2114 assert_selection_ranges(
2115 "use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
2116 editor,
2117 cx,
2118 );
2119
2120 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
2121 assert_selection_ranges(
2122 "use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
2123 editor,
2124 cx,
2125 );
2126 });
2127}
2128
2129#[gpui::test]
2130fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
2131 init_test(cx, |_| {});
2132
2133 let editor = cx.add_window(|window, cx| {
2134 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
2135 build_editor(buffer, window, cx)
2136 });
2137
2138 _ = editor.update(cx, |editor, window, cx| {
2139 editor.set_wrap_width(Some(140.0.into()), cx);
2140 assert_eq!(
2141 editor.display_text(cx),
2142 "use one::{\n two::three::\n four::five\n};"
2143 );
2144
2145 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2146 s.select_display_ranges([
2147 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
2148 ]);
2149 });
2150
2151 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2152 assert_eq!(
2153 display_ranges(editor, cx),
2154 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
2155 );
2156
2157 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2158 assert_eq!(
2159 display_ranges(editor, cx),
2160 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2161 );
2162
2163 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2164 assert_eq!(
2165 display_ranges(editor, cx),
2166 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2167 );
2168
2169 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2170 assert_eq!(
2171 display_ranges(editor, cx),
2172 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2173 );
2174
2175 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2176 assert_eq!(
2177 display_ranges(editor, cx),
2178 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2179 );
2180
2181 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2182 assert_eq!(
2183 display_ranges(editor, cx),
2184 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2185 );
2186 });
2187}
2188
2189#[gpui::test]
2190async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2191 init_test(cx, |_| {});
2192 let mut cx = EditorTestContext::new(cx).await;
2193
2194 let line_height = cx.editor(|editor, window, _| {
2195 editor
2196 .style()
2197 .unwrap()
2198 .text
2199 .line_height_in_pixels(window.rem_size())
2200 });
2201 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2202
2203 cx.set_state(
2204 &r#"ˇone
2205 two
2206
2207 three
2208 fourˇ
2209 five
2210
2211 six"#
2212 .unindent(),
2213 );
2214
2215 cx.update_editor(|editor, window, cx| {
2216 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2217 });
2218 cx.assert_editor_state(
2219 &r#"one
2220 two
2221 ˇ
2222 three
2223 four
2224 five
2225 ˇ
2226 six"#
2227 .unindent(),
2228 );
2229
2230 cx.update_editor(|editor, window, cx| {
2231 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2232 });
2233 cx.assert_editor_state(
2234 &r#"one
2235 two
2236
2237 three
2238 four
2239 five
2240 ˇ
2241 sixˇ"#
2242 .unindent(),
2243 );
2244
2245 cx.update_editor(|editor, window, cx| {
2246 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2247 });
2248 cx.assert_editor_state(
2249 &r#"one
2250 two
2251
2252 three
2253 four
2254 five
2255
2256 sixˇ"#
2257 .unindent(),
2258 );
2259
2260 cx.update_editor(|editor, window, cx| {
2261 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2262 });
2263 cx.assert_editor_state(
2264 &r#"one
2265 two
2266
2267 three
2268 four
2269 five
2270 ˇ
2271 six"#
2272 .unindent(),
2273 );
2274
2275 cx.update_editor(|editor, window, cx| {
2276 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2277 });
2278 cx.assert_editor_state(
2279 &r#"one
2280 two
2281 ˇ
2282 three
2283 four
2284 five
2285
2286 six"#
2287 .unindent(),
2288 );
2289
2290 cx.update_editor(|editor, window, cx| {
2291 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2292 });
2293 cx.assert_editor_state(
2294 &r#"ˇone
2295 two
2296
2297 three
2298 four
2299 five
2300
2301 six"#
2302 .unindent(),
2303 );
2304}
2305
2306#[gpui::test]
2307async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2308 init_test(cx, |_| {});
2309 let mut cx = EditorTestContext::new(cx).await;
2310 let line_height = cx.editor(|editor, window, _| {
2311 editor
2312 .style()
2313 .unwrap()
2314 .text
2315 .line_height_in_pixels(window.rem_size())
2316 });
2317 let window = cx.window;
2318 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2319
2320 cx.set_state(
2321 r#"ˇone
2322 two
2323 three
2324 four
2325 five
2326 six
2327 seven
2328 eight
2329 nine
2330 ten
2331 "#,
2332 );
2333
2334 cx.update_editor(|editor, window, cx| {
2335 assert_eq!(
2336 editor.snapshot(window, cx).scroll_position(),
2337 gpui::Point::new(0., 0.)
2338 );
2339 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2340 assert_eq!(
2341 editor.snapshot(window, cx).scroll_position(),
2342 gpui::Point::new(0., 3.)
2343 );
2344 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2345 assert_eq!(
2346 editor.snapshot(window, cx).scroll_position(),
2347 gpui::Point::new(0., 6.)
2348 );
2349 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2350 assert_eq!(
2351 editor.snapshot(window, cx).scroll_position(),
2352 gpui::Point::new(0., 3.)
2353 );
2354
2355 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2356 assert_eq!(
2357 editor.snapshot(window, cx).scroll_position(),
2358 gpui::Point::new(0., 1.)
2359 );
2360 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2361 assert_eq!(
2362 editor.snapshot(window, cx).scroll_position(),
2363 gpui::Point::new(0., 3.)
2364 );
2365 });
2366}
2367
2368#[gpui::test]
2369async fn test_autoscroll(cx: &mut TestAppContext) {
2370 init_test(cx, |_| {});
2371 let mut cx = EditorTestContext::new(cx).await;
2372
2373 let line_height = cx.update_editor(|editor, window, cx| {
2374 editor.set_vertical_scroll_margin(2, cx);
2375 editor
2376 .style()
2377 .unwrap()
2378 .text
2379 .line_height_in_pixels(window.rem_size())
2380 });
2381 let window = cx.window;
2382 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2383
2384 cx.set_state(
2385 r#"ˇone
2386 two
2387 three
2388 four
2389 five
2390 six
2391 seven
2392 eight
2393 nine
2394 ten
2395 "#,
2396 );
2397 cx.update_editor(|editor, window, cx| {
2398 assert_eq!(
2399 editor.snapshot(window, cx).scroll_position(),
2400 gpui::Point::new(0., 0.0)
2401 );
2402 });
2403
2404 // Add a cursor below the visible area. Since both cursors cannot fit
2405 // on screen, the editor autoscrolls to reveal the newest cursor, and
2406 // allows the vertical scroll margin below that cursor.
2407 cx.update_editor(|editor, window, cx| {
2408 editor.change_selections(Default::default(), window, cx, |selections| {
2409 selections.select_ranges([
2410 Point::new(0, 0)..Point::new(0, 0),
2411 Point::new(6, 0)..Point::new(6, 0),
2412 ]);
2413 })
2414 });
2415 cx.update_editor(|editor, window, cx| {
2416 assert_eq!(
2417 editor.snapshot(window, cx).scroll_position(),
2418 gpui::Point::new(0., 3.0)
2419 );
2420 });
2421
2422 // Move down. The editor cursor scrolls down to track the newest cursor.
2423 cx.update_editor(|editor, window, cx| {
2424 editor.move_down(&Default::default(), window, cx);
2425 });
2426 cx.update_editor(|editor, window, cx| {
2427 assert_eq!(
2428 editor.snapshot(window, cx).scroll_position(),
2429 gpui::Point::new(0., 4.0)
2430 );
2431 });
2432
2433 // Add a cursor above the visible area. Since both cursors fit on screen,
2434 // the editor scrolls to show both.
2435 cx.update_editor(|editor, window, cx| {
2436 editor.change_selections(Default::default(), window, cx, |selections| {
2437 selections.select_ranges([
2438 Point::new(1, 0)..Point::new(1, 0),
2439 Point::new(6, 0)..Point::new(6, 0),
2440 ]);
2441 })
2442 });
2443 cx.update_editor(|editor, window, cx| {
2444 assert_eq!(
2445 editor.snapshot(window, cx).scroll_position(),
2446 gpui::Point::new(0., 1.0)
2447 );
2448 });
2449}
2450
2451#[gpui::test]
2452async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2453 init_test(cx, |_| {});
2454 let mut cx = EditorTestContext::new(cx).await;
2455
2456 let line_height = cx.editor(|editor, window, _cx| {
2457 editor
2458 .style()
2459 .unwrap()
2460 .text
2461 .line_height_in_pixels(window.rem_size())
2462 });
2463 let window = cx.window;
2464 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2465 cx.set_state(
2466 &r#"
2467 ˇone
2468 two
2469 threeˇ
2470 four
2471 five
2472 six
2473 seven
2474 eight
2475 nine
2476 ten
2477 "#
2478 .unindent(),
2479 );
2480
2481 cx.update_editor(|editor, window, cx| {
2482 editor.move_page_down(&MovePageDown::default(), window, cx)
2483 });
2484 cx.assert_editor_state(
2485 &r#"
2486 one
2487 two
2488 three
2489 ˇfour
2490 five
2491 sixˇ
2492 seven
2493 eight
2494 nine
2495 ten
2496 "#
2497 .unindent(),
2498 );
2499
2500 cx.update_editor(|editor, window, cx| {
2501 editor.move_page_down(&MovePageDown::default(), window, cx)
2502 });
2503 cx.assert_editor_state(
2504 &r#"
2505 one
2506 two
2507 three
2508 four
2509 five
2510 six
2511 ˇseven
2512 eight
2513 nineˇ
2514 ten
2515 "#
2516 .unindent(),
2517 );
2518
2519 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2520 cx.assert_editor_state(
2521 &r#"
2522 one
2523 two
2524 three
2525 ˇfour
2526 five
2527 sixˇ
2528 seven
2529 eight
2530 nine
2531 ten
2532 "#
2533 .unindent(),
2534 );
2535
2536 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2537 cx.assert_editor_state(
2538 &r#"
2539 ˇone
2540 two
2541 threeˇ
2542 four
2543 five
2544 six
2545 seven
2546 eight
2547 nine
2548 ten
2549 "#
2550 .unindent(),
2551 );
2552
2553 // Test select collapsing
2554 cx.update_editor(|editor, window, cx| {
2555 editor.move_page_down(&MovePageDown::default(), window, cx);
2556 editor.move_page_down(&MovePageDown::default(), window, cx);
2557 editor.move_page_down(&MovePageDown::default(), window, cx);
2558 });
2559 cx.assert_editor_state(
2560 &r#"
2561 one
2562 two
2563 three
2564 four
2565 five
2566 six
2567 seven
2568 eight
2569 nine
2570 ˇten
2571 ˇ"#
2572 .unindent(),
2573 );
2574}
2575
2576#[gpui::test]
2577async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2578 init_test(cx, |_| {});
2579 let mut cx = EditorTestContext::new(cx).await;
2580 cx.set_state("one «two threeˇ» four");
2581 cx.update_editor(|editor, window, cx| {
2582 editor.delete_to_beginning_of_line(
2583 &DeleteToBeginningOfLine {
2584 stop_at_indent: false,
2585 },
2586 window,
2587 cx,
2588 );
2589 assert_eq!(editor.text(cx), " four");
2590 });
2591}
2592
2593#[gpui::test]
2594async fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2595 init_test(cx, |_| {});
2596
2597 let mut cx = EditorTestContext::new(cx).await;
2598
2599 // For an empty selection, the preceding word fragment is deleted.
2600 // For non-empty selections, only selected characters are deleted.
2601 cx.set_state("onˇe two t«hreˇ»e four");
2602 cx.update_editor(|editor, window, cx| {
2603 editor.delete_to_previous_word_start(
2604 &DeleteToPreviousWordStart {
2605 ignore_newlines: false,
2606 ignore_brackets: false,
2607 },
2608 window,
2609 cx,
2610 );
2611 });
2612 cx.assert_editor_state("ˇe two tˇe four");
2613
2614 cx.set_state("e tˇwo te «fˇ»our");
2615 cx.update_editor(|editor, window, cx| {
2616 editor.delete_to_next_word_end(
2617 &DeleteToNextWordEnd {
2618 ignore_newlines: false,
2619 ignore_brackets: false,
2620 },
2621 window,
2622 cx,
2623 );
2624 });
2625 cx.assert_editor_state("e tˇ te ˇour");
2626}
2627
2628#[gpui::test]
2629async fn test_delete_whitespaces(cx: &mut TestAppContext) {
2630 init_test(cx, |_| {});
2631
2632 let mut cx = EditorTestContext::new(cx).await;
2633
2634 cx.set_state("here is some text ˇwith a space");
2635 cx.update_editor(|editor, window, cx| {
2636 editor.delete_to_previous_word_start(
2637 &DeleteToPreviousWordStart {
2638 ignore_newlines: false,
2639 ignore_brackets: true,
2640 },
2641 window,
2642 cx,
2643 );
2644 });
2645 // Continuous whitespace sequences are removed entirely, words behind them are not affected by the deletion action.
2646 cx.assert_editor_state("here is some textˇwith a space");
2647
2648 cx.set_state("here is some text ˇwith a space");
2649 cx.update_editor(|editor, window, cx| {
2650 editor.delete_to_previous_word_start(
2651 &DeleteToPreviousWordStart {
2652 ignore_newlines: false,
2653 ignore_brackets: false,
2654 },
2655 window,
2656 cx,
2657 );
2658 });
2659 cx.assert_editor_state("here is some textˇwith a space");
2660
2661 cx.set_state("here is some textˇ with a space");
2662 cx.update_editor(|editor, window, cx| {
2663 editor.delete_to_next_word_end(
2664 &DeleteToNextWordEnd {
2665 ignore_newlines: false,
2666 ignore_brackets: true,
2667 },
2668 window,
2669 cx,
2670 );
2671 });
2672 // Same happens in the other direction.
2673 cx.assert_editor_state("here is some textˇwith a space");
2674
2675 cx.set_state("here is some textˇ with a space");
2676 cx.update_editor(|editor, window, cx| {
2677 editor.delete_to_next_word_end(
2678 &DeleteToNextWordEnd {
2679 ignore_newlines: false,
2680 ignore_brackets: false,
2681 },
2682 window,
2683 cx,
2684 );
2685 });
2686 cx.assert_editor_state("here is some textˇwith a space");
2687
2688 cx.set_state("here is some textˇ with a space");
2689 cx.update_editor(|editor, window, cx| {
2690 editor.delete_to_next_word_end(
2691 &DeleteToNextWordEnd {
2692 ignore_newlines: true,
2693 ignore_brackets: false,
2694 },
2695 window,
2696 cx,
2697 );
2698 });
2699 cx.assert_editor_state("here is some textˇwith a space");
2700 cx.update_editor(|editor, window, cx| {
2701 editor.delete_to_previous_word_start(
2702 &DeleteToPreviousWordStart {
2703 ignore_newlines: true,
2704 ignore_brackets: false,
2705 },
2706 window,
2707 cx,
2708 );
2709 });
2710 cx.assert_editor_state("here is some ˇwith a space");
2711 cx.update_editor(|editor, window, cx| {
2712 editor.delete_to_previous_word_start(
2713 &DeleteToPreviousWordStart {
2714 ignore_newlines: true,
2715 ignore_brackets: false,
2716 },
2717 window,
2718 cx,
2719 );
2720 });
2721 // Single whitespaces are removed with the word behind them.
2722 cx.assert_editor_state("here is ˇwith a space");
2723 cx.update_editor(|editor, window, cx| {
2724 editor.delete_to_previous_word_start(
2725 &DeleteToPreviousWordStart {
2726 ignore_newlines: true,
2727 ignore_brackets: false,
2728 },
2729 window,
2730 cx,
2731 );
2732 });
2733 cx.assert_editor_state("here ˇwith a space");
2734 cx.update_editor(|editor, window, cx| {
2735 editor.delete_to_previous_word_start(
2736 &DeleteToPreviousWordStart {
2737 ignore_newlines: true,
2738 ignore_brackets: false,
2739 },
2740 window,
2741 cx,
2742 );
2743 });
2744 cx.assert_editor_state("ˇwith a space");
2745 cx.update_editor(|editor, window, cx| {
2746 editor.delete_to_previous_word_start(
2747 &DeleteToPreviousWordStart {
2748 ignore_newlines: true,
2749 ignore_brackets: false,
2750 },
2751 window,
2752 cx,
2753 );
2754 });
2755 cx.assert_editor_state("ˇwith a space");
2756 cx.update_editor(|editor, window, cx| {
2757 editor.delete_to_next_word_end(
2758 &DeleteToNextWordEnd {
2759 ignore_newlines: true,
2760 ignore_brackets: false,
2761 },
2762 window,
2763 cx,
2764 );
2765 });
2766 // Same happens in the other direction.
2767 cx.assert_editor_state("ˇ a space");
2768 cx.update_editor(|editor, window, cx| {
2769 editor.delete_to_next_word_end(
2770 &DeleteToNextWordEnd {
2771 ignore_newlines: true,
2772 ignore_brackets: false,
2773 },
2774 window,
2775 cx,
2776 );
2777 });
2778 cx.assert_editor_state("ˇ space");
2779 cx.update_editor(|editor, window, cx| {
2780 editor.delete_to_next_word_end(
2781 &DeleteToNextWordEnd {
2782 ignore_newlines: true,
2783 ignore_brackets: false,
2784 },
2785 window,
2786 cx,
2787 );
2788 });
2789 cx.assert_editor_state("ˇ");
2790 cx.update_editor(|editor, window, cx| {
2791 editor.delete_to_next_word_end(
2792 &DeleteToNextWordEnd {
2793 ignore_newlines: true,
2794 ignore_brackets: false,
2795 },
2796 window,
2797 cx,
2798 );
2799 });
2800 cx.assert_editor_state("ˇ");
2801 cx.update_editor(|editor, window, cx| {
2802 editor.delete_to_previous_word_start(
2803 &DeleteToPreviousWordStart {
2804 ignore_newlines: true,
2805 ignore_brackets: false,
2806 },
2807 window,
2808 cx,
2809 );
2810 });
2811 cx.assert_editor_state("ˇ");
2812}
2813
2814#[gpui::test]
2815async fn test_delete_to_bracket(cx: &mut TestAppContext) {
2816 init_test(cx, |_| {});
2817
2818 let language = Arc::new(
2819 Language::new(
2820 LanguageConfig {
2821 brackets: BracketPairConfig {
2822 pairs: vec![
2823 BracketPair {
2824 start: "\"".to_string(),
2825 end: "\"".to_string(),
2826 close: true,
2827 surround: true,
2828 newline: false,
2829 },
2830 BracketPair {
2831 start: "(".to_string(),
2832 end: ")".to_string(),
2833 close: true,
2834 surround: true,
2835 newline: true,
2836 },
2837 ],
2838 ..BracketPairConfig::default()
2839 },
2840 ..LanguageConfig::default()
2841 },
2842 Some(tree_sitter_rust::LANGUAGE.into()),
2843 )
2844 .with_brackets_query(
2845 r#"
2846 ("(" @open ")" @close)
2847 ("\"" @open "\"" @close)
2848 "#,
2849 )
2850 .unwrap(),
2851 );
2852
2853 let mut cx = EditorTestContext::new(cx).await;
2854 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2855
2856 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2857 cx.update_editor(|editor, window, cx| {
2858 editor.delete_to_previous_word_start(
2859 &DeleteToPreviousWordStart {
2860 ignore_newlines: true,
2861 ignore_brackets: false,
2862 },
2863 window,
2864 cx,
2865 );
2866 });
2867 // Deletion stops before brackets if asked to not ignore them.
2868 cx.assert_editor_state(r#"macro!("ˇCOMMENT");"#);
2869 cx.update_editor(|editor, window, cx| {
2870 editor.delete_to_previous_word_start(
2871 &DeleteToPreviousWordStart {
2872 ignore_newlines: true,
2873 ignore_brackets: false,
2874 },
2875 window,
2876 cx,
2877 );
2878 });
2879 // Deletion has to remove a single bracket and then stop again.
2880 cx.assert_editor_state(r#"macro!(ˇCOMMENT");"#);
2881
2882 cx.update_editor(|editor, window, cx| {
2883 editor.delete_to_previous_word_start(
2884 &DeleteToPreviousWordStart {
2885 ignore_newlines: true,
2886 ignore_brackets: false,
2887 },
2888 window,
2889 cx,
2890 );
2891 });
2892 cx.assert_editor_state(r#"macro!ˇCOMMENT");"#);
2893
2894 cx.update_editor(|editor, window, cx| {
2895 editor.delete_to_previous_word_start(
2896 &DeleteToPreviousWordStart {
2897 ignore_newlines: true,
2898 ignore_brackets: false,
2899 },
2900 window,
2901 cx,
2902 );
2903 });
2904 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2905
2906 cx.update_editor(|editor, window, cx| {
2907 editor.delete_to_previous_word_start(
2908 &DeleteToPreviousWordStart {
2909 ignore_newlines: true,
2910 ignore_brackets: false,
2911 },
2912 window,
2913 cx,
2914 );
2915 });
2916 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2917
2918 cx.update_editor(|editor, window, cx| {
2919 editor.delete_to_next_word_end(
2920 &DeleteToNextWordEnd {
2921 ignore_newlines: true,
2922 ignore_brackets: false,
2923 },
2924 window,
2925 cx,
2926 );
2927 });
2928 // Brackets on the right are not paired anymore, hence deletion does not stop at them
2929 cx.assert_editor_state(r#"ˇ");"#);
2930
2931 cx.update_editor(|editor, window, cx| {
2932 editor.delete_to_next_word_end(
2933 &DeleteToNextWordEnd {
2934 ignore_newlines: true,
2935 ignore_brackets: false,
2936 },
2937 window,
2938 cx,
2939 );
2940 });
2941 cx.assert_editor_state(r#"ˇ"#);
2942
2943 cx.update_editor(|editor, window, cx| {
2944 editor.delete_to_next_word_end(
2945 &DeleteToNextWordEnd {
2946 ignore_newlines: true,
2947 ignore_brackets: false,
2948 },
2949 window,
2950 cx,
2951 );
2952 });
2953 cx.assert_editor_state(r#"ˇ"#);
2954
2955 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2956 cx.update_editor(|editor, window, cx| {
2957 editor.delete_to_previous_word_start(
2958 &DeleteToPreviousWordStart {
2959 ignore_newlines: true,
2960 ignore_brackets: true,
2961 },
2962 window,
2963 cx,
2964 );
2965 });
2966 cx.assert_editor_state(r#"macroˇCOMMENT");"#);
2967}
2968
2969#[gpui::test]
2970fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2971 init_test(cx, |_| {});
2972
2973 let editor = cx.add_window(|window, cx| {
2974 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2975 build_editor(buffer, window, cx)
2976 });
2977 let del_to_prev_word_start = DeleteToPreviousWordStart {
2978 ignore_newlines: false,
2979 ignore_brackets: false,
2980 };
2981 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2982 ignore_newlines: true,
2983 ignore_brackets: false,
2984 };
2985
2986 _ = editor.update(cx, |editor, window, cx| {
2987 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2988 s.select_display_ranges([
2989 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2990 ])
2991 });
2992 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2993 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2994 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2995 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2996 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2997 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2998 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2999 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
3000 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
3001 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
3002 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
3003 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3004 });
3005}
3006
3007#[gpui::test]
3008fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
3009 init_test(cx, |_| {});
3010
3011 let editor = cx.add_window(|window, cx| {
3012 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
3013 build_editor(buffer, window, cx)
3014 });
3015 let del_to_next_word_end = DeleteToNextWordEnd {
3016 ignore_newlines: false,
3017 ignore_brackets: false,
3018 };
3019 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
3020 ignore_newlines: true,
3021 ignore_brackets: false,
3022 };
3023
3024 _ = editor.update(cx, |editor, window, cx| {
3025 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3026 s.select_display_ranges([
3027 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
3028 ])
3029 });
3030 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3031 assert_eq!(
3032 editor.buffer.read(cx).read(cx).text(),
3033 "one\n two\nthree\n four"
3034 );
3035 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3036 assert_eq!(
3037 editor.buffer.read(cx).read(cx).text(),
3038 "\n two\nthree\n four"
3039 );
3040 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3041 assert_eq!(
3042 editor.buffer.read(cx).read(cx).text(),
3043 "two\nthree\n four"
3044 );
3045 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3046 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
3047 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3048 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
3049 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3050 assert_eq!(editor.buffer.read(cx).read(cx).text(), "four");
3051 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3052 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3053 });
3054}
3055
3056#[gpui::test]
3057fn test_newline(cx: &mut TestAppContext) {
3058 init_test(cx, |_| {});
3059
3060 let editor = cx.add_window(|window, cx| {
3061 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
3062 build_editor(buffer, window, cx)
3063 });
3064
3065 _ = editor.update(cx, |editor, window, cx| {
3066 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3067 s.select_display_ranges([
3068 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
3069 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
3070 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
3071 ])
3072 });
3073
3074 editor.newline(&Newline, window, cx);
3075 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
3076 });
3077}
3078
3079#[gpui::test]
3080async fn test_newline_yaml(cx: &mut TestAppContext) {
3081 init_test(cx, |_| {});
3082
3083 let mut cx = EditorTestContext::new(cx).await;
3084 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3085 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3086
3087 // Object (between 2 fields)
3088 cx.set_state(indoc! {"
3089 test:ˇ
3090 hello: bye"});
3091 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3092 cx.assert_editor_state(indoc! {"
3093 test:
3094 ˇ
3095 hello: bye"});
3096
3097 // Object (first and single line)
3098 cx.set_state(indoc! {"
3099 test:ˇ"});
3100 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3101 cx.assert_editor_state(indoc! {"
3102 test:
3103 ˇ"});
3104
3105 // Array with objects (after first element)
3106 cx.set_state(indoc! {"
3107 test:
3108 - foo: barˇ"});
3109 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3110 cx.assert_editor_state(indoc! {"
3111 test:
3112 - foo: bar
3113 ˇ"});
3114
3115 // Array with objects and comment
3116 cx.set_state(indoc! {"
3117 test:
3118 - foo: bar
3119 - bar: # testˇ"});
3120 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3121 cx.assert_editor_state(indoc! {"
3122 test:
3123 - foo: bar
3124 - bar: # test
3125 ˇ"});
3126
3127 // Array with objects (after second element)
3128 cx.set_state(indoc! {"
3129 test:
3130 - foo: bar
3131 - bar: fooˇ"});
3132 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3133 cx.assert_editor_state(indoc! {"
3134 test:
3135 - foo: bar
3136 - bar: foo
3137 ˇ"});
3138
3139 // Array with strings (after first element)
3140 cx.set_state(indoc! {"
3141 test:
3142 - fooˇ"});
3143 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3144 cx.assert_editor_state(indoc! {"
3145 test:
3146 - foo
3147 ˇ"});
3148}
3149
3150#[gpui::test]
3151fn test_newline_with_old_selections(cx: &mut TestAppContext) {
3152 init_test(cx, |_| {});
3153
3154 let editor = cx.add_window(|window, cx| {
3155 let buffer = MultiBuffer::build_simple(
3156 "
3157 a
3158 b(
3159 X
3160 )
3161 c(
3162 X
3163 )
3164 "
3165 .unindent()
3166 .as_str(),
3167 cx,
3168 );
3169 let mut editor = build_editor(buffer, window, cx);
3170 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3171 s.select_ranges([
3172 Point::new(2, 4)..Point::new(2, 5),
3173 Point::new(5, 4)..Point::new(5, 5),
3174 ])
3175 });
3176 editor
3177 });
3178
3179 _ = editor.update(cx, |editor, window, cx| {
3180 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3181 editor.buffer.update(cx, |buffer, cx| {
3182 buffer.edit(
3183 [
3184 (Point::new(1, 2)..Point::new(3, 0), ""),
3185 (Point::new(4, 2)..Point::new(6, 0), ""),
3186 ],
3187 None,
3188 cx,
3189 );
3190 assert_eq!(
3191 buffer.read(cx).text(),
3192 "
3193 a
3194 b()
3195 c()
3196 "
3197 .unindent()
3198 );
3199 });
3200 assert_eq!(
3201 editor.selections.ranges(&editor.display_snapshot(cx)),
3202 &[
3203 Point::new(1, 2)..Point::new(1, 2),
3204 Point::new(2, 2)..Point::new(2, 2),
3205 ],
3206 );
3207
3208 editor.newline(&Newline, window, cx);
3209 assert_eq!(
3210 editor.text(cx),
3211 "
3212 a
3213 b(
3214 )
3215 c(
3216 )
3217 "
3218 .unindent()
3219 );
3220
3221 // The selections are moved after the inserted newlines
3222 assert_eq!(
3223 editor.selections.ranges(&editor.display_snapshot(cx)),
3224 &[
3225 Point::new(2, 0)..Point::new(2, 0),
3226 Point::new(4, 0)..Point::new(4, 0),
3227 ],
3228 );
3229 });
3230}
3231
3232#[gpui::test]
3233async fn test_newline_above(cx: &mut TestAppContext) {
3234 init_test(cx, |settings| {
3235 settings.defaults.tab_size = NonZeroU32::new(4)
3236 });
3237
3238 let language = Arc::new(
3239 Language::new(
3240 LanguageConfig::default(),
3241 Some(tree_sitter_rust::LANGUAGE.into()),
3242 )
3243 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3244 .unwrap(),
3245 );
3246
3247 let mut cx = EditorTestContext::new(cx).await;
3248 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3249 cx.set_state(indoc! {"
3250 const a: ˇA = (
3251 (ˇ
3252 «const_functionˇ»(ˇ),
3253 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3254 )ˇ
3255 ˇ);ˇ
3256 "});
3257
3258 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
3259 cx.assert_editor_state(indoc! {"
3260 ˇ
3261 const a: A = (
3262 ˇ
3263 (
3264 ˇ
3265 ˇ
3266 const_function(),
3267 ˇ
3268 ˇ
3269 ˇ
3270 ˇ
3271 something_else,
3272 ˇ
3273 )
3274 ˇ
3275 ˇ
3276 );
3277 "});
3278}
3279
3280#[gpui::test]
3281async fn test_newline_below(cx: &mut TestAppContext) {
3282 init_test(cx, |settings| {
3283 settings.defaults.tab_size = NonZeroU32::new(4)
3284 });
3285
3286 let language = Arc::new(
3287 Language::new(
3288 LanguageConfig::default(),
3289 Some(tree_sitter_rust::LANGUAGE.into()),
3290 )
3291 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3292 .unwrap(),
3293 );
3294
3295 let mut cx = EditorTestContext::new(cx).await;
3296 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3297 cx.set_state(indoc! {"
3298 const a: ˇA = (
3299 (ˇ
3300 «const_functionˇ»(ˇ),
3301 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3302 )ˇ
3303 ˇ);ˇ
3304 "});
3305
3306 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
3307 cx.assert_editor_state(indoc! {"
3308 const a: A = (
3309 ˇ
3310 (
3311 ˇ
3312 const_function(),
3313 ˇ
3314 ˇ
3315 something_else,
3316 ˇ
3317 ˇ
3318 ˇ
3319 ˇ
3320 )
3321 ˇ
3322 );
3323 ˇ
3324 ˇ
3325 "});
3326}
3327
3328#[gpui::test]
3329async fn test_newline_comments(cx: &mut TestAppContext) {
3330 init_test(cx, |settings| {
3331 settings.defaults.tab_size = NonZeroU32::new(4)
3332 });
3333
3334 let language = Arc::new(Language::new(
3335 LanguageConfig {
3336 line_comments: vec!["// ".into()],
3337 ..LanguageConfig::default()
3338 },
3339 None,
3340 ));
3341 {
3342 let mut cx = EditorTestContext::new(cx).await;
3343 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3344 cx.set_state(indoc! {"
3345 // Fooˇ
3346 "});
3347
3348 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3349 cx.assert_editor_state(indoc! {"
3350 // Foo
3351 // ˇ
3352 "});
3353 // Ensure that we add comment prefix when existing line contains space
3354 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3355 cx.assert_editor_state(
3356 indoc! {"
3357 // Foo
3358 //s
3359 // ˇ
3360 "}
3361 .replace("s", " ") // s is used as space placeholder to prevent format on save
3362 .as_str(),
3363 );
3364 // Ensure that we add comment prefix when existing line does not contain space
3365 cx.set_state(indoc! {"
3366 // Foo
3367 //ˇ
3368 "});
3369 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3370 cx.assert_editor_state(indoc! {"
3371 // Foo
3372 //
3373 // ˇ
3374 "});
3375 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
3376 cx.set_state(indoc! {"
3377 ˇ// Foo
3378 "});
3379 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3380 cx.assert_editor_state(indoc! {"
3381
3382 ˇ// Foo
3383 "});
3384 }
3385 // Ensure that comment continuations can be disabled.
3386 update_test_language_settings(cx, |settings| {
3387 settings.defaults.extend_comment_on_newline = Some(false);
3388 });
3389 let mut cx = EditorTestContext::new(cx).await;
3390 cx.set_state(indoc! {"
3391 // Fooˇ
3392 "});
3393 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3394 cx.assert_editor_state(indoc! {"
3395 // Foo
3396 ˇ
3397 "});
3398}
3399
3400#[gpui::test]
3401async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
3402 init_test(cx, |settings| {
3403 settings.defaults.tab_size = NonZeroU32::new(4)
3404 });
3405
3406 let language = Arc::new(Language::new(
3407 LanguageConfig {
3408 line_comments: vec!["// ".into(), "/// ".into()],
3409 ..LanguageConfig::default()
3410 },
3411 None,
3412 ));
3413 {
3414 let mut cx = EditorTestContext::new(cx).await;
3415 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3416 cx.set_state(indoc! {"
3417 //ˇ
3418 "});
3419 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3420 cx.assert_editor_state(indoc! {"
3421 //
3422 // ˇ
3423 "});
3424
3425 cx.set_state(indoc! {"
3426 ///ˇ
3427 "});
3428 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3429 cx.assert_editor_state(indoc! {"
3430 ///
3431 /// ˇ
3432 "});
3433 }
3434}
3435
3436#[gpui::test]
3437async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
3438 init_test(cx, |settings| {
3439 settings.defaults.tab_size = NonZeroU32::new(4)
3440 });
3441
3442 let language = Arc::new(
3443 Language::new(
3444 LanguageConfig {
3445 documentation_comment: Some(language::BlockCommentConfig {
3446 start: "/**".into(),
3447 end: "*/".into(),
3448 prefix: "* ".into(),
3449 tab_size: 1,
3450 }),
3451
3452 ..LanguageConfig::default()
3453 },
3454 Some(tree_sitter_rust::LANGUAGE.into()),
3455 )
3456 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
3457 .unwrap(),
3458 );
3459
3460 {
3461 let mut cx = EditorTestContext::new(cx).await;
3462 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3463 cx.set_state(indoc! {"
3464 /**ˇ
3465 "});
3466
3467 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3468 cx.assert_editor_state(indoc! {"
3469 /**
3470 * ˇ
3471 "});
3472 // Ensure that if cursor is before the comment start,
3473 // we do not actually insert a comment prefix.
3474 cx.set_state(indoc! {"
3475 ˇ/**
3476 "});
3477 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3478 cx.assert_editor_state(indoc! {"
3479
3480 ˇ/**
3481 "});
3482 // Ensure that if cursor is between it doesn't add comment prefix.
3483 cx.set_state(indoc! {"
3484 /*ˇ*
3485 "});
3486 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3487 cx.assert_editor_state(indoc! {"
3488 /*
3489 ˇ*
3490 "});
3491 // Ensure that if suffix exists on same line after cursor it adds new line.
3492 cx.set_state(indoc! {"
3493 /**ˇ*/
3494 "});
3495 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3496 cx.assert_editor_state(indoc! {"
3497 /**
3498 * ˇ
3499 */
3500 "});
3501 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3502 cx.set_state(indoc! {"
3503 /**ˇ */
3504 "});
3505 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3506 cx.assert_editor_state(indoc! {"
3507 /**
3508 * ˇ
3509 */
3510 "});
3511 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3512 cx.set_state(indoc! {"
3513 /** ˇ*/
3514 "});
3515 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3516 cx.assert_editor_state(
3517 indoc! {"
3518 /**s
3519 * ˇ
3520 */
3521 "}
3522 .replace("s", " ") // s is used as space placeholder to prevent format on save
3523 .as_str(),
3524 );
3525 // Ensure that delimiter space is preserved when newline on already
3526 // spaced delimiter.
3527 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3528 cx.assert_editor_state(
3529 indoc! {"
3530 /**s
3531 *s
3532 * ˇ
3533 */
3534 "}
3535 .replace("s", " ") // s is used as space placeholder to prevent format on save
3536 .as_str(),
3537 );
3538 // Ensure that delimiter space is preserved when space is not
3539 // on existing delimiter.
3540 cx.set_state(indoc! {"
3541 /**
3542 *ˇ
3543 */
3544 "});
3545 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3546 cx.assert_editor_state(indoc! {"
3547 /**
3548 *
3549 * ˇ
3550 */
3551 "});
3552 // Ensure that if suffix exists on same line after cursor it
3553 // doesn't add extra new line if prefix is not on same line.
3554 cx.set_state(indoc! {"
3555 /**
3556 ˇ*/
3557 "});
3558 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3559 cx.assert_editor_state(indoc! {"
3560 /**
3561
3562 ˇ*/
3563 "});
3564 // Ensure that it detects suffix after existing prefix.
3565 cx.set_state(indoc! {"
3566 /**ˇ/
3567 "});
3568 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3569 cx.assert_editor_state(indoc! {"
3570 /**
3571 ˇ/
3572 "});
3573 // Ensure that if suffix exists on same line before
3574 // cursor it does not add comment prefix.
3575 cx.set_state(indoc! {"
3576 /** */ˇ
3577 "});
3578 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3579 cx.assert_editor_state(indoc! {"
3580 /** */
3581 ˇ
3582 "});
3583 // Ensure that if suffix exists on same line before
3584 // cursor it does not add comment prefix.
3585 cx.set_state(indoc! {"
3586 /**
3587 *
3588 */ˇ
3589 "});
3590 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3591 cx.assert_editor_state(indoc! {"
3592 /**
3593 *
3594 */
3595 ˇ
3596 "});
3597
3598 // Ensure that inline comment followed by code
3599 // doesn't add comment prefix on newline
3600 cx.set_state(indoc! {"
3601 /** */ textˇ
3602 "});
3603 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3604 cx.assert_editor_state(indoc! {"
3605 /** */ text
3606 ˇ
3607 "});
3608
3609 // Ensure that text after comment end tag
3610 // doesn't add comment prefix on newline
3611 cx.set_state(indoc! {"
3612 /**
3613 *
3614 */ˇtext
3615 "});
3616 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3617 cx.assert_editor_state(indoc! {"
3618 /**
3619 *
3620 */
3621 ˇtext
3622 "});
3623
3624 // Ensure if not comment block it doesn't
3625 // add comment prefix on newline
3626 cx.set_state(indoc! {"
3627 * textˇ
3628 "});
3629 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3630 cx.assert_editor_state(indoc! {"
3631 * text
3632 ˇ
3633 "});
3634 }
3635 // Ensure that comment continuations can be disabled.
3636 update_test_language_settings(cx, |settings| {
3637 settings.defaults.extend_comment_on_newline = Some(false);
3638 });
3639 let mut cx = EditorTestContext::new(cx).await;
3640 cx.set_state(indoc! {"
3641 /**ˇ
3642 "});
3643 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3644 cx.assert_editor_state(indoc! {"
3645 /**
3646 ˇ
3647 "});
3648}
3649
3650#[gpui::test]
3651async fn test_newline_comments_with_block_comment(cx: &mut TestAppContext) {
3652 init_test(cx, |settings| {
3653 settings.defaults.tab_size = NonZeroU32::new(4)
3654 });
3655
3656 let lua_language = Arc::new(Language::new(
3657 LanguageConfig {
3658 line_comments: vec!["--".into()],
3659 block_comment: Some(language::BlockCommentConfig {
3660 start: "--[[".into(),
3661 prefix: "".into(),
3662 end: "]]".into(),
3663 tab_size: 0,
3664 }),
3665 ..LanguageConfig::default()
3666 },
3667 None,
3668 ));
3669
3670 let mut cx = EditorTestContext::new(cx).await;
3671 cx.update_buffer(|buffer, cx| buffer.set_language(Some(lua_language), cx));
3672
3673 // Line with line comment should extend
3674 cx.set_state(indoc! {"
3675 --ˇ
3676 "});
3677 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3678 cx.assert_editor_state(indoc! {"
3679 --
3680 --ˇ
3681 "});
3682
3683 // Line with block comment that matches line comment should not extend
3684 cx.set_state(indoc! {"
3685 --[[ˇ
3686 "});
3687 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3688 cx.assert_editor_state(indoc! {"
3689 --[[
3690 ˇ
3691 "});
3692}
3693
3694#[gpui::test]
3695fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3696 init_test(cx, |_| {});
3697
3698 let editor = cx.add_window(|window, cx| {
3699 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3700 let mut editor = build_editor(buffer, window, cx);
3701 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3702 s.select_ranges([3..4, 11..12, 19..20])
3703 });
3704 editor
3705 });
3706
3707 _ = editor.update(cx, |editor, window, cx| {
3708 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3709 editor.buffer.update(cx, |buffer, cx| {
3710 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3711 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3712 });
3713 assert_eq!(
3714 editor.selections.ranges(&editor.display_snapshot(cx)),
3715 &[2..2, 7..7, 12..12],
3716 );
3717
3718 editor.insert("Z", window, cx);
3719 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3720
3721 // The selections are moved after the inserted characters
3722 assert_eq!(
3723 editor.selections.ranges(&editor.display_snapshot(cx)),
3724 &[3..3, 9..9, 15..15],
3725 );
3726 });
3727}
3728
3729#[gpui::test]
3730async fn test_tab(cx: &mut TestAppContext) {
3731 init_test(cx, |settings| {
3732 settings.defaults.tab_size = NonZeroU32::new(3)
3733 });
3734
3735 let mut cx = EditorTestContext::new(cx).await;
3736 cx.set_state(indoc! {"
3737 ˇabˇc
3738 ˇ🏀ˇ🏀ˇefg
3739 dˇ
3740 "});
3741 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3742 cx.assert_editor_state(indoc! {"
3743 ˇab ˇc
3744 ˇ🏀 ˇ🏀 ˇefg
3745 d ˇ
3746 "});
3747
3748 cx.set_state(indoc! {"
3749 a
3750 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3751 "});
3752 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3753 cx.assert_editor_state(indoc! {"
3754 a
3755 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3756 "});
3757}
3758
3759#[gpui::test]
3760async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3761 init_test(cx, |_| {});
3762
3763 let mut cx = EditorTestContext::new(cx).await;
3764 let language = Arc::new(
3765 Language::new(
3766 LanguageConfig::default(),
3767 Some(tree_sitter_rust::LANGUAGE.into()),
3768 )
3769 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3770 .unwrap(),
3771 );
3772 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3773
3774 // test when all cursors are not at suggested indent
3775 // then simply move to their suggested indent location
3776 cx.set_state(indoc! {"
3777 const a: B = (
3778 c(
3779 ˇ
3780 ˇ )
3781 );
3782 "});
3783 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3784 cx.assert_editor_state(indoc! {"
3785 const a: B = (
3786 c(
3787 ˇ
3788 ˇ)
3789 );
3790 "});
3791
3792 // test cursor already at suggested indent not moving when
3793 // other cursors are yet to reach their suggested indents
3794 cx.set_state(indoc! {"
3795 ˇ
3796 const a: B = (
3797 c(
3798 d(
3799 ˇ
3800 )
3801 ˇ
3802 ˇ )
3803 );
3804 "});
3805 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3806 cx.assert_editor_state(indoc! {"
3807 ˇ
3808 const a: B = (
3809 c(
3810 d(
3811 ˇ
3812 )
3813 ˇ
3814 ˇ)
3815 );
3816 "});
3817 // test when all cursors are at suggested indent then tab is inserted
3818 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3819 cx.assert_editor_state(indoc! {"
3820 ˇ
3821 const a: B = (
3822 c(
3823 d(
3824 ˇ
3825 )
3826 ˇ
3827 ˇ)
3828 );
3829 "});
3830
3831 // test when current indent is less than suggested indent,
3832 // we adjust line to match suggested indent and move cursor to it
3833 //
3834 // when no other cursor is at word boundary, all of them should move
3835 cx.set_state(indoc! {"
3836 const a: B = (
3837 c(
3838 d(
3839 ˇ
3840 ˇ )
3841 ˇ )
3842 );
3843 "});
3844 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3845 cx.assert_editor_state(indoc! {"
3846 const a: B = (
3847 c(
3848 d(
3849 ˇ
3850 ˇ)
3851 ˇ)
3852 );
3853 "});
3854
3855 // test when current indent is less than suggested indent,
3856 // we adjust line to match suggested indent and move cursor to it
3857 //
3858 // when some other cursor is at word boundary, it should not move
3859 cx.set_state(indoc! {"
3860 const a: B = (
3861 c(
3862 d(
3863 ˇ
3864 ˇ )
3865 ˇ)
3866 );
3867 "});
3868 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3869 cx.assert_editor_state(indoc! {"
3870 const a: B = (
3871 c(
3872 d(
3873 ˇ
3874 ˇ)
3875 ˇ)
3876 );
3877 "});
3878
3879 // test when current indent is more than suggested indent,
3880 // we just move cursor to current indent instead of suggested indent
3881 //
3882 // when no other cursor is at word boundary, all of them should move
3883 cx.set_state(indoc! {"
3884 const a: B = (
3885 c(
3886 d(
3887 ˇ
3888 ˇ )
3889 ˇ )
3890 );
3891 "});
3892 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3893 cx.assert_editor_state(indoc! {"
3894 const a: B = (
3895 c(
3896 d(
3897 ˇ
3898 ˇ)
3899 ˇ)
3900 );
3901 "});
3902 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3903 cx.assert_editor_state(indoc! {"
3904 const a: B = (
3905 c(
3906 d(
3907 ˇ
3908 ˇ)
3909 ˇ)
3910 );
3911 "});
3912
3913 // test when current indent is more than suggested indent,
3914 // we just move cursor to current indent instead of suggested indent
3915 //
3916 // when some other cursor is at word boundary, it doesn't move
3917 cx.set_state(indoc! {"
3918 const a: B = (
3919 c(
3920 d(
3921 ˇ
3922 ˇ )
3923 ˇ)
3924 );
3925 "});
3926 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3927 cx.assert_editor_state(indoc! {"
3928 const a: B = (
3929 c(
3930 d(
3931 ˇ
3932 ˇ)
3933 ˇ)
3934 );
3935 "});
3936
3937 // handle auto-indent when there are multiple cursors on the same line
3938 cx.set_state(indoc! {"
3939 const a: B = (
3940 c(
3941 ˇ ˇ
3942 ˇ )
3943 );
3944 "});
3945 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3946 cx.assert_editor_state(indoc! {"
3947 const a: B = (
3948 c(
3949 ˇ
3950 ˇ)
3951 );
3952 "});
3953}
3954
3955#[gpui::test]
3956async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3957 init_test(cx, |settings| {
3958 settings.defaults.tab_size = NonZeroU32::new(3)
3959 });
3960
3961 let mut cx = EditorTestContext::new(cx).await;
3962 cx.set_state(indoc! {"
3963 ˇ
3964 \t ˇ
3965 \t ˇ
3966 \t ˇ
3967 \t \t\t \t \t\t \t\t \t \t ˇ
3968 "});
3969
3970 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3971 cx.assert_editor_state(indoc! {"
3972 ˇ
3973 \t ˇ
3974 \t ˇ
3975 \t ˇ
3976 \t \t\t \t \t\t \t\t \t \t ˇ
3977 "});
3978}
3979
3980#[gpui::test]
3981async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3982 init_test(cx, |settings| {
3983 settings.defaults.tab_size = NonZeroU32::new(4)
3984 });
3985
3986 let language = Arc::new(
3987 Language::new(
3988 LanguageConfig::default(),
3989 Some(tree_sitter_rust::LANGUAGE.into()),
3990 )
3991 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3992 .unwrap(),
3993 );
3994
3995 let mut cx = EditorTestContext::new(cx).await;
3996 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3997 cx.set_state(indoc! {"
3998 fn a() {
3999 if b {
4000 \t ˇc
4001 }
4002 }
4003 "});
4004
4005 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4006 cx.assert_editor_state(indoc! {"
4007 fn a() {
4008 if b {
4009 ˇc
4010 }
4011 }
4012 "});
4013}
4014
4015#[gpui::test]
4016async fn test_indent_outdent(cx: &mut TestAppContext) {
4017 init_test(cx, |settings| {
4018 settings.defaults.tab_size = NonZeroU32::new(4);
4019 });
4020
4021 let mut cx = EditorTestContext::new(cx).await;
4022
4023 cx.set_state(indoc! {"
4024 «oneˇ» «twoˇ»
4025 three
4026 four
4027 "});
4028 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4029 cx.assert_editor_state(indoc! {"
4030 «oneˇ» «twoˇ»
4031 three
4032 four
4033 "});
4034
4035 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4036 cx.assert_editor_state(indoc! {"
4037 «oneˇ» «twoˇ»
4038 three
4039 four
4040 "});
4041
4042 // select across line ending
4043 cx.set_state(indoc! {"
4044 one two
4045 t«hree
4046 ˇ» four
4047 "});
4048 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4049 cx.assert_editor_state(indoc! {"
4050 one two
4051 t«hree
4052 ˇ» four
4053 "});
4054
4055 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4056 cx.assert_editor_state(indoc! {"
4057 one two
4058 t«hree
4059 ˇ» four
4060 "});
4061
4062 // Ensure that indenting/outdenting works when the cursor is at column 0.
4063 cx.set_state(indoc! {"
4064 one two
4065 ˇthree
4066 four
4067 "});
4068 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4069 cx.assert_editor_state(indoc! {"
4070 one two
4071 ˇthree
4072 four
4073 "});
4074
4075 cx.set_state(indoc! {"
4076 one two
4077 ˇ three
4078 four
4079 "});
4080 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4081 cx.assert_editor_state(indoc! {"
4082 one two
4083 ˇthree
4084 four
4085 "});
4086}
4087
4088#[gpui::test]
4089async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) {
4090 // This is a regression test for issue #33761
4091 init_test(cx, |_| {});
4092
4093 let mut cx = EditorTestContext::new(cx).await;
4094 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
4095 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
4096
4097 cx.set_state(
4098 r#"ˇ# ingress:
4099ˇ# api:
4100ˇ# enabled: false
4101ˇ# pathType: Prefix
4102ˇ# console:
4103ˇ# enabled: false
4104ˇ# pathType: Prefix
4105"#,
4106 );
4107
4108 // Press tab to indent all lines
4109 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4110
4111 cx.assert_editor_state(
4112 r#" ˇ# ingress:
4113 ˇ# api:
4114 ˇ# enabled: false
4115 ˇ# pathType: Prefix
4116 ˇ# console:
4117 ˇ# enabled: false
4118 ˇ# pathType: Prefix
4119"#,
4120 );
4121}
4122
4123#[gpui::test]
4124async fn test_indent_yaml_non_comments_with_multiple_cursors(cx: &mut TestAppContext) {
4125 // This is a test to make sure our fix for issue #33761 didn't break anything
4126 init_test(cx, |_| {});
4127
4128 let mut cx = EditorTestContext::new(cx).await;
4129 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
4130 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
4131
4132 cx.set_state(
4133 r#"ˇingress:
4134ˇ api:
4135ˇ enabled: false
4136ˇ pathType: Prefix
4137"#,
4138 );
4139
4140 // Press tab to indent all lines
4141 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4142
4143 cx.assert_editor_state(
4144 r#"ˇingress:
4145 ˇapi:
4146 ˇenabled: false
4147 ˇpathType: Prefix
4148"#,
4149 );
4150}
4151
4152#[gpui::test]
4153async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
4154 init_test(cx, |settings| {
4155 settings.defaults.hard_tabs = Some(true);
4156 });
4157
4158 let mut cx = EditorTestContext::new(cx).await;
4159
4160 // select two ranges on one line
4161 cx.set_state(indoc! {"
4162 «oneˇ» «twoˇ»
4163 three
4164 four
4165 "});
4166 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4167 cx.assert_editor_state(indoc! {"
4168 \t«oneˇ» «twoˇ»
4169 three
4170 four
4171 "});
4172 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4173 cx.assert_editor_state(indoc! {"
4174 \t\t«oneˇ» «twoˇ»
4175 three
4176 four
4177 "});
4178 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4179 cx.assert_editor_state(indoc! {"
4180 \t«oneˇ» «twoˇ»
4181 three
4182 four
4183 "});
4184 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4185 cx.assert_editor_state(indoc! {"
4186 «oneˇ» «twoˇ»
4187 three
4188 four
4189 "});
4190
4191 // select across a line ending
4192 cx.set_state(indoc! {"
4193 one two
4194 t«hree
4195 ˇ»four
4196 "});
4197 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4198 cx.assert_editor_state(indoc! {"
4199 one two
4200 \tt«hree
4201 ˇ»four
4202 "});
4203 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4204 cx.assert_editor_state(indoc! {"
4205 one two
4206 \t\tt«hree
4207 ˇ»four
4208 "});
4209 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4210 cx.assert_editor_state(indoc! {"
4211 one two
4212 \tt«hree
4213 ˇ»four
4214 "});
4215 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4216 cx.assert_editor_state(indoc! {"
4217 one two
4218 t«hree
4219 ˇ»four
4220 "});
4221
4222 // Ensure that indenting/outdenting works when the cursor is at column 0.
4223 cx.set_state(indoc! {"
4224 one two
4225 ˇthree
4226 four
4227 "});
4228 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4229 cx.assert_editor_state(indoc! {"
4230 one two
4231 ˇthree
4232 four
4233 "});
4234 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4235 cx.assert_editor_state(indoc! {"
4236 one two
4237 \tˇthree
4238 four
4239 "});
4240 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4241 cx.assert_editor_state(indoc! {"
4242 one two
4243 ˇthree
4244 four
4245 "});
4246}
4247
4248#[gpui::test]
4249fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
4250 init_test(cx, |settings| {
4251 settings.languages.0.extend([
4252 (
4253 "TOML".into(),
4254 LanguageSettingsContent {
4255 tab_size: NonZeroU32::new(2),
4256 ..Default::default()
4257 },
4258 ),
4259 (
4260 "Rust".into(),
4261 LanguageSettingsContent {
4262 tab_size: NonZeroU32::new(4),
4263 ..Default::default()
4264 },
4265 ),
4266 ]);
4267 });
4268
4269 let toml_language = Arc::new(Language::new(
4270 LanguageConfig {
4271 name: "TOML".into(),
4272 ..Default::default()
4273 },
4274 None,
4275 ));
4276 let rust_language = Arc::new(Language::new(
4277 LanguageConfig {
4278 name: "Rust".into(),
4279 ..Default::default()
4280 },
4281 None,
4282 ));
4283
4284 let toml_buffer =
4285 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
4286 let rust_buffer =
4287 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
4288 let multibuffer = cx.new(|cx| {
4289 let mut multibuffer = MultiBuffer::new(ReadWrite);
4290 multibuffer.push_excerpts(
4291 toml_buffer.clone(),
4292 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
4293 cx,
4294 );
4295 multibuffer.push_excerpts(
4296 rust_buffer.clone(),
4297 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
4298 cx,
4299 );
4300 multibuffer
4301 });
4302
4303 cx.add_window(|window, cx| {
4304 let mut editor = build_editor(multibuffer, window, cx);
4305
4306 assert_eq!(
4307 editor.text(cx),
4308 indoc! {"
4309 a = 1
4310 b = 2
4311
4312 const c: usize = 3;
4313 "}
4314 );
4315
4316 select_ranges(
4317 &mut editor,
4318 indoc! {"
4319 «aˇ» = 1
4320 b = 2
4321
4322 «const c:ˇ» usize = 3;
4323 "},
4324 window,
4325 cx,
4326 );
4327
4328 editor.tab(&Tab, window, cx);
4329 assert_text_with_selections(
4330 &mut editor,
4331 indoc! {"
4332 «aˇ» = 1
4333 b = 2
4334
4335 «const c:ˇ» usize = 3;
4336 "},
4337 cx,
4338 );
4339 editor.backtab(&Backtab, window, cx);
4340 assert_text_with_selections(
4341 &mut editor,
4342 indoc! {"
4343 «aˇ» = 1
4344 b = 2
4345
4346 «const c:ˇ» usize = 3;
4347 "},
4348 cx,
4349 );
4350
4351 editor
4352 });
4353}
4354
4355#[gpui::test]
4356async fn test_backspace(cx: &mut TestAppContext) {
4357 init_test(cx, |_| {});
4358
4359 let mut cx = EditorTestContext::new(cx).await;
4360
4361 // Basic backspace
4362 cx.set_state(indoc! {"
4363 onˇe two three
4364 fou«rˇ» five six
4365 seven «ˇeight nine
4366 »ten
4367 "});
4368 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4369 cx.assert_editor_state(indoc! {"
4370 oˇe two three
4371 fouˇ five six
4372 seven ˇten
4373 "});
4374
4375 // Test backspace inside and around indents
4376 cx.set_state(indoc! {"
4377 zero
4378 ˇone
4379 ˇtwo
4380 ˇ ˇ ˇ three
4381 ˇ ˇ four
4382 "});
4383 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4384 cx.assert_editor_state(indoc! {"
4385 zero
4386 ˇone
4387 ˇtwo
4388 ˇ threeˇ four
4389 "});
4390}
4391
4392#[gpui::test]
4393async fn test_delete(cx: &mut TestAppContext) {
4394 init_test(cx, |_| {});
4395
4396 let mut cx = EditorTestContext::new(cx).await;
4397 cx.set_state(indoc! {"
4398 onˇe two three
4399 fou«rˇ» five six
4400 seven «ˇeight nine
4401 »ten
4402 "});
4403 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
4404 cx.assert_editor_state(indoc! {"
4405 onˇ two three
4406 fouˇ five six
4407 seven ˇten
4408 "});
4409}
4410
4411#[gpui::test]
4412fn test_delete_line(cx: &mut TestAppContext) {
4413 init_test(cx, |_| {});
4414
4415 let editor = cx.add_window(|window, cx| {
4416 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4417 build_editor(buffer, window, cx)
4418 });
4419 _ = editor.update(cx, |editor, window, cx| {
4420 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4421 s.select_display_ranges([
4422 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4423 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4424 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4425 ])
4426 });
4427 editor.delete_line(&DeleteLine, window, cx);
4428 assert_eq!(editor.display_text(cx), "ghi");
4429 assert_eq!(
4430 display_ranges(editor, cx),
4431 vec![
4432 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
4433 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4434 ]
4435 );
4436 });
4437
4438 let editor = cx.add_window(|window, cx| {
4439 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4440 build_editor(buffer, window, cx)
4441 });
4442 _ = editor.update(cx, |editor, window, cx| {
4443 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4444 s.select_display_ranges([
4445 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
4446 ])
4447 });
4448 editor.delete_line(&DeleteLine, window, cx);
4449 assert_eq!(editor.display_text(cx), "ghi\n");
4450 assert_eq!(
4451 display_ranges(editor, cx),
4452 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
4453 );
4454 });
4455
4456 let editor = cx.add_window(|window, cx| {
4457 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n\njkl\nmno", cx);
4458 build_editor(buffer, window, cx)
4459 });
4460 _ = editor.update(cx, |editor, window, cx| {
4461 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4462 s.select_display_ranges([
4463 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(2), 1)
4464 ])
4465 });
4466 editor.delete_line(&DeleteLine, window, cx);
4467 assert_eq!(editor.display_text(cx), "\njkl\nmno");
4468 assert_eq!(
4469 display_ranges(editor, cx),
4470 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
4471 );
4472 });
4473}
4474
4475#[gpui::test]
4476fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
4477 init_test(cx, |_| {});
4478
4479 cx.add_window(|window, cx| {
4480 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4481 let mut editor = build_editor(buffer.clone(), window, cx);
4482 let buffer = buffer.read(cx).as_singleton().unwrap();
4483
4484 assert_eq!(
4485 editor
4486 .selections
4487 .ranges::<Point>(&editor.display_snapshot(cx)),
4488 &[Point::new(0, 0)..Point::new(0, 0)]
4489 );
4490
4491 // When on single line, replace newline at end by space
4492 editor.join_lines(&JoinLines, window, cx);
4493 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4494 assert_eq!(
4495 editor
4496 .selections
4497 .ranges::<Point>(&editor.display_snapshot(cx)),
4498 &[Point::new(0, 3)..Point::new(0, 3)]
4499 );
4500
4501 // When multiple lines are selected, remove newlines that are spanned by the selection
4502 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4503 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
4504 });
4505 editor.join_lines(&JoinLines, window, cx);
4506 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
4507 assert_eq!(
4508 editor
4509 .selections
4510 .ranges::<Point>(&editor.display_snapshot(cx)),
4511 &[Point::new(0, 11)..Point::new(0, 11)]
4512 );
4513
4514 // Undo should be transactional
4515 editor.undo(&Undo, window, cx);
4516 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4517 assert_eq!(
4518 editor
4519 .selections
4520 .ranges::<Point>(&editor.display_snapshot(cx)),
4521 &[Point::new(0, 5)..Point::new(2, 2)]
4522 );
4523
4524 // When joining an empty line don't insert a space
4525 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4526 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
4527 });
4528 editor.join_lines(&JoinLines, window, cx);
4529 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
4530 assert_eq!(
4531 editor
4532 .selections
4533 .ranges::<Point>(&editor.display_snapshot(cx)),
4534 [Point::new(2, 3)..Point::new(2, 3)]
4535 );
4536
4537 // We can remove trailing newlines
4538 editor.join_lines(&JoinLines, window, cx);
4539 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4540 assert_eq!(
4541 editor
4542 .selections
4543 .ranges::<Point>(&editor.display_snapshot(cx)),
4544 [Point::new(2, 3)..Point::new(2, 3)]
4545 );
4546
4547 // We don't blow up on the last line
4548 editor.join_lines(&JoinLines, window, cx);
4549 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4550 assert_eq!(
4551 editor
4552 .selections
4553 .ranges::<Point>(&editor.display_snapshot(cx)),
4554 [Point::new(2, 3)..Point::new(2, 3)]
4555 );
4556
4557 // reset to test indentation
4558 editor.buffer.update(cx, |buffer, cx| {
4559 buffer.edit(
4560 [
4561 (Point::new(1, 0)..Point::new(1, 2), " "),
4562 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
4563 ],
4564 None,
4565 cx,
4566 )
4567 });
4568
4569 // We remove any leading spaces
4570 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
4571 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4572 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
4573 });
4574 editor.join_lines(&JoinLines, window, cx);
4575 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
4576
4577 // We don't insert a space for a line containing only spaces
4578 editor.join_lines(&JoinLines, window, cx);
4579 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
4580
4581 // We ignore any leading tabs
4582 editor.join_lines(&JoinLines, window, cx);
4583 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
4584
4585 editor
4586 });
4587}
4588
4589#[gpui::test]
4590fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
4591 init_test(cx, |_| {});
4592
4593 cx.add_window(|window, cx| {
4594 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4595 let mut editor = build_editor(buffer.clone(), window, cx);
4596 let buffer = buffer.read(cx).as_singleton().unwrap();
4597
4598 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4599 s.select_ranges([
4600 Point::new(0, 2)..Point::new(1, 1),
4601 Point::new(1, 2)..Point::new(1, 2),
4602 Point::new(3, 1)..Point::new(3, 2),
4603 ])
4604 });
4605
4606 editor.join_lines(&JoinLines, window, cx);
4607 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
4608
4609 assert_eq!(
4610 editor
4611 .selections
4612 .ranges::<Point>(&editor.display_snapshot(cx)),
4613 [
4614 Point::new(0, 7)..Point::new(0, 7),
4615 Point::new(1, 3)..Point::new(1, 3)
4616 ]
4617 );
4618 editor
4619 });
4620}
4621
4622#[gpui::test]
4623async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
4624 init_test(cx, |_| {});
4625
4626 let mut cx = EditorTestContext::new(cx).await;
4627
4628 let diff_base = r#"
4629 Line 0
4630 Line 1
4631 Line 2
4632 Line 3
4633 "#
4634 .unindent();
4635
4636 cx.set_state(
4637 &r#"
4638 ˇLine 0
4639 Line 1
4640 Line 2
4641 Line 3
4642 "#
4643 .unindent(),
4644 );
4645
4646 cx.set_head_text(&diff_base);
4647 executor.run_until_parked();
4648
4649 // Join lines
4650 cx.update_editor(|editor, window, cx| {
4651 editor.join_lines(&JoinLines, window, cx);
4652 });
4653 executor.run_until_parked();
4654
4655 cx.assert_editor_state(
4656 &r#"
4657 Line 0ˇ Line 1
4658 Line 2
4659 Line 3
4660 "#
4661 .unindent(),
4662 );
4663 // Join again
4664 cx.update_editor(|editor, window, cx| {
4665 editor.join_lines(&JoinLines, window, cx);
4666 });
4667 executor.run_until_parked();
4668
4669 cx.assert_editor_state(
4670 &r#"
4671 Line 0 Line 1ˇ Line 2
4672 Line 3
4673 "#
4674 .unindent(),
4675 );
4676}
4677
4678#[gpui::test]
4679async fn test_custom_newlines_cause_no_false_positive_diffs(
4680 executor: BackgroundExecutor,
4681 cx: &mut TestAppContext,
4682) {
4683 init_test(cx, |_| {});
4684 let mut cx = EditorTestContext::new(cx).await;
4685 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
4686 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
4687 executor.run_until_parked();
4688
4689 cx.update_editor(|editor, window, cx| {
4690 let snapshot = editor.snapshot(window, cx);
4691 assert_eq!(
4692 snapshot
4693 .buffer_snapshot()
4694 .diff_hunks_in_range(0..snapshot.buffer_snapshot().len())
4695 .collect::<Vec<_>>(),
4696 Vec::new(),
4697 "Should not have any diffs for files with custom newlines"
4698 );
4699 });
4700}
4701
4702#[gpui::test]
4703async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
4704 init_test(cx, |_| {});
4705
4706 let mut cx = EditorTestContext::new(cx).await;
4707
4708 // Test sort_lines_case_insensitive()
4709 cx.set_state(indoc! {"
4710 «z
4711 y
4712 x
4713 Z
4714 Y
4715 Xˇ»
4716 "});
4717 cx.update_editor(|e, window, cx| {
4718 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
4719 });
4720 cx.assert_editor_state(indoc! {"
4721 «x
4722 X
4723 y
4724 Y
4725 z
4726 Zˇ»
4727 "});
4728
4729 // Test sort_lines_by_length()
4730 //
4731 // Demonstrates:
4732 // - ∞ is 3 bytes UTF-8, but sorted by its char count (1)
4733 // - sort is stable
4734 cx.set_state(indoc! {"
4735 «123
4736 æ
4737 12
4738 ∞
4739 1
4740 æˇ»
4741 "});
4742 cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
4743 cx.assert_editor_state(indoc! {"
4744 «æ
4745 ∞
4746 1
4747 æ
4748 12
4749 123ˇ»
4750 "});
4751
4752 // Test reverse_lines()
4753 cx.set_state(indoc! {"
4754 «5
4755 4
4756 3
4757 2
4758 1ˇ»
4759 "});
4760 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4761 cx.assert_editor_state(indoc! {"
4762 «1
4763 2
4764 3
4765 4
4766 5ˇ»
4767 "});
4768
4769 // Skip testing shuffle_line()
4770
4771 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4772 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4773
4774 // Don't manipulate when cursor is on single line, but expand the selection
4775 cx.set_state(indoc! {"
4776 ddˇdd
4777 ccc
4778 bb
4779 a
4780 "});
4781 cx.update_editor(|e, window, cx| {
4782 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4783 });
4784 cx.assert_editor_state(indoc! {"
4785 «ddddˇ»
4786 ccc
4787 bb
4788 a
4789 "});
4790
4791 // Basic manipulate case
4792 // Start selection moves to column 0
4793 // End of selection shrinks to fit shorter line
4794 cx.set_state(indoc! {"
4795 dd«d
4796 ccc
4797 bb
4798 aaaaaˇ»
4799 "});
4800 cx.update_editor(|e, window, cx| {
4801 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4802 });
4803 cx.assert_editor_state(indoc! {"
4804 «aaaaa
4805 bb
4806 ccc
4807 dddˇ»
4808 "});
4809
4810 // Manipulate case with newlines
4811 cx.set_state(indoc! {"
4812 dd«d
4813 ccc
4814
4815 bb
4816 aaaaa
4817
4818 ˇ»
4819 "});
4820 cx.update_editor(|e, window, cx| {
4821 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4822 });
4823 cx.assert_editor_state(indoc! {"
4824 «
4825
4826 aaaaa
4827 bb
4828 ccc
4829 dddˇ»
4830
4831 "});
4832
4833 // Adding new line
4834 cx.set_state(indoc! {"
4835 aa«a
4836 bbˇ»b
4837 "});
4838 cx.update_editor(|e, window, cx| {
4839 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4840 });
4841 cx.assert_editor_state(indoc! {"
4842 «aaa
4843 bbb
4844 added_lineˇ»
4845 "});
4846
4847 // Removing line
4848 cx.set_state(indoc! {"
4849 aa«a
4850 bbbˇ»
4851 "});
4852 cx.update_editor(|e, window, cx| {
4853 e.manipulate_immutable_lines(window, cx, |lines| {
4854 lines.pop();
4855 })
4856 });
4857 cx.assert_editor_state(indoc! {"
4858 «aaaˇ»
4859 "});
4860
4861 // Removing all lines
4862 cx.set_state(indoc! {"
4863 aa«a
4864 bbbˇ»
4865 "});
4866 cx.update_editor(|e, window, cx| {
4867 e.manipulate_immutable_lines(window, cx, |lines| {
4868 lines.drain(..);
4869 })
4870 });
4871 cx.assert_editor_state(indoc! {"
4872 ˇ
4873 "});
4874}
4875
4876#[gpui::test]
4877async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4878 init_test(cx, |_| {});
4879
4880 let mut cx = EditorTestContext::new(cx).await;
4881
4882 // Consider continuous selection as single selection
4883 cx.set_state(indoc! {"
4884 Aaa«aa
4885 cˇ»c«c
4886 bb
4887 aaaˇ»aa
4888 "});
4889 cx.update_editor(|e, window, cx| {
4890 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4891 });
4892 cx.assert_editor_state(indoc! {"
4893 «Aaaaa
4894 ccc
4895 bb
4896 aaaaaˇ»
4897 "});
4898
4899 cx.set_state(indoc! {"
4900 Aaa«aa
4901 cˇ»c«c
4902 bb
4903 aaaˇ»aa
4904 "});
4905 cx.update_editor(|e, window, cx| {
4906 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4907 });
4908 cx.assert_editor_state(indoc! {"
4909 «Aaaaa
4910 ccc
4911 bbˇ»
4912 "});
4913
4914 // Consider non continuous selection as distinct dedup operations
4915 cx.set_state(indoc! {"
4916 «aaaaa
4917 bb
4918 aaaaa
4919 aaaaaˇ»
4920
4921 aaa«aaˇ»
4922 "});
4923 cx.update_editor(|e, window, cx| {
4924 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4925 });
4926 cx.assert_editor_state(indoc! {"
4927 «aaaaa
4928 bbˇ»
4929
4930 «aaaaaˇ»
4931 "});
4932}
4933
4934#[gpui::test]
4935async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4936 init_test(cx, |_| {});
4937
4938 let mut cx = EditorTestContext::new(cx).await;
4939
4940 cx.set_state(indoc! {"
4941 «Aaa
4942 aAa
4943 Aaaˇ»
4944 "});
4945 cx.update_editor(|e, window, cx| {
4946 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4947 });
4948 cx.assert_editor_state(indoc! {"
4949 «Aaa
4950 aAaˇ»
4951 "});
4952
4953 cx.set_state(indoc! {"
4954 «Aaa
4955 aAa
4956 aaAˇ»
4957 "});
4958 cx.update_editor(|e, window, cx| {
4959 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4960 });
4961 cx.assert_editor_state(indoc! {"
4962 «Aaaˇ»
4963 "});
4964}
4965
4966#[gpui::test]
4967async fn test_wrap_in_tag_single_selection(cx: &mut TestAppContext) {
4968 init_test(cx, |_| {});
4969
4970 let mut cx = EditorTestContext::new(cx).await;
4971
4972 let js_language = Arc::new(Language::new(
4973 LanguageConfig {
4974 name: "JavaScript".into(),
4975 wrap_characters: Some(language::WrapCharactersConfig {
4976 start_prefix: "<".into(),
4977 start_suffix: ">".into(),
4978 end_prefix: "</".into(),
4979 end_suffix: ">".into(),
4980 }),
4981 ..LanguageConfig::default()
4982 },
4983 None,
4984 ));
4985
4986 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
4987
4988 cx.set_state(indoc! {"
4989 «testˇ»
4990 "});
4991 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4992 cx.assert_editor_state(indoc! {"
4993 <«ˇ»>test</«ˇ»>
4994 "});
4995
4996 cx.set_state(indoc! {"
4997 «test
4998 testˇ»
4999 "});
5000 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5001 cx.assert_editor_state(indoc! {"
5002 <«ˇ»>test
5003 test</«ˇ»>
5004 "});
5005
5006 cx.set_state(indoc! {"
5007 teˇst
5008 "});
5009 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5010 cx.assert_editor_state(indoc! {"
5011 te<«ˇ»></«ˇ»>st
5012 "});
5013}
5014
5015#[gpui::test]
5016async fn test_wrap_in_tag_multi_selection(cx: &mut TestAppContext) {
5017 init_test(cx, |_| {});
5018
5019 let mut cx = EditorTestContext::new(cx).await;
5020
5021 let js_language = Arc::new(Language::new(
5022 LanguageConfig {
5023 name: "JavaScript".into(),
5024 wrap_characters: Some(language::WrapCharactersConfig {
5025 start_prefix: "<".into(),
5026 start_suffix: ">".into(),
5027 end_prefix: "</".into(),
5028 end_suffix: ">".into(),
5029 }),
5030 ..LanguageConfig::default()
5031 },
5032 None,
5033 ));
5034
5035 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
5036
5037 cx.set_state(indoc! {"
5038 «testˇ»
5039 «testˇ» «testˇ»
5040 «testˇ»
5041 "});
5042 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5043 cx.assert_editor_state(indoc! {"
5044 <«ˇ»>test</«ˇ»>
5045 <«ˇ»>test</«ˇ»> <«ˇ»>test</«ˇ»>
5046 <«ˇ»>test</«ˇ»>
5047 "});
5048
5049 cx.set_state(indoc! {"
5050 «test
5051 testˇ»
5052 «test
5053 testˇ»
5054 "});
5055 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5056 cx.assert_editor_state(indoc! {"
5057 <«ˇ»>test
5058 test</«ˇ»>
5059 <«ˇ»>test
5060 test</«ˇ»>
5061 "});
5062}
5063
5064#[gpui::test]
5065async fn test_wrap_in_tag_does_nothing_in_unsupported_languages(cx: &mut TestAppContext) {
5066 init_test(cx, |_| {});
5067
5068 let mut cx = EditorTestContext::new(cx).await;
5069
5070 let plaintext_language = Arc::new(Language::new(
5071 LanguageConfig {
5072 name: "Plain Text".into(),
5073 ..LanguageConfig::default()
5074 },
5075 None,
5076 ));
5077
5078 cx.update_buffer(|buffer, cx| buffer.set_language(Some(plaintext_language), cx));
5079
5080 cx.set_state(indoc! {"
5081 «testˇ»
5082 "});
5083 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5084 cx.assert_editor_state(indoc! {"
5085 «testˇ»
5086 "});
5087}
5088
5089#[gpui::test]
5090async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
5091 init_test(cx, |_| {});
5092
5093 let mut cx = EditorTestContext::new(cx).await;
5094
5095 // Manipulate with multiple selections on a single line
5096 cx.set_state(indoc! {"
5097 dd«dd
5098 cˇ»c«c
5099 bb
5100 aaaˇ»aa
5101 "});
5102 cx.update_editor(|e, window, cx| {
5103 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5104 });
5105 cx.assert_editor_state(indoc! {"
5106 «aaaaa
5107 bb
5108 ccc
5109 ddddˇ»
5110 "});
5111
5112 // Manipulate with multiple disjoin selections
5113 cx.set_state(indoc! {"
5114 5«
5115 4
5116 3
5117 2
5118 1ˇ»
5119
5120 dd«dd
5121 ccc
5122 bb
5123 aaaˇ»aa
5124 "});
5125 cx.update_editor(|e, window, cx| {
5126 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5127 });
5128 cx.assert_editor_state(indoc! {"
5129 «1
5130 2
5131 3
5132 4
5133 5ˇ»
5134
5135 «aaaaa
5136 bb
5137 ccc
5138 ddddˇ»
5139 "});
5140
5141 // Adding lines on each selection
5142 cx.set_state(indoc! {"
5143 2«
5144 1ˇ»
5145
5146 bb«bb
5147 aaaˇ»aa
5148 "});
5149 cx.update_editor(|e, window, cx| {
5150 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
5151 });
5152 cx.assert_editor_state(indoc! {"
5153 «2
5154 1
5155 added lineˇ»
5156
5157 «bbbb
5158 aaaaa
5159 added lineˇ»
5160 "});
5161
5162 // Removing lines on each selection
5163 cx.set_state(indoc! {"
5164 2«
5165 1ˇ»
5166
5167 bb«bb
5168 aaaˇ»aa
5169 "});
5170 cx.update_editor(|e, window, cx| {
5171 e.manipulate_immutable_lines(window, cx, |lines| {
5172 lines.pop();
5173 })
5174 });
5175 cx.assert_editor_state(indoc! {"
5176 «2ˇ»
5177
5178 «bbbbˇ»
5179 "});
5180}
5181
5182#[gpui::test]
5183async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
5184 init_test(cx, |settings| {
5185 settings.defaults.tab_size = NonZeroU32::new(3)
5186 });
5187
5188 let mut cx = EditorTestContext::new(cx).await;
5189
5190 // MULTI SELECTION
5191 // Ln.1 "«" tests empty lines
5192 // Ln.9 tests just leading whitespace
5193 cx.set_state(indoc! {"
5194 «
5195 abc // No indentationˇ»
5196 «\tabc // 1 tabˇ»
5197 \t\tabc « ˇ» // 2 tabs
5198 \t ab«c // Tab followed by space
5199 \tabc // Space followed by tab (3 spaces should be the result)
5200 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5201 abˇ»ˇc ˇ ˇ // Already space indented«
5202 \t
5203 \tabc\tdef // Only the leading tab is manipulatedˇ»
5204 "});
5205 cx.update_editor(|e, window, cx| {
5206 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5207 });
5208 cx.assert_editor_state(
5209 indoc! {"
5210 «
5211 abc // No indentation
5212 abc // 1 tab
5213 abc // 2 tabs
5214 abc // Tab followed by space
5215 abc // Space followed by tab (3 spaces should be the result)
5216 abc // Mixed indentation (tab conversion depends on the column)
5217 abc // Already space indented
5218 ·
5219 abc\tdef // Only the leading tab is manipulatedˇ»
5220 "}
5221 .replace("·", "")
5222 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5223 );
5224
5225 // Test on just a few lines, the others should remain unchanged
5226 // Only lines (3, 5, 10, 11) should change
5227 cx.set_state(
5228 indoc! {"
5229 ·
5230 abc // No indentation
5231 \tabcˇ // 1 tab
5232 \t\tabc // 2 tabs
5233 \t abcˇ // Tab followed by space
5234 \tabc // Space followed by tab (3 spaces should be the result)
5235 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5236 abc // Already space indented
5237 «\t
5238 \tabc\tdef // Only the leading tab is manipulatedˇ»
5239 "}
5240 .replace("·", "")
5241 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5242 );
5243 cx.update_editor(|e, window, cx| {
5244 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5245 });
5246 cx.assert_editor_state(
5247 indoc! {"
5248 ·
5249 abc // No indentation
5250 « abc // 1 tabˇ»
5251 \t\tabc // 2 tabs
5252 « abc // Tab followed by spaceˇ»
5253 \tabc // Space followed by tab (3 spaces should be the result)
5254 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5255 abc // Already space indented
5256 « ·
5257 abc\tdef // Only the leading tab is manipulatedˇ»
5258 "}
5259 .replace("·", "")
5260 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5261 );
5262
5263 // SINGLE SELECTION
5264 // Ln.1 "«" tests empty lines
5265 // Ln.9 tests just leading whitespace
5266 cx.set_state(indoc! {"
5267 «
5268 abc // No indentation
5269 \tabc // 1 tab
5270 \t\tabc // 2 tabs
5271 \t abc // Tab followed by space
5272 \tabc // Space followed by tab (3 spaces should be the result)
5273 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5274 abc // Already space indented
5275 \t
5276 \tabc\tdef // Only the leading tab is manipulatedˇ»
5277 "});
5278 cx.update_editor(|e, window, cx| {
5279 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5280 });
5281 cx.assert_editor_state(
5282 indoc! {"
5283 «
5284 abc // No indentation
5285 abc // 1 tab
5286 abc // 2 tabs
5287 abc // Tab followed by space
5288 abc // Space followed by tab (3 spaces should be the result)
5289 abc // Mixed indentation (tab conversion depends on the column)
5290 abc // Already space indented
5291 ·
5292 abc\tdef // Only the leading tab is manipulatedˇ»
5293 "}
5294 .replace("·", "")
5295 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5296 );
5297}
5298
5299#[gpui::test]
5300async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
5301 init_test(cx, |settings| {
5302 settings.defaults.tab_size = NonZeroU32::new(3)
5303 });
5304
5305 let mut cx = EditorTestContext::new(cx).await;
5306
5307 // MULTI SELECTION
5308 // Ln.1 "«" tests empty lines
5309 // Ln.11 tests just leading whitespace
5310 cx.set_state(indoc! {"
5311 «
5312 abˇ»ˇc // No indentation
5313 abc ˇ ˇ // 1 space (< 3 so dont convert)
5314 abc « // 2 spaces (< 3 so dont convert)
5315 abc // 3 spaces (convert)
5316 abc ˇ» // 5 spaces (1 tab + 2 spaces)
5317 «\tˇ»\t«\tˇ»abc // Already tab indented
5318 «\t abc // Tab followed by space
5319 \tabc // Space followed by tab (should be consumed due to tab)
5320 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5321 \tˇ» «\t
5322 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
5323 "});
5324 cx.update_editor(|e, window, cx| {
5325 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5326 });
5327 cx.assert_editor_state(indoc! {"
5328 «
5329 abc // No indentation
5330 abc // 1 space (< 3 so dont convert)
5331 abc // 2 spaces (< 3 so dont convert)
5332 \tabc // 3 spaces (convert)
5333 \t abc // 5 spaces (1 tab + 2 spaces)
5334 \t\t\tabc // Already tab indented
5335 \t abc // Tab followed by space
5336 \tabc // Space followed by tab (should be consumed due to tab)
5337 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5338 \t\t\t
5339 \tabc \t // Only the leading spaces should be convertedˇ»
5340 "});
5341
5342 // Test on just a few lines, the other should remain unchanged
5343 // Only lines (4, 8, 11, 12) should change
5344 cx.set_state(
5345 indoc! {"
5346 ·
5347 abc // No indentation
5348 abc // 1 space (< 3 so dont convert)
5349 abc // 2 spaces (< 3 so dont convert)
5350 « abc // 3 spaces (convert)ˇ»
5351 abc // 5 spaces (1 tab + 2 spaces)
5352 \t\t\tabc // Already tab indented
5353 \t abc // Tab followed by space
5354 \tabc ˇ // Space followed by tab (should be consumed due to tab)
5355 \t\t \tabc // Mixed indentation
5356 \t \t \t \tabc // Mixed indentation
5357 \t \tˇ
5358 « abc \t // Only the leading spaces should be convertedˇ»
5359 "}
5360 .replace("·", "")
5361 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5362 );
5363 cx.update_editor(|e, window, cx| {
5364 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5365 });
5366 cx.assert_editor_state(
5367 indoc! {"
5368 ·
5369 abc // No indentation
5370 abc // 1 space (< 3 so dont convert)
5371 abc // 2 spaces (< 3 so dont convert)
5372 «\tabc // 3 spaces (convert)ˇ»
5373 abc // 5 spaces (1 tab + 2 spaces)
5374 \t\t\tabc // Already tab indented
5375 \t abc // Tab followed by space
5376 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
5377 \t\t \tabc // Mixed indentation
5378 \t \t \t \tabc // Mixed indentation
5379 «\t\t\t
5380 \tabc \t // Only the leading spaces should be convertedˇ»
5381 "}
5382 .replace("·", "")
5383 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5384 );
5385
5386 // SINGLE SELECTION
5387 // Ln.1 "«" tests empty lines
5388 // Ln.11 tests just leading whitespace
5389 cx.set_state(indoc! {"
5390 «
5391 abc // No indentation
5392 abc // 1 space (< 3 so dont convert)
5393 abc // 2 spaces (< 3 so dont convert)
5394 abc // 3 spaces (convert)
5395 abc // 5 spaces (1 tab + 2 spaces)
5396 \t\t\tabc // Already tab indented
5397 \t abc // Tab followed by space
5398 \tabc // Space followed by tab (should be consumed due to tab)
5399 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5400 \t \t
5401 abc \t // Only the leading spaces should be convertedˇ»
5402 "});
5403 cx.update_editor(|e, window, cx| {
5404 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5405 });
5406 cx.assert_editor_state(indoc! {"
5407 «
5408 abc // No indentation
5409 abc // 1 space (< 3 so dont convert)
5410 abc // 2 spaces (< 3 so dont convert)
5411 \tabc // 3 spaces (convert)
5412 \t abc // 5 spaces (1 tab + 2 spaces)
5413 \t\t\tabc // Already tab indented
5414 \t abc // Tab followed by space
5415 \tabc // Space followed by tab (should be consumed due to tab)
5416 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5417 \t\t\t
5418 \tabc \t // Only the leading spaces should be convertedˇ»
5419 "});
5420}
5421
5422#[gpui::test]
5423async fn test_toggle_case(cx: &mut TestAppContext) {
5424 init_test(cx, |_| {});
5425
5426 let mut cx = EditorTestContext::new(cx).await;
5427
5428 // If all lower case -> upper case
5429 cx.set_state(indoc! {"
5430 «hello worldˇ»
5431 "});
5432 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5433 cx.assert_editor_state(indoc! {"
5434 «HELLO WORLDˇ»
5435 "});
5436
5437 // If all upper case -> lower case
5438 cx.set_state(indoc! {"
5439 «HELLO WORLDˇ»
5440 "});
5441 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5442 cx.assert_editor_state(indoc! {"
5443 «hello worldˇ»
5444 "});
5445
5446 // If any upper case characters are identified -> lower case
5447 // This matches JetBrains IDEs
5448 cx.set_state(indoc! {"
5449 «hEllo worldˇ»
5450 "});
5451 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5452 cx.assert_editor_state(indoc! {"
5453 «hello worldˇ»
5454 "});
5455}
5456
5457#[gpui::test]
5458async fn test_convert_to_sentence_case(cx: &mut TestAppContext) {
5459 init_test(cx, |_| {});
5460
5461 let mut cx = EditorTestContext::new(cx).await;
5462
5463 cx.set_state(indoc! {"
5464 «implement-windows-supportˇ»
5465 "});
5466 cx.update_editor(|e, window, cx| {
5467 e.convert_to_sentence_case(&ConvertToSentenceCase, window, cx)
5468 });
5469 cx.assert_editor_state(indoc! {"
5470 «Implement windows supportˇ»
5471 "});
5472}
5473
5474#[gpui::test]
5475async fn test_manipulate_text(cx: &mut TestAppContext) {
5476 init_test(cx, |_| {});
5477
5478 let mut cx = EditorTestContext::new(cx).await;
5479
5480 // Test convert_to_upper_case()
5481 cx.set_state(indoc! {"
5482 «hello worldˇ»
5483 "});
5484 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5485 cx.assert_editor_state(indoc! {"
5486 «HELLO WORLDˇ»
5487 "});
5488
5489 // Test convert_to_lower_case()
5490 cx.set_state(indoc! {"
5491 «HELLO WORLDˇ»
5492 "});
5493 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
5494 cx.assert_editor_state(indoc! {"
5495 «hello worldˇ»
5496 "});
5497
5498 // Test multiple line, single selection case
5499 cx.set_state(indoc! {"
5500 «The quick brown
5501 fox jumps over
5502 the lazy dogˇ»
5503 "});
5504 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
5505 cx.assert_editor_state(indoc! {"
5506 «The Quick Brown
5507 Fox Jumps Over
5508 The Lazy Dogˇ»
5509 "});
5510
5511 // Test multiple line, single selection case
5512 cx.set_state(indoc! {"
5513 «The quick brown
5514 fox jumps over
5515 the lazy dogˇ»
5516 "});
5517 cx.update_editor(|e, window, cx| {
5518 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
5519 });
5520 cx.assert_editor_state(indoc! {"
5521 «TheQuickBrown
5522 FoxJumpsOver
5523 TheLazyDogˇ»
5524 "});
5525
5526 // From here on out, test more complex cases of manipulate_text()
5527
5528 // Test no selection case - should affect words cursors are in
5529 // Cursor at beginning, middle, and end of word
5530 cx.set_state(indoc! {"
5531 ˇhello big beauˇtiful worldˇ
5532 "});
5533 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5534 cx.assert_editor_state(indoc! {"
5535 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
5536 "});
5537
5538 // Test multiple selections on a single line and across multiple lines
5539 cx.set_state(indoc! {"
5540 «Theˇ» quick «brown
5541 foxˇ» jumps «overˇ»
5542 the «lazyˇ» dog
5543 "});
5544 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5545 cx.assert_editor_state(indoc! {"
5546 «THEˇ» quick «BROWN
5547 FOXˇ» jumps «OVERˇ»
5548 the «LAZYˇ» dog
5549 "});
5550
5551 // Test case where text length grows
5552 cx.set_state(indoc! {"
5553 «tschüߡ»
5554 "});
5555 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5556 cx.assert_editor_state(indoc! {"
5557 «TSCHÜSSˇ»
5558 "});
5559
5560 // Test to make sure we don't crash when text shrinks
5561 cx.set_state(indoc! {"
5562 aaa_bbbˇ
5563 "});
5564 cx.update_editor(|e, window, cx| {
5565 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5566 });
5567 cx.assert_editor_state(indoc! {"
5568 «aaaBbbˇ»
5569 "});
5570
5571 // Test to make sure we all aware of the fact that each word can grow and shrink
5572 // Final selections should be aware of this fact
5573 cx.set_state(indoc! {"
5574 aaa_bˇbb bbˇb_ccc ˇccc_ddd
5575 "});
5576 cx.update_editor(|e, window, cx| {
5577 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5578 });
5579 cx.assert_editor_state(indoc! {"
5580 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
5581 "});
5582
5583 cx.set_state(indoc! {"
5584 «hElLo, WoRld!ˇ»
5585 "});
5586 cx.update_editor(|e, window, cx| {
5587 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
5588 });
5589 cx.assert_editor_state(indoc! {"
5590 «HeLlO, wOrLD!ˇ»
5591 "});
5592
5593 // Test selections with `line_mode() = true`.
5594 cx.update_editor(|editor, _window, _cx| editor.selections.set_line_mode(true));
5595 cx.set_state(indoc! {"
5596 «The quick brown
5597 fox jumps over
5598 tˇ»he lazy dog
5599 "});
5600 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5601 cx.assert_editor_state(indoc! {"
5602 «THE QUICK BROWN
5603 FOX JUMPS OVER
5604 THE LAZY DOGˇ»
5605 "});
5606}
5607
5608#[gpui::test]
5609fn test_duplicate_line(cx: &mut TestAppContext) {
5610 init_test(cx, |_| {});
5611
5612 let editor = cx.add_window(|window, cx| {
5613 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5614 build_editor(buffer, window, cx)
5615 });
5616 _ = editor.update(cx, |editor, window, cx| {
5617 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5618 s.select_display_ranges([
5619 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5620 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5621 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5622 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5623 ])
5624 });
5625 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5626 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5627 assert_eq!(
5628 display_ranges(editor, cx),
5629 vec![
5630 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
5631 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
5632 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5633 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5634 ]
5635 );
5636 });
5637
5638 let editor = cx.add_window(|window, cx| {
5639 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5640 build_editor(buffer, window, cx)
5641 });
5642 _ = editor.update(cx, |editor, window, cx| {
5643 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5644 s.select_display_ranges([
5645 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5646 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5647 ])
5648 });
5649 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5650 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5651 assert_eq!(
5652 display_ranges(editor, cx),
5653 vec![
5654 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
5655 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
5656 ]
5657 );
5658 });
5659
5660 // With `duplicate_line_up` the selections move to the duplicated lines,
5661 // which are inserted above the original lines
5662 let editor = cx.add_window(|window, cx| {
5663 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5664 build_editor(buffer, window, cx)
5665 });
5666 _ = editor.update(cx, |editor, window, cx| {
5667 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5668 s.select_display_ranges([
5669 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5670 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5671 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5672 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5673 ])
5674 });
5675 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5676 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5677 assert_eq!(
5678 display_ranges(editor, cx),
5679 vec![
5680 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5681 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5682 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
5683 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0),
5684 ]
5685 );
5686 });
5687
5688 let editor = cx.add_window(|window, cx| {
5689 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5690 build_editor(buffer, window, cx)
5691 });
5692 _ = editor.update(cx, |editor, window, cx| {
5693 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5694 s.select_display_ranges([
5695 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5696 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5697 ])
5698 });
5699 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5700 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5701 assert_eq!(
5702 display_ranges(editor, cx),
5703 vec![
5704 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5705 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5706 ]
5707 );
5708 });
5709
5710 let editor = cx.add_window(|window, cx| {
5711 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5712 build_editor(buffer, window, cx)
5713 });
5714 _ = editor.update(cx, |editor, window, cx| {
5715 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5716 s.select_display_ranges([
5717 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5718 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5719 ])
5720 });
5721 editor.duplicate_selection(&DuplicateSelection, window, cx);
5722 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
5723 assert_eq!(
5724 display_ranges(editor, cx),
5725 vec![
5726 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5727 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
5728 ]
5729 );
5730 });
5731}
5732
5733#[gpui::test]
5734fn test_move_line_up_down(cx: &mut TestAppContext) {
5735 init_test(cx, |_| {});
5736
5737 let editor = cx.add_window(|window, cx| {
5738 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5739 build_editor(buffer, window, cx)
5740 });
5741 _ = editor.update(cx, |editor, window, cx| {
5742 editor.fold_creases(
5743 vec![
5744 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5745 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5746 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5747 ],
5748 true,
5749 window,
5750 cx,
5751 );
5752 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5753 s.select_display_ranges([
5754 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5755 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5756 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5757 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
5758 ])
5759 });
5760 assert_eq!(
5761 editor.display_text(cx),
5762 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
5763 );
5764
5765 editor.move_line_up(&MoveLineUp, window, cx);
5766 assert_eq!(
5767 editor.display_text(cx),
5768 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
5769 );
5770 assert_eq!(
5771 display_ranges(editor, cx),
5772 vec![
5773 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5774 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5775 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5776 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5777 ]
5778 );
5779 });
5780
5781 _ = editor.update(cx, |editor, window, cx| {
5782 editor.move_line_down(&MoveLineDown, window, cx);
5783 assert_eq!(
5784 editor.display_text(cx),
5785 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
5786 );
5787 assert_eq!(
5788 display_ranges(editor, cx),
5789 vec![
5790 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5791 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5792 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5793 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5794 ]
5795 );
5796 });
5797
5798 _ = editor.update(cx, |editor, window, cx| {
5799 editor.move_line_down(&MoveLineDown, window, cx);
5800 assert_eq!(
5801 editor.display_text(cx),
5802 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
5803 );
5804 assert_eq!(
5805 display_ranges(editor, cx),
5806 vec![
5807 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5808 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5809 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5810 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5811 ]
5812 );
5813 });
5814
5815 _ = editor.update(cx, |editor, window, cx| {
5816 editor.move_line_up(&MoveLineUp, window, cx);
5817 assert_eq!(
5818 editor.display_text(cx),
5819 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
5820 );
5821 assert_eq!(
5822 display_ranges(editor, cx),
5823 vec![
5824 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5825 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5826 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5827 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5828 ]
5829 );
5830 });
5831}
5832
5833#[gpui::test]
5834fn test_move_line_up_selection_at_end_of_fold(cx: &mut TestAppContext) {
5835 init_test(cx, |_| {});
5836 let editor = cx.add_window(|window, cx| {
5837 let buffer = MultiBuffer::build_simple("\n\n\n\n\n\naaaa\nbbbb\ncccc", cx);
5838 build_editor(buffer, window, cx)
5839 });
5840 _ = editor.update(cx, |editor, window, cx| {
5841 editor.fold_creases(
5842 vec![Crease::simple(
5843 Point::new(6, 4)..Point::new(7, 4),
5844 FoldPlaceholder::test(),
5845 )],
5846 true,
5847 window,
5848 cx,
5849 );
5850 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5851 s.select_ranges([Point::new(7, 4)..Point::new(7, 4)])
5852 });
5853 assert_eq!(editor.display_text(cx), "\n\n\n\n\n\naaaa⋯\ncccc");
5854 editor.move_line_up(&MoveLineUp, window, cx);
5855 let buffer_text = editor.buffer.read(cx).snapshot(cx).text();
5856 assert_eq!(buffer_text, "\n\n\n\n\naaaa\nbbbb\n\ncccc");
5857 });
5858}
5859
5860#[gpui::test]
5861fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
5862 init_test(cx, |_| {});
5863
5864 let editor = cx.add_window(|window, cx| {
5865 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5866 build_editor(buffer, window, cx)
5867 });
5868 _ = editor.update(cx, |editor, window, cx| {
5869 let snapshot = editor.buffer.read(cx).snapshot(cx);
5870 editor.insert_blocks(
5871 [BlockProperties {
5872 style: BlockStyle::Fixed,
5873 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
5874 height: Some(1),
5875 render: Arc::new(|_| div().into_any()),
5876 priority: 0,
5877 }],
5878 Some(Autoscroll::fit()),
5879 cx,
5880 );
5881 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5882 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
5883 });
5884 editor.move_line_down(&MoveLineDown, window, cx);
5885 });
5886}
5887
5888#[gpui::test]
5889async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
5890 init_test(cx, |_| {});
5891
5892 let mut cx = EditorTestContext::new(cx).await;
5893 cx.set_state(
5894 &"
5895 ˇzero
5896 one
5897 two
5898 three
5899 four
5900 five
5901 "
5902 .unindent(),
5903 );
5904
5905 // Create a four-line block that replaces three lines of text.
5906 cx.update_editor(|editor, window, cx| {
5907 let snapshot = editor.snapshot(window, cx);
5908 let snapshot = &snapshot.buffer_snapshot();
5909 let placement = BlockPlacement::Replace(
5910 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
5911 );
5912 editor.insert_blocks(
5913 [BlockProperties {
5914 placement,
5915 height: Some(4),
5916 style: BlockStyle::Sticky,
5917 render: Arc::new(|_| gpui::div().into_any_element()),
5918 priority: 0,
5919 }],
5920 None,
5921 cx,
5922 );
5923 });
5924
5925 // Move down so that the cursor touches the block.
5926 cx.update_editor(|editor, window, cx| {
5927 editor.move_down(&Default::default(), window, cx);
5928 });
5929 cx.assert_editor_state(
5930 &"
5931 zero
5932 «one
5933 two
5934 threeˇ»
5935 four
5936 five
5937 "
5938 .unindent(),
5939 );
5940
5941 // Move down past the block.
5942 cx.update_editor(|editor, window, cx| {
5943 editor.move_down(&Default::default(), window, cx);
5944 });
5945 cx.assert_editor_state(
5946 &"
5947 zero
5948 one
5949 two
5950 three
5951 ˇfour
5952 five
5953 "
5954 .unindent(),
5955 );
5956}
5957
5958#[gpui::test]
5959fn test_transpose(cx: &mut TestAppContext) {
5960 init_test(cx, |_| {});
5961
5962 _ = cx.add_window(|window, cx| {
5963 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
5964 editor.set_style(EditorStyle::default(), window, cx);
5965 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5966 s.select_ranges([1..1])
5967 });
5968 editor.transpose(&Default::default(), window, cx);
5969 assert_eq!(editor.text(cx), "bac");
5970 assert_eq!(
5971 editor.selections.ranges(&editor.display_snapshot(cx)),
5972 [2..2]
5973 );
5974
5975 editor.transpose(&Default::default(), window, cx);
5976 assert_eq!(editor.text(cx), "bca");
5977 assert_eq!(
5978 editor.selections.ranges(&editor.display_snapshot(cx)),
5979 [3..3]
5980 );
5981
5982 editor.transpose(&Default::default(), window, cx);
5983 assert_eq!(editor.text(cx), "bac");
5984 assert_eq!(
5985 editor.selections.ranges(&editor.display_snapshot(cx)),
5986 [3..3]
5987 );
5988
5989 editor
5990 });
5991
5992 _ = cx.add_window(|window, cx| {
5993 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5994 editor.set_style(EditorStyle::default(), window, cx);
5995 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5996 s.select_ranges([3..3])
5997 });
5998 editor.transpose(&Default::default(), window, cx);
5999 assert_eq!(editor.text(cx), "acb\nde");
6000 assert_eq!(
6001 editor.selections.ranges(&editor.display_snapshot(cx)),
6002 [3..3]
6003 );
6004
6005 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6006 s.select_ranges([4..4])
6007 });
6008 editor.transpose(&Default::default(), window, cx);
6009 assert_eq!(editor.text(cx), "acbd\ne");
6010 assert_eq!(
6011 editor.selections.ranges(&editor.display_snapshot(cx)),
6012 [5..5]
6013 );
6014
6015 editor.transpose(&Default::default(), window, cx);
6016 assert_eq!(editor.text(cx), "acbde\n");
6017 assert_eq!(
6018 editor.selections.ranges(&editor.display_snapshot(cx)),
6019 [6..6]
6020 );
6021
6022 editor.transpose(&Default::default(), window, cx);
6023 assert_eq!(editor.text(cx), "acbd\ne");
6024 assert_eq!(
6025 editor.selections.ranges(&editor.display_snapshot(cx)),
6026 [6..6]
6027 );
6028
6029 editor
6030 });
6031
6032 _ = cx.add_window(|window, cx| {
6033 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
6034 editor.set_style(EditorStyle::default(), window, cx);
6035 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6036 s.select_ranges([1..1, 2..2, 4..4])
6037 });
6038 editor.transpose(&Default::default(), window, cx);
6039 assert_eq!(editor.text(cx), "bacd\ne");
6040 assert_eq!(
6041 editor.selections.ranges(&editor.display_snapshot(cx)),
6042 [2..2, 3..3, 5..5]
6043 );
6044
6045 editor.transpose(&Default::default(), window, cx);
6046 assert_eq!(editor.text(cx), "bcade\n");
6047 assert_eq!(
6048 editor.selections.ranges(&editor.display_snapshot(cx)),
6049 [3..3, 4..4, 6..6]
6050 );
6051
6052 editor.transpose(&Default::default(), window, cx);
6053 assert_eq!(editor.text(cx), "bcda\ne");
6054 assert_eq!(
6055 editor.selections.ranges(&editor.display_snapshot(cx)),
6056 [4..4, 6..6]
6057 );
6058
6059 editor.transpose(&Default::default(), window, cx);
6060 assert_eq!(editor.text(cx), "bcade\n");
6061 assert_eq!(
6062 editor.selections.ranges(&editor.display_snapshot(cx)),
6063 [4..4, 6..6]
6064 );
6065
6066 editor.transpose(&Default::default(), window, cx);
6067 assert_eq!(editor.text(cx), "bcaed\n");
6068 assert_eq!(
6069 editor.selections.ranges(&editor.display_snapshot(cx)),
6070 [5..5, 6..6]
6071 );
6072
6073 editor
6074 });
6075
6076 _ = cx.add_window(|window, cx| {
6077 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
6078 editor.set_style(EditorStyle::default(), window, cx);
6079 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6080 s.select_ranges([4..4])
6081 });
6082 editor.transpose(&Default::default(), window, cx);
6083 assert_eq!(editor.text(cx), "🏀🍐✋");
6084 assert_eq!(
6085 editor.selections.ranges(&editor.display_snapshot(cx)),
6086 [8..8]
6087 );
6088
6089 editor.transpose(&Default::default(), window, cx);
6090 assert_eq!(editor.text(cx), "🏀✋🍐");
6091 assert_eq!(
6092 editor.selections.ranges(&editor.display_snapshot(cx)),
6093 [11..11]
6094 );
6095
6096 editor.transpose(&Default::default(), window, cx);
6097 assert_eq!(editor.text(cx), "🏀🍐✋");
6098 assert_eq!(
6099 editor.selections.ranges(&editor.display_snapshot(cx)),
6100 [11..11]
6101 );
6102
6103 editor
6104 });
6105}
6106
6107#[gpui::test]
6108async fn test_rewrap(cx: &mut TestAppContext) {
6109 init_test(cx, |settings| {
6110 settings.languages.0.extend([
6111 (
6112 "Markdown".into(),
6113 LanguageSettingsContent {
6114 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
6115 preferred_line_length: Some(40),
6116 ..Default::default()
6117 },
6118 ),
6119 (
6120 "Plain Text".into(),
6121 LanguageSettingsContent {
6122 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
6123 preferred_line_length: Some(40),
6124 ..Default::default()
6125 },
6126 ),
6127 (
6128 "C++".into(),
6129 LanguageSettingsContent {
6130 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6131 preferred_line_length: Some(40),
6132 ..Default::default()
6133 },
6134 ),
6135 (
6136 "Python".into(),
6137 LanguageSettingsContent {
6138 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6139 preferred_line_length: Some(40),
6140 ..Default::default()
6141 },
6142 ),
6143 (
6144 "Rust".into(),
6145 LanguageSettingsContent {
6146 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6147 preferred_line_length: Some(40),
6148 ..Default::default()
6149 },
6150 ),
6151 ])
6152 });
6153
6154 let mut cx = EditorTestContext::new(cx).await;
6155
6156 let cpp_language = Arc::new(Language::new(
6157 LanguageConfig {
6158 name: "C++".into(),
6159 line_comments: vec!["// ".into()],
6160 ..LanguageConfig::default()
6161 },
6162 None,
6163 ));
6164 let python_language = Arc::new(Language::new(
6165 LanguageConfig {
6166 name: "Python".into(),
6167 line_comments: vec!["# ".into()],
6168 ..LanguageConfig::default()
6169 },
6170 None,
6171 ));
6172 let markdown_language = Arc::new(Language::new(
6173 LanguageConfig {
6174 name: "Markdown".into(),
6175 rewrap_prefixes: vec![
6176 regex::Regex::new("\\d+\\.\\s+").unwrap(),
6177 regex::Regex::new("[-*+]\\s+").unwrap(),
6178 ],
6179 ..LanguageConfig::default()
6180 },
6181 None,
6182 ));
6183 let rust_language = Arc::new(
6184 Language::new(
6185 LanguageConfig {
6186 name: "Rust".into(),
6187 line_comments: vec!["// ".into(), "/// ".into()],
6188 ..LanguageConfig::default()
6189 },
6190 Some(tree_sitter_rust::LANGUAGE.into()),
6191 )
6192 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
6193 .unwrap(),
6194 );
6195
6196 let plaintext_language = Arc::new(Language::new(
6197 LanguageConfig {
6198 name: "Plain Text".into(),
6199 ..LanguageConfig::default()
6200 },
6201 None,
6202 ));
6203
6204 // Test basic rewrapping of a long line with a cursor
6205 assert_rewrap(
6206 indoc! {"
6207 // ˇThis is a long comment that needs to be wrapped.
6208 "},
6209 indoc! {"
6210 // ˇThis is a long comment that needs to
6211 // be wrapped.
6212 "},
6213 cpp_language.clone(),
6214 &mut cx,
6215 );
6216
6217 // Test rewrapping a full selection
6218 assert_rewrap(
6219 indoc! {"
6220 «// This selected long comment needs to be wrapped.ˇ»"
6221 },
6222 indoc! {"
6223 «// This selected long comment needs to
6224 // be wrapped.ˇ»"
6225 },
6226 cpp_language.clone(),
6227 &mut cx,
6228 );
6229
6230 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
6231 assert_rewrap(
6232 indoc! {"
6233 // ˇThis is the first line.
6234 // Thisˇ is the second line.
6235 // This is the thirdˇ line, all part of one paragraph.
6236 "},
6237 indoc! {"
6238 // ˇThis is the first line. Thisˇ is the
6239 // second line. This is the thirdˇ line,
6240 // all part of one paragraph.
6241 "},
6242 cpp_language.clone(),
6243 &mut cx,
6244 );
6245
6246 // Test multiple cursors in different paragraphs trigger separate rewraps
6247 assert_rewrap(
6248 indoc! {"
6249 // ˇThis is the first paragraph, first line.
6250 // ˇThis is the first paragraph, second line.
6251
6252 // ˇThis is the second paragraph, first line.
6253 // ˇThis is the second paragraph, second line.
6254 "},
6255 indoc! {"
6256 // ˇThis is the first paragraph, first
6257 // line. ˇThis is the first paragraph,
6258 // second line.
6259
6260 // ˇThis is the second paragraph, first
6261 // line. ˇThis is the second paragraph,
6262 // second line.
6263 "},
6264 cpp_language.clone(),
6265 &mut cx,
6266 );
6267
6268 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
6269 assert_rewrap(
6270 indoc! {"
6271 «// A regular long long comment to be wrapped.
6272 /// A documentation long comment to be wrapped.ˇ»
6273 "},
6274 indoc! {"
6275 «// A regular long long comment to be
6276 // wrapped.
6277 /// A documentation long comment to be
6278 /// wrapped.ˇ»
6279 "},
6280 rust_language.clone(),
6281 &mut cx,
6282 );
6283
6284 // Test that change in indentation level trigger seperate rewraps
6285 assert_rewrap(
6286 indoc! {"
6287 fn foo() {
6288 «// This is a long comment at the base indent.
6289 // This is a long comment at the next indent.ˇ»
6290 }
6291 "},
6292 indoc! {"
6293 fn foo() {
6294 «// This is a long comment at the
6295 // base indent.
6296 // This is a long comment at the
6297 // next indent.ˇ»
6298 }
6299 "},
6300 rust_language.clone(),
6301 &mut cx,
6302 );
6303
6304 // Test that different comment prefix characters (e.g., '#') are handled correctly
6305 assert_rewrap(
6306 indoc! {"
6307 # ˇThis is a long comment using a pound sign.
6308 "},
6309 indoc! {"
6310 # ˇThis is a long comment using a pound
6311 # sign.
6312 "},
6313 python_language,
6314 &mut cx,
6315 );
6316
6317 // Test rewrapping only affects comments, not code even when selected
6318 assert_rewrap(
6319 indoc! {"
6320 «/// This doc comment is long and should be wrapped.
6321 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6322 "},
6323 indoc! {"
6324 «/// This doc comment is long and should
6325 /// be wrapped.
6326 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6327 "},
6328 rust_language.clone(),
6329 &mut cx,
6330 );
6331
6332 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
6333 assert_rewrap(
6334 indoc! {"
6335 # Header
6336
6337 A long long long line of markdown text to wrap.ˇ
6338 "},
6339 indoc! {"
6340 # Header
6341
6342 A long long long line of markdown text
6343 to wrap.ˇ
6344 "},
6345 markdown_language.clone(),
6346 &mut cx,
6347 );
6348
6349 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
6350 assert_rewrap(
6351 indoc! {"
6352 «1. This is a numbered list item that is very long and needs to be wrapped properly.
6353 2. This is a numbered list item that is very long and needs to be wrapped properly.
6354 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
6355 "},
6356 indoc! {"
6357 «1. This is a numbered list item that is
6358 very long and needs to be wrapped
6359 properly.
6360 2. This is a numbered list item that is
6361 very long and needs to be wrapped
6362 properly.
6363 - This is an unordered list item that is
6364 also very long and should not merge
6365 with the numbered item.ˇ»
6366 "},
6367 markdown_language.clone(),
6368 &mut cx,
6369 );
6370
6371 // Test that rewrapping add indents for rewrapping boundary if not exists already.
6372 assert_rewrap(
6373 indoc! {"
6374 «1. This is a numbered list item that is
6375 very long and needs to be wrapped
6376 properly.
6377 2. This is a numbered list item that is
6378 very long and needs to be wrapped
6379 properly.
6380 - This is an unordered list item that is
6381 also very long and should not merge with
6382 the numbered item.ˇ»
6383 "},
6384 indoc! {"
6385 «1. This is a numbered list item that is
6386 very long and needs to be wrapped
6387 properly.
6388 2. This is a numbered list item that is
6389 very long and needs to be wrapped
6390 properly.
6391 - This is an unordered list item that is
6392 also very long and should not merge
6393 with the numbered item.ˇ»
6394 "},
6395 markdown_language.clone(),
6396 &mut cx,
6397 );
6398
6399 // Test that rewrapping maintain indents even when they already exists.
6400 assert_rewrap(
6401 indoc! {"
6402 «1. This is a numbered list
6403 item that is very long and needs to be wrapped properly.
6404 2. This is a numbered list
6405 item that is very long and needs to be wrapped properly.
6406 - This is an unordered list item that is also very long and
6407 should not merge with the numbered item.ˇ»
6408 "},
6409 indoc! {"
6410 «1. This is a numbered list item that is
6411 very long and needs to be wrapped
6412 properly.
6413 2. This is a numbered list item that is
6414 very long and needs to be wrapped
6415 properly.
6416 - This is an unordered list item that is
6417 also very long and should not merge
6418 with the numbered item.ˇ»
6419 "},
6420 markdown_language,
6421 &mut cx,
6422 );
6423
6424 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
6425 assert_rewrap(
6426 indoc! {"
6427 ˇThis is a very long line of plain text that will be wrapped.
6428 "},
6429 indoc! {"
6430 ˇThis is a very long line of plain text
6431 that will be wrapped.
6432 "},
6433 plaintext_language.clone(),
6434 &mut cx,
6435 );
6436
6437 // Test that non-commented code acts as a paragraph boundary within a selection
6438 assert_rewrap(
6439 indoc! {"
6440 «// This is the first long comment block to be wrapped.
6441 fn my_func(a: u32);
6442 // This is the second long comment block to be wrapped.ˇ»
6443 "},
6444 indoc! {"
6445 «// This is the first long comment block
6446 // to be wrapped.
6447 fn my_func(a: u32);
6448 // This is the second long comment block
6449 // to be wrapped.ˇ»
6450 "},
6451 rust_language,
6452 &mut cx,
6453 );
6454
6455 // Test rewrapping multiple selections, including ones with blank lines or tabs
6456 assert_rewrap(
6457 indoc! {"
6458 «ˇThis is a very long line that will be wrapped.
6459
6460 This is another paragraph in the same selection.»
6461
6462 «\tThis is a very long indented line that will be wrapped.ˇ»
6463 "},
6464 indoc! {"
6465 «ˇThis is a very long line that will be
6466 wrapped.
6467
6468 This is another paragraph in the same
6469 selection.»
6470
6471 «\tThis is a very long indented line
6472 \tthat will be wrapped.ˇ»
6473 "},
6474 plaintext_language,
6475 &mut cx,
6476 );
6477
6478 // Test that an empty comment line acts as a paragraph boundary
6479 assert_rewrap(
6480 indoc! {"
6481 // ˇThis is a long comment that will be wrapped.
6482 //
6483 // And this is another long comment that will also be wrapped.ˇ
6484 "},
6485 indoc! {"
6486 // ˇThis is a long comment that will be
6487 // wrapped.
6488 //
6489 // And this is another long comment that
6490 // will also be wrapped.ˇ
6491 "},
6492 cpp_language,
6493 &mut cx,
6494 );
6495
6496 #[track_caller]
6497 fn assert_rewrap(
6498 unwrapped_text: &str,
6499 wrapped_text: &str,
6500 language: Arc<Language>,
6501 cx: &mut EditorTestContext,
6502 ) {
6503 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6504 cx.set_state(unwrapped_text);
6505 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6506 cx.assert_editor_state(wrapped_text);
6507 }
6508}
6509
6510#[gpui::test]
6511async fn test_rewrap_block_comments(cx: &mut TestAppContext) {
6512 init_test(cx, |settings| {
6513 settings.languages.0.extend([(
6514 "Rust".into(),
6515 LanguageSettingsContent {
6516 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6517 preferred_line_length: Some(40),
6518 ..Default::default()
6519 },
6520 )])
6521 });
6522
6523 let mut cx = EditorTestContext::new(cx).await;
6524
6525 let rust_lang = Arc::new(
6526 Language::new(
6527 LanguageConfig {
6528 name: "Rust".into(),
6529 line_comments: vec!["// ".into()],
6530 block_comment: Some(BlockCommentConfig {
6531 start: "/*".into(),
6532 end: "*/".into(),
6533 prefix: "* ".into(),
6534 tab_size: 1,
6535 }),
6536 documentation_comment: Some(BlockCommentConfig {
6537 start: "/**".into(),
6538 end: "*/".into(),
6539 prefix: "* ".into(),
6540 tab_size: 1,
6541 }),
6542
6543 ..LanguageConfig::default()
6544 },
6545 Some(tree_sitter_rust::LANGUAGE.into()),
6546 )
6547 .with_override_query("[(line_comment) (block_comment)] @comment.inclusive")
6548 .unwrap(),
6549 );
6550
6551 // regular block comment
6552 assert_rewrap(
6553 indoc! {"
6554 /*
6555 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6556 */
6557 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6558 "},
6559 indoc! {"
6560 /*
6561 *ˇ Lorem ipsum dolor sit amet,
6562 * consectetur adipiscing elit.
6563 */
6564 /*
6565 *ˇ Lorem ipsum dolor sit amet,
6566 * consectetur adipiscing elit.
6567 */
6568 "},
6569 rust_lang.clone(),
6570 &mut cx,
6571 );
6572
6573 // indent is respected
6574 assert_rewrap(
6575 indoc! {"
6576 {}
6577 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6578 "},
6579 indoc! {"
6580 {}
6581 /*
6582 *ˇ Lorem ipsum dolor sit amet,
6583 * consectetur adipiscing elit.
6584 */
6585 "},
6586 rust_lang.clone(),
6587 &mut cx,
6588 );
6589
6590 // short block comments with inline delimiters
6591 assert_rewrap(
6592 indoc! {"
6593 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6594 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6595 */
6596 /*
6597 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6598 "},
6599 indoc! {"
6600 /*
6601 *ˇ Lorem ipsum dolor sit amet,
6602 * consectetur adipiscing elit.
6603 */
6604 /*
6605 *ˇ Lorem ipsum dolor sit amet,
6606 * consectetur adipiscing elit.
6607 */
6608 /*
6609 *ˇ Lorem ipsum dolor sit amet,
6610 * consectetur adipiscing elit.
6611 */
6612 "},
6613 rust_lang.clone(),
6614 &mut cx,
6615 );
6616
6617 // multiline block comment with inline start/end delimiters
6618 assert_rewrap(
6619 indoc! {"
6620 /*ˇ Lorem ipsum dolor sit amet,
6621 * consectetur adipiscing elit. */
6622 "},
6623 indoc! {"
6624 /*
6625 *ˇ Lorem ipsum dolor sit amet,
6626 * consectetur adipiscing elit.
6627 */
6628 "},
6629 rust_lang.clone(),
6630 &mut cx,
6631 );
6632
6633 // block comment rewrap still respects paragraph bounds
6634 assert_rewrap(
6635 indoc! {"
6636 /*
6637 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6638 *
6639 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6640 */
6641 "},
6642 indoc! {"
6643 /*
6644 *ˇ Lorem ipsum dolor sit amet,
6645 * consectetur adipiscing elit.
6646 *
6647 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6648 */
6649 "},
6650 rust_lang.clone(),
6651 &mut cx,
6652 );
6653
6654 // documentation comments
6655 assert_rewrap(
6656 indoc! {"
6657 /**ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6658 /**
6659 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6660 */
6661 "},
6662 indoc! {"
6663 /**
6664 *ˇ Lorem ipsum dolor sit amet,
6665 * consectetur adipiscing elit.
6666 */
6667 /**
6668 *ˇ Lorem ipsum dolor sit amet,
6669 * consectetur adipiscing elit.
6670 */
6671 "},
6672 rust_lang.clone(),
6673 &mut cx,
6674 );
6675
6676 // different, adjacent comments
6677 assert_rewrap(
6678 indoc! {"
6679 /**
6680 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6681 */
6682 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6683 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6684 "},
6685 indoc! {"
6686 /**
6687 *ˇ Lorem ipsum dolor sit amet,
6688 * consectetur adipiscing elit.
6689 */
6690 /*
6691 *ˇ Lorem ipsum dolor sit amet,
6692 * consectetur adipiscing elit.
6693 */
6694 //ˇ Lorem ipsum dolor sit amet,
6695 // consectetur adipiscing elit.
6696 "},
6697 rust_lang.clone(),
6698 &mut cx,
6699 );
6700
6701 // selection w/ single short block comment
6702 assert_rewrap(
6703 indoc! {"
6704 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6705 "},
6706 indoc! {"
6707 «/*
6708 * Lorem ipsum dolor sit amet,
6709 * consectetur adipiscing elit.
6710 */ˇ»
6711 "},
6712 rust_lang.clone(),
6713 &mut cx,
6714 );
6715
6716 // rewrapping a single comment w/ abutting comments
6717 assert_rewrap(
6718 indoc! {"
6719 /* ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. */
6720 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6721 "},
6722 indoc! {"
6723 /*
6724 * ˇLorem ipsum dolor sit amet,
6725 * consectetur adipiscing elit.
6726 */
6727 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6728 "},
6729 rust_lang.clone(),
6730 &mut cx,
6731 );
6732
6733 // selection w/ non-abutting short block comments
6734 assert_rewrap(
6735 indoc! {"
6736 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6737
6738 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6739 "},
6740 indoc! {"
6741 «/*
6742 * Lorem ipsum dolor sit amet,
6743 * consectetur adipiscing elit.
6744 */
6745
6746 /*
6747 * Lorem ipsum dolor sit amet,
6748 * consectetur adipiscing elit.
6749 */ˇ»
6750 "},
6751 rust_lang.clone(),
6752 &mut cx,
6753 );
6754
6755 // selection of multiline block comments
6756 assert_rewrap(
6757 indoc! {"
6758 «/* Lorem ipsum dolor sit amet,
6759 * consectetur adipiscing elit. */ˇ»
6760 "},
6761 indoc! {"
6762 «/*
6763 * Lorem ipsum dolor sit amet,
6764 * consectetur adipiscing elit.
6765 */ˇ»
6766 "},
6767 rust_lang.clone(),
6768 &mut cx,
6769 );
6770
6771 // partial selection of multiline block comments
6772 assert_rewrap(
6773 indoc! {"
6774 «/* Lorem ipsum dolor sit amet,ˇ»
6775 * consectetur adipiscing elit. */
6776 /* Lorem ipsum dolor sit amet,
6777 «* consectetur adipiscing elit. */ˇ»
6778 "},
6779 indoc! {"
6780 «/*
6781 * Lorem ipsum dolor sit amet,ˇ»
6782 * consectetur adipiscing elit. */
6783 /* Lorem ipsum dolor sit amet,
6784 «* consectetur adipiscing elit.
6785 */ˇ»
6786 "},
6787 rust_lang.clone(),
6788 &mut cx,
6789 );
6790
6791 // selection w/ abutting short block comments
6792 // TODO: should not be combined; should rewrap as 2 comments
6793 assert_rewrap(
6794 indoc! {"
6795 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6796 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6797 "},
6798 // desired behavior:
6799 // indoc! {"
6800 // «/*
6801 // * Lorem ipsum dolor sit amet,
6802 // * consectetur adipiscing elit.
6803 // */
6804 // /*
6805 // * Lorem ipsum dolor sit amet,
6806 // * consectetur adipiscing elit.
6807 // */ˇ»
6808 // "},
6809 // actual behaviour:
6810 indoc! {"
6811 «/*
6812 * Lorem ipsum dolor sit amet,
6813 * consectetur adipiscing elit. Lorem
6814 * ipsum dolor sit amet, consectetur
6815 * adipiscing elit.
6816 */ˇ»
6817 "},
6818 rust_lang.clone(),
6819 &mut cx,
6820 );
6821
6822 // TODO: same as above, but with delimiters on separate line
6823 // assert_rewrap(
6824 // indoc! {"
6825 // «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6826 // */
6827 // /*
6828 // * Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6829 // "},
6830 // // desired:
6831 // // indoc! {"
6832 // // «/*
6833 // // * Lorem ipsum dolor sit amet,
6834 // // * consectetur adipiscing elit.
6835 // // */
6836 // // /*
6837 // // * Lorem ipsum dolor sit amet,
6838 // // * consectetur adipiscing elit.
6839 // // */ˇ»
6840 // // "},
6841 // // actual: (but with trailing w/s on the empty lines)
6842 // indoc! {"
6843 // «/*
6844 // * Lorem ipsum dolor sit amet,
6845 // * consectetur adipiscing elit.
6846 // *
6847 // */
6848 // /*
6849 // *
6850 // * Lorem ipsum dolor sit amet,
6851 // * consectetur adipiscing elit.
6852 // */ˇ»
6853 // "},
6854 // rust_lang.clone(),
6855 // &mut cx,
6856 // );
6857
6858 // TODO these are unhandled edge cases; not correct, just documenting known issues
6859 assert_rewrap(
6860 indoc! {"
6861 /*
6862 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6863 */
6864 /*
6865 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6866 /*ˇ Lorem ipsum dolor sit amet */ /* consectetur adipiscing elit. */
6867 "},
6868 // desired:
6869 // indoc! {"
6870 // /*
6871 // *ˇ Lorem ipsum dolor sit amet,
6872 // * consectetur adipiscing elit.
6873 // */
6874 // /*
6875 // *ˇ Lorem ipsum dolor sit amet,
6876 // * consectetur adipiscing elit.
6877 // */
6878 // /*
6879 // *ˇ Lorem ipsum dolor sit amet
6880 // */ /* consectetur adipiscing elit. */
6881 // "},
6882 // actual:
6883 indoc! {"
6884 /*
6885 //ˇ Lorem ipsum dolor sit amet,
6886 // consectetur adipiscing elit.
6887 */
6888 /*
6889 * //ˇ Lorem ipsum dolor sit amet,
6890 * consectetur adipiscing elit.
6891 */
6892 /*
6893 *ˇ Lorem ipsum dolor sit amet */ /*
6894 * consectetur adipiscing elit.
6895 */
6896 "},
6897 rust_lang,
6898 &mut cx,
6899 );
6900
6901 #[track_caller]
6902 fn assert_rewrap(
6903 unwrapped_text: &str,
6904 wrapped_text: &str,
6905 language: Arc<Language>,
6906 cx: &mut EditorTestContext,
6907 ) {
6908 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6909 cx.set_state(unwrapped_text);
6910 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6911 cx.assert_editor_state(wrapped_text);
6912 }
6913}
6914
6915#[gpui::test]
6916async fn test_hard_wrap(cx: &mut TestAppContext) {
6917 init_test(cx, |_| {});
6918 let mut cx = EditorTestContext::new(cx).await;
6919
6920 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
6921 cx.update_editor(|editor, _, cx| {
6922 editor.set_hard_wrap(Some(14), cx);
6923 });
6924
6925 cx.set_state(indoc!(
6926 "
6927 one two three ˇ
6928 "
6929 ));
6930 cx.simulate_input("four");
6931 cx.run_until_parked();
6932
6933 cx.assert_editor_state(indoc!(
6934 "
6935 one two three
6936 fourˇ
6937 "
6938 ));
6939
6940 cx.update_editor(|editor, window, cx| {
6941 editor.newline(&Default::default(), window, cx);
6942 });
6943 cx.run_until_parked();
6944 cx.assert_editor_state(indoc!(
6945 "
6946 one two three
6947 four
6948 ˇ
6949 "
6950 ));
6951
6952 cx.simulate_input("five");
6953 cx.run_until_parked();
6954 cx.assert_editor_state(indoc!(
6955 "
6956 one two three
6957 four
6958 fiveˇ
6959 "
6960 ));
6961
6962 cx.update_editor(|editor, window, cx| {
6963 editor.newline(&Default::default(), window, cx);
6964 });
6965 cx.run_until_parked();
6966 cx.simulate_input("# ");
6967 cx.run_until_parked();
6968 cx.assert_editor_state(indoc!(
6969 "
6970 one two three
6971 four
6972 five
6973 # ˇ
6974 "
6975 ));
6976
6977 cx.update_editor(|editor, window, cx| {
6978 editor.newline(&Default::default(), window, cx);
6979 });
6980 cx.run_until_parked();
6981 cx.assert_editor_state(indoc!(
6982 "
6983 one two three
6984 four
6985 five
6986 #\x20
6987 #ˇ
6988 "
6989 ));
6990
6991 cx.simulate_input(" 6");
6992 cx.run_until_parked();
6993 cx.assert_editor_state(indoc!(
6994 "
6995 one two three
6996 four
6997 five
6998 #
6999 # 6ˇ
7000 "
7001 ));
7002}
7003
7004#[gpui::test]
7005async fn test_cut_line_ends(cx: &mut TestAppContext) {
7006 init_test(cx, |_| {});
7007
7008 let mut cx = EditorTestContext::new(cx).await;
7009
7010 cx.set_state(indoc! {"The quick brownˇ"});
7011 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
7012 cx.assert_editor_state(indoc! {"The quick brownˇ"});
7013
7014 cx.set_state(indoc! {"The emacs foxˇ"});
7015 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
7016 cx.assert_editor_state(indoc! {"The emacs foxˇ"});
7017
7018 cx.set_state(indoc! {"
7019 The quick« brownˇ»
7020 fox jumps overˇ
7021 the lazy dog"});
7022 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7023 cx.assert_editor_state(indoc! {"
7024 The quickˇ
7025 ˇthe lazy dog"});
7026
7027 cx.set_state(indoc! {"
7028 The quick« brownˇ»
7029 fox jumps overˇ
7030 the lazy dog"});
7031 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
7032 cx.assert_editor_state(indoc! {"
7033 The quickˇ
7034 fox jumps overˇthe lazy dog"});
7035
7036 cx.set_state(indoc! {"
7037 The quick« brownˇ»
7038 fox jumps overˇ
7039 the lazy dog"});
7040 cx.update_editor(|e, window, cx| {
7041 e.cut_to_end_of_line(
7042 &CutToEndOfLine {
7043 stop_at_newlines: true,
7044 },
7045 window,
7046 cx,
7047 )
7048 });
7049 cx.assert_editor_state(indoc! {"
7050 The quickˇ
7051 fox jumps overˇ
7052 the lazy dog"});
7053
7054 cx.set_state(indoc! {"
7055 The quick« brownˇ»
7056 fox jumps overˇ
7057 the lazy dog"});
7058 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
7059 cx.assert_editor_state(indoc! {"
7060 The quickˇ
7061 fox jumps overˇthe lazy dog"});
7062}
7063
7064#[gpui::test]
7065async fn test_clipboard(cx: &mut TestAppContext) {
7066 init_test(cx, |_| {});
7067
7068 let mut cx = EditorTestContext::new(cx).await;
7069
7070 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
7071 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7072 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
7073
7074 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
7075 cx.set_state("two ˇfour ˇsix ˇ");
7076 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7077 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
7078
7079 // Paste again but with only two cursors. Since the number of cursors doesn't
7080 // match the number of slices in the clipboard, the entire clipboard text
7081 // is pasted at each cursor.
7082 cx.set_state("ˇtwo one✅ four three six five ˇ");
7083 cx.update_editor(|e, window, cx| {
7084 e.handle_input("( ", window, cx);
7085 e.paste(&Paste, window, cx);
7086 e.handle_input(") ", window, cx);
7087 });
7088 cx.assert_editor_state(
7089 &([
7090 "( one✅ ",
7091 "three ",
7092 "five ) ˇtwo one✅ four three six five ( one✅ ",
7093 "three ",
7094 "five ) ˇ",
7095 ]
7096 .join("\n")),
7097 );
7098
7099 // Cut with three selections, one of which is full-line.
7100 cx.set_state(indoc! {"
7101 1«2ˇ»3
7102 4ˇ567
7103 «8ˇ»9"});
7104 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7105 cx.assert_editor_state(indoc! {"
7106 1ˇ3
7107 ˇ9"});
7108
7109 // Paste with three selections, noticing how the copied selection that was full-line
7110 // gets inserted before the second cursor.
7111 cx.set_state(indoc! {"
7112 1ˇ3
7113 9ˇ
7114 «oˇ»ne"});
7115 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7116 cx.assert_editor_state(indoc! {"
7117 12ˇ3
7118 4567
7119 9ˇ
7120 8ˇne"});
7121
7122 // Copy with a single cursor only, which writes the whole line into the clipboard.
7123 cx.set_state(indoc! {"
7124 The quick brown
7125 fox juˇmps over
7126 the lazy dog"});
7127 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7128 assert_eq!(
7129 cx.read_from_clipboard()
7130 .and_then(|item| item.text().as_deref().map(str::to_string)),
7131 Some("fox jumps over\n".to_string())
7132 );
7133
7134 // Paste with three selections, noticing how the copied full-line selection is inserted
7135 // before the empty selections but replaces the selection that is non-empty.
7136 cx.set_state(indoc! {"
7137 Tˇhe quick brown
7138 «foˇ»x jumps over
7139 tˇhe lazy dog"});
7140 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7141 cx.assert_editor_state(indoc! {"
7142 fox jumps over
7143 Tˇhe quick brown
7144 fox jumps over
7145 ˇx jumps over
7146 fox jumps over
7147 tˇhe lazy dog"});
7148}
7149
7150#[gpui::test]
7151async fn test_copy_trim(cx: &mut TestAppContext) {
7152 init_test(cx, |_| {});
7153
7154 let mut cx = EditorTestContext::new(cx).await;
7155 cx.set_state(
7156 r#" «for selection in selections.iter() {
7157 let mut start = selection.start;
7158 let mut end = selection.end;
7159 let is_entire_line = selection.is_empty();
7160 if is_entire_line {
7161 start = Point::new(start.row, 0);ˇ»
7162 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7163 }
7164 "#,
7165 );
7166 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7167 assert_eq!(
7168 cx.read_from_clipboard()
7169 .and_then(|item| item.text().as_deref().map(str::to_string)),
7170 Some(
7171 "for selection in selections.iter() {
7172 let mut start = selection.start;
7173 let mut end = selection.end;
7174 let is_entire_line = selection.is_empty();
7175 if is_entire_line {
7176 start = Point::new(start.row, 0);"
7177 .to_string()
7178 ),
7179 "Regular copying preserves all indentation selected",
7180 );
7181 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7182 assert_eq!(
7183 cx.read_from_clipboard()
7184 .and_then(|item| item.text().as_deref().map(str::to_string)),
7185 Some(
7186 "for selection in selections.iter() {
7187let mut start = selection.start;
7188let mut end = selection.end;
7189let is_entire_line = selection.is_empty();
7190if is_entire_line {
7191 start = Point::new(start.row, 0);"
7192 .to_string()
7193 ),
7194 "Copying with stripping should strip all leading whitespaces"
7195 );
7196
7197 cx.set_state(
7198 r#" « for selection in selections.iter() {
7199 let mut start = selection.start;
7200 let mut end = selection.end;
7201 let is_entire_line = selection.is_empty();
7202 if is_entire_line {
7203 start = Point::new(start.row, 0);ˇ»
7204 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7205 }
7206 "#,
7207 );
7208 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7209 assert_eq!(
7210 cx.read_from_clipboard()
7211 .and_then(|item| item.text().as_deref().map(str::to_string)),
7212 Some(
7213 " for selection in selections.iter() {
7214 let mut start = selection.start;
7215 let mut end = selection.end;
7216 let is_entire_line = selection.is_empty();
7217 if is_entire_line {
7218 start = Point::new(start.row, 0);"
7219 .to_string()
7220 ),
7221 "Regular copying preserves all indentation selected",
7222 );
7223 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7224 assert_eq!(
7225 cx.read_from_clipboard()
7226 .and_then(|item| item.text().as_deref().map(str::to_string)),
7227 Some(
7228 "for selection in selections.iter() {
7229let mut start = selection.start;
7230let mut end = selection.end;
7231let is_entire_line = selection.is_empty();
7232if is_entire_line {
7233 start = Point::new(start.row, 0);"
7234 .to_string()
7235 ),
7236 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
7237 );
7238
7239 cx.set_state(
7240 r#" «ˇ for selection in selections.iter() {
7241 let mut start = selection.start;
7242 let mut end = selection.end;
7243 let is_entire_line = selection.is_empty();
7244 if is_entire_line {
7245 start = Point::new(start.row, 0);»
7246 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7247 }
7248 "#,
7249 );
7250 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7251 assert_eq!(
7252 cx.read_from_clipboard()
7253 .and_then(|item| item.text().as_deref().map(str::to_string)),
7254 Some(
7255 " for selection in selections.iter() {
7256 let mut start = selection.start;
7257 let mut end = selection.end;
7258 let is_entire_line = selection.is_empty();
7259 if is_entire_line {
7260 start = Point::new(start.row, 0);"
7261 .to_string()
7262 ),
7263 "Regular copying for reverse selection works the same",
7264 );
7265 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7266 assert_eq!(
7267 cx.read_from_clipboard()
7268 .and_then(|item| item.text().as_deref().map(str::to_string)),
7269 Some(
7270 "for selection in selections.iter() {
7271let mut start = selection.start;
7272let mut end = selection.end;
7273let is_entire_line = selection.is_empty();
7274if is_entire_line {
7275 start = Point::new(start.row, 0);"
7276 .to_string()
7277 ),
7278 "Copying with stripping for reverse selection works the same"
7279 );
7280
7281 cx.set_state(
7282 r#" for selection «in selections.iter() {
7283 let mut start = selection.start;
7284 let mut end = selection.end;
7285 let is_entire_line = selection.is_empty();
7286 if is_entire_line {
7287 start = Point::new(start.row, 0);ˇ»
7288 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7289 }
7290 "#,
7291 );
7292 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7293 assert_eq!(
7294 cx.read_from_clipboard()
7295 .and_then(|item| item.text().as_deref().map(str::to_string)),
7296 Some(
7297 "in selections.iter() {
7298 let mut start = selection.start;
7299 let mut end = selection.end;
7300 let is_entire_line = selection.is_empty();
7301 if is_entire_line {
7302 start = Point::new(start.row, 0);"
7303 .to_string()
7304 ),
7305 "When selecting past the indent, the copying works as usual",
7306 );
7307 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7308 assert_eq!(
7309 cx.read_from_clipboard()
7310 .and_then(|item| item.text().as_deref().map(str::to_string)),
7311 Some(
7312 "in selections.iter() {
7313 let mut start = selection.start;
7314 let mut end = selection.end;
7315 let is_entire_line = selection.is_empty();
7316 if is_entire_line {
7317 start = Point::new(start.row, 0);"
7318 .to_string()
7319 ),
7320 "When selecting past the indent, nothing is trimmed"
7321 );
7322
7323 cx.set_state(
7324 r#" «for selection in selections.iter() {
7325 let mut start = selection.start;
7326
7327 let mut end = selection.end;
7328 let is_entire_line = selection.is_empty();
7329 if is_entire_line {
7330 start = Point::new(start.row, 0);
7331ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
7332 }
7333 "#,
7334 );
7335 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7336 assert_eq!(
7337 cx.read_from_clipboard()
7338 .and_then(|item| item.text().as_deref().map(str::to_string)),
7339 Some(
7340 "for selection in selections.iter() {
7341let mut start = selection.start;
7342
7343let mut end = selection.end;
7344let is_entire_line = selection.is_empty();
7345if is_entire_line {
7346 start = Point::new(start.row, 0);
7347"
7348 .to_string()
7349 ),
7350 "Copying with stripping should ignore empty lines"
7351 );
7352}
7353
7354#[gpui::test]
7355async fn test_paste_multiline(cx: &mut TestAppContext) {
7356 init_test(cx, |_| {});
7357
7358 let mut cx = EditorTestContext::new(cx).await;
7359 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7360
7361 // Cut an indented block, without the leading whitespace.
7362 cx.set_state(indoc! {"
7363 const a: B = (
7364 c(),
7365 «d(
7366 e,
7367 f
7368 )ˇ»
7369 );
7370 "});
7371 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7372 cx.assert_editor_state(indoc! {"
7373 const a: B = (
7374 c(),
7375 ˇ
7376 );
7377 "});
7378
7379 // Paste it at the same position.
7380 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7381 cx.assert_editor_state(indoc! {"
7382 const a: B = (
7383 c(),
7384 d(
7385 e,
7386 f
7387 )ˇ
7388 );
7389 "});
7390
7391 // Paste it at a line with a lower indent level.
7392 cx.set_state(indoc! {"
7393 ˇ
7394 const a: B = (
7395 c(),
7396 );
7397 "});
7398 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7399 cx.assert_editor_state(indoc! {"
7400 d(
7401 e,
7402 f
7403 )ˇ
7404 const a: B = (
7405 c(),
7406 );
7407 "});
7408
7409 // Cut an indented block, with the leading whitespace.
7410 cx.set_state(indoc! {"
7411 const a: B = (
7412 c(),
7413 « d(
7414 e,
7415 f
7416 )
7417 ˇ»);
7418 "});
7419 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7420 cx.assert_editor_state(indoc! {"
7421 const a: B = (
7422 c(),
7423 ˇ);
7424 "});
7425
7426 // Paste it at the same position.
7427 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7428 cx.assert_editor_state(indoc! {"
7429 const a: B = (
7430 c(),
7431 d(
7432 e,
7433 f
7434 )
7435 ˇ);
7436 "});
7437
7438 // Paste it at a line with a higher indent level.
7439 cx.set_state(indoc! {"
7440 const a: B = (
7441 c(),
7442 d(
7443 e,
7444 fˇ
7445 )
7446 );
7447 "});
7448 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7449 cx.assert_editor_state(indoc! {"
7450 const a: B = (
7451 c(),
7452 d(
7453 e,
7454 f d(
7455 e,
7456 f
7457 )
7458 ˇ
7459 )
7460 );
7461 "});
7462
7463 // Copy an indented block, starting mid-line
7464 cx.set_state(indoc! {"
7465 const a: B = (
7466 c(),
7467 somethin«g(
7468 e,
7469 f
7470 )ˇ»
7471 );
7472 "});
7473 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7474
7475 // Paste it on a line with a lower indent level
7476 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
7477 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7478 cx.assert_editor_state(indoc! {"
7479 const a: B = (
7480 c(),
7481 something(
7482 e,
7483 f
7484 )
7485 );
7486 g(
7487 e,
7488 f
7489 )ˇ"});
7490}
7491
7492#[gpui::test]
7493async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
7494 init_test(cx, |_| {});
7495
7496 cx.write_to_clipboard(ClipboardItem::new_string(
7497 " d(\n e\n );\n".into(),
7498 ));
7499
7500 let mut cx = EditorTestContext::new(cx).await;
7501 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7502
7503 cx.set_state(indoc! {"
7504 fn a() {
7505 b();
7506 if c() {
7507 ˇ
7508 }
7509 }
7510 "});
7511
7512 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7513 cx.assert_editor_state(indoc! {"
7514 fn a() {
7515 b();
7516 if c() {
7517 d(
7518 e
7519 );
7520 ˇ
7521 }
7522 }
7523 "});
7524
7525 cx.set_state(indoc! {"
7526 fn a() {
7527 b();
7528 ˇ
7529 }
7530 "});
7531
7532 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7533 cx.assert_editor_state(indoc! {"
7534 fn a() {
7535 b();
7536 d(
7537 e
7538 );
7539 ˇ
7540 }
7541 "});
7542}
7543
7544#[gpui::test]
7545fn test_select_all(cx: &mut TestAppContext) {
7546 init_test(cx, |_| {});
7547
7548 let editor = cx.add_window(|window, cx| {
7549 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
7550 build_editor(buffer, window, cx)
7551 });
7552 _ = editor.update(cx, |editor, window, cx| {
7553 editor.select_all(&SelectAll, window, cx);
7554 assert_eq!(
7555 display_ranges(editor, cx),
7556 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
7557 );
7558 });
7559}
7560
7561#[gpui::test]
7562fn test_select_line(cx: &mut TestAppContext) {
7563 init_test(cx, |_| {});
7564
7565 let editor = cx.add_window(|window, cx| {
7566 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
7567 build_editor(buffer, window, cx)
7568 });
7569 _ = editor.update(cx, |editor, window, cx| {
7570 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7571 s.select_display_ranges([
7572 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7573 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7574 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7575 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
7576 ])
7577 });
7578 editor.select_line(&SelectLine, window, cx);
7579 assert_eq!(
7580 display_ranges(editor, cx),
7581 vec![
7582 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
7583 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
7584 ]
7585 );
7586 });
7587
7588 _ = editor.update(cx, |editor, window, cx| {
7589 editor.select_line(&SelectLine, window, cx);
7590 assert_eq!(
7591 display_ranges(editor, cx),
7592 vec![
7593 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
7594 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
7595 ]
7596 );
7597 });
7598
7599 _ = editor.update(cx, |editor, window, cx| {
7600 editor.select_line(&SelectLine, window, cx);
7601 assert_eq!(
7602 display_ranges(editor, cx),
7603 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
7604 );
7605 });
7606}
7607
7608#[gpui::test]
7609async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
7610 init_test(cx, |_| {});
7611 let mut cx = EditorTestContext::new(cx).await;
7612
7613 #[track_caller]
7614 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
7615 cx.set_state(initial_state);
7616 cx.update_editor(|e, window, cx| {
7617 e.split_selection_into_lines(&Default::default(), window, cx)
7618 });
7619 cx.assert_editor_state(expected_state);
7620 }
7621
7622 // Selection starts and ends at the middle of lines, left-to-right
7623 test(
7624 &mut cx,
7625 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
7626 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7627 );
7628 // Same thing, right-to-left
7629 test(
7630 &mut cx,
7631 "aa\nb«b\ncc\ndd\neˇ»e\nff",
7632 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7633 );
7634
7635 // Whole buffer, left-to-right, last line *doesn't* end with newline
7636 test(
7637 &mut cx,
7638 "«ˇaa\nbb\ncc\ndd\nee\nff»",
7639 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7640 );
7641 // Same thing, right-to-left
7642 test(
7643 &mut cx,
7644 "«aa\nbb\ncc\ndd\nee\nffˇ»",
7645 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7646 );
7647
7648 // Whole buffer, left-to-right, last line ends with newline
7649 test(
7650 &mut cx,
7651 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
7652 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7653 );
7654 // Same thing, right-to-left
7655 test(
7656 &mut cx,
7657 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
7658 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7659 );
7660
7661 // Starts at the end of a line, ends at the start of another
7662 test(
7663 &mut cx,
7664 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
7665 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
7666 );
7667}
7668
7669#[gpui::test]
7670async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
7671 init_test(cx, |_| {});
7672
7673 let editor = cx.add_window(|window, cx| {
7674 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
7675 build_editor(buffer, window, cx)
7676 });
7677
7678 // setup
7679 _ = editor.update(cx, |editor, window, cx| {
7680 editor.fold_creases(
7681 vec![
7682 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
7683 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
7684 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
7685 ],
7686 true,
7687 window,
7688 cx,
7689 );
7690 assert_eq!(
7691 editor.display_text(cx),
7692 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7693 );
7694 });
7695
7696 _ = editor.update(cx, |editor, window, cx| {
7697 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7698 s.select_display_ranges([
7699 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7700 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7701 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7702 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
7703 ])
7704 });
7705 editor.split_selection_into_lines(&Default::default(), window, cx);
7706 assert_eq!(
7707 editor.display_text(cx),
7708 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7709 );
7710 });
7711 EditorTestContext::for_editor(editor, cx)
7712 .await
7713 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
7714
7715 _ = editor.update(cx, |editor, window, cx| {
7716 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7717 s.select_display_ranges([
7718 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
7719 ])
7720 });
7721 editor.split_selection_into_lines(&Default::default(), window, cx);
7722 assert_eq!(
7723 editor.display_text(cx),
7724 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
7725 );
7726 assert_eq!(
7727 display_ranges(editor, cx),
7728 [
7729 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
7730 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
7731 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
7732 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
7733 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
7734 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
7735 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
7736 ]
7737 );
7738 });
7739 EditorTestContext::for_editor(editor, cx)
7740 .await
7741 .assert_editor_state(
7742 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
7743 );
7744}
7745
7746#[gpui::test]
7747async fn test_add_selection_above_below(cx: &mut TestAppContext) {
7748 init_test(cx, |_| {});
7749
7750 let mut cx = EditorTestContext::new(cx).await;
7751
7752 cx.set_state(indoc!(
7753 r#"abc
7754 defˇghi
7755
7756 jk
7757 nlmo
7758 "#
7759 ));
7760
7761 cx.update_editor(|editor, window, cx| {
7762 editor.add_selection_above(&Default::default(), window, cx);
7763 });
7764
7765 cx.assert_editor_state(indoc!(
7766 r#"abcˇ
7767 defˇghi
7768
7769 jk
7770 nlmo
7771 "#
7772 ));
7773
7774 cx.update_editor(|editor, window, cx| {
7775 editor.add_selection_above(&Default::default(), window, cx);
7776 });
7777
7778 cx.assert_editor_state(indoc!(
7779 r#"abcˇ
7780 defˇghi
7781
7782 jk
7783 nlmo
7784 "#
7785 ));
7786
7787 cx.update_editor(|editor, window, cx| {
7788 editor.add_selection_below(&Default::default(), window, cx);
7789 });
7790
7791 cx.assert_editor_state(indoc!(
7792 r#"abc
7793 defˇghi
7794
7795 jk
7796 nlmo
7797 "#
7798 ));
7799
7800 cx.update_editor(|editor, window, cx| {
7801 editor.undo_selection(&Default::default(), window, cx);
7802 });
7803
7804 cx.assert_editor_state(indoc!(
7805 r#"abcˇ
7806 defˇghi
7807
7808 jk
7809 nlmo
7810 "#
7811 ));
7812
7813 cx.update_editor(|editor, window, cx| {
7814 editor.redo_selection(&Default::default(), window, cx);
7815 });
7816
7817 cx.assert_editor_state(indoc!(
7818 r#"abc
7819 defˇghi
7820
7821 jk
7822 nlmo
7823 "#
7824 ));
7825
7826 cx.update_editor(|editor, window, cx| {
7827 editor.add_selection_below(&Default::default(), window, cx);
7828 });
7829
7830 cx.assert_editor_state(indoc!(
7831 r#"abc
7832 defˇghi
7833 ˇ
7834 jk
7835 nlmo
7836 "#
7837 ));
7838
7839 cx.update_editor(|editor, window, cx| {
7840 editor.add_selection_below(&Default::default(), window, cx);
7841 });
7842
7843 cx.assert_editor_state(indoc!(
7844 r#"abc
7845 defˇghi
7846 ˇ
7847 jkˇ
7848 nlmo
7849 "#
7850 ));
7851
7852 cx.update_editor(|editor, window, cx| {
7853 editor.add_selection_below(&Default::default(), window, cx);
7854 });
7855
7856 cx.assert_editor_state(indoc!(
7857 r#"abc
7858 defˇghi
7859 ˇ
7860 jkˇ
7861 nlmˇo
7862 "#
7863 ));
7864
7865 cx.update_editor(|editor, window, cx| {
7866 editor.add_selection_below(&Default::default(), window, cx);
7867 });
7868
7869 cx.assert_editor_state(indoc!(
7870 r#"abc
7871 defˇghi
7872 ˇ
7873 jkˇ
7874 nlmˇo
7875 ˇ"#
7876 ));
7877
7878 // change selections
7879 cx.set_state(indoc!(
7880 r#"abc
7881 def«ˇg»hi
7882
7883 jk
7884 nlmo
7885 "#
7886 ));
7887
7888 cx.update_editor(|editor, window, cx| {
7889 editor.add_selection_below(&Default::default(), window, cx);
7890 });
7891
7892 cx.assert_editor_state(indoc!(
7893 r#"abc
7894 def«ˇg»hi
7895
7896 jk
7897 nlm«ˇo»
7898 "#
7899 ));
7900
7901 cx.update_editor(|editor, window, cx| {
7902 editor.add_selection_below(&Default::default(), window, cx);
7903 });
7904
7905 cx.assert_editor_state(indoc!(
7906 r#"abc
7907 def«ˇg»hi
7908
7909 jk
7910 nlm«ˇo»
7911 "#
7912 ));
7913
7914 cx.update_editor(|editor, window, cx| {
7915 editor.add_selection_above(&Default::default(), window, cx);
7916 });
7917
7918 cx.assert_editor_state(indoc!(
7919 r#"abc
7920 def«ˇg»hi
7921
7922 jk
7923 nlmo
7924 "#
7925 ));
7926
7927 cx.update_editor(|editor, window, cx| {
7928 editor.add_selection_above(&Default::default(), window, cx);
7929 });
7930
7931 cx.assert_editor_state(indoc!(
7932 r#"abc
7933 def«ˇg»hi
7934
7935 jk
7936 nlmo
7937 "#
7938 ));
7939
7940 // Change selections again
7941 cx.set_state(indoc!(
7942 r#"a«bc
7943 defgˇ»hi
7944
7945 jk
7946 nlmo
7947 "#
7948 ));
7949
7950 cx.update_editor(|editor, window, cx| {
7951 editor.add_selection_below(&Default::default(), window, cx);
7952 });
7953
7954 cx.assert_editor_state(indoc!(
7955 r#"a«bcˇ»
7956 d«efgˇ»hi
7957
7958 j«kˇ»
7959 nlmo
7960 "#
7961 ));
7962
7963 cx.update_editor(|editor, window, cx| {
7964 editor.add_selection_below(&Default::default(), window, cx);
7965 });
7966 cx.assert_editor_state(indoc!(
7967 r#"a«bcˇ»
7968 d«efgˇ»hi
7969
7970 j«kˇ»
7971 n«lmoˇ»
7972 "#
7973 ));
7974 cx.update_editor(|editor, window, cx| {
7975 editor.add_selection_above(&Default::default(), window, cx);
7976 });
7977
7978 cx.assert_editor_state(indoc!(
7979 r#"a«bcˇ»
7980 d«efgˇ»hi
7981
7982 j«kˇ»
7983 nlmo
7984 "#
7985 ));
7986
7987 // Change selections again
7988 cx.set_state(indoc!(
7989 r#"abc
7990 d«ˇefghi
7991
7992 jk
7993 nlm»o
7994 "#
7995 ));
7996
7997 cx.update_editor(|editor, window, cx| {
7998 editor.add_selection_above(&Default::default(), window, cx);
7999 });
8000
8001 cx.assert_editor_state(indoc!(
8002 r#"a«ˇbc»
8003 d«ˇef»ghi
8004
8005 j«ˇk»
8006 n«ˇlm»o
8007 "#
8008 ));
8009
8010 cx.update_editor(|editor, window, cx| {
8011 editor.add_selection_below(&Default::default(), window, cx);
8012 });
8013
8014 cx.assert_editor_state(indoc!(
8015 r#"abc
8016 d«ˇef»ghi
8017
8018 j«ˇk»
8019 n«ˇlm»o
8020 "#
8021 ));
8022}
8023
8024#[gpui::test]
8025async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
8026 init_test(cx, |_| {});
8027 let mut cx = EditorTestContext::new(cx).await;
8028
8029 cx.set_state(indoc!(
8030 r#"line onˇe
8031 liˇne two
8032 line three
8033 line four"#
8034 ));
8035
8036 cx.update_editor(|editor, window, cx| {
8037 editor.add_selection_below(&Default::default(), window, cx);
8038 });
8039
8040 // test multiple cursors expand in the same direction
8041 cx.assert_editor_state(indoc!(
8042 r#"line onˇe
8043 liˇne twˇo
8044 liˇne three
8045 line four"#
8046 ));
8047
8048 cx.update_editor(|editor, window, cx| {
8049 editor.add_selection_below(&Default::default(), window, cx);
8050 });
8051
8052 cx.update_editor(|editor, window, cx| {
8053 editor.add_selection_below(&Default::default(), window, cx);
8054 });
8055
8056 // test multiple cursors expand below overflow
8057 cx.assert_editor_state(indoc!(
8058 r#"line onˇe
8059 liˇne twˇo
8060 liˇne thˇree
8061 liˇne foˇur"#
8062 ));
8063
8064 cx.update_editor(|editor, window, cx| {
8065 editor.add_selection_above(&Default::default(), window, cx);
8066 });
8067
8068 // test multiple cursors retrieves back correctly
8069 cx.assert_editor_state(indoc!(
8070 r#"line onˇe
8071 liˇne twˇo
8072 liˇne thˇree
8073 line four"#
8074 ));
8075
8076 cx.update_editor(|editor, window, cx| {
8077 editor.add_selection_above(&Default::default(), window, cx);
8078 });
8079
8080 cx.update_editor(|editor, window, cx| {
8081 editor.add_selection_above(&Default::default(), window, cx);
8082 });
8083
8084 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
8085 cx.assert_editor_state(indoc!(
8086 r#"liˇne onˇe
8087 liˇne two
8088 line three
8089 line four"#
8090 ));
8091
8092 cx.update_editor(|editor, window, cx| {
8093 editor.undo_selection(&Default::default(), window, cx);
8094 });
8095
8096 // test undo
8097 cx.assert_editor_state(indoc!(
8098 r#"line onˇe
8099 liˇne twˇo
8100 line three
8101 line four"#
8102 ));
8103
8104 cx.update_editor(|editor, window, cx| {
8105 editor.redo_selection(&Default::default(), window, cx);
8106 });
8107
8108 // test redo
8109 cx.assert_editor_state(indoc!(
8110 r#"liˇne onˇe
8111 liˇne two
8112 line three
8113 line four"#
8114 ));
8115
8116 cx.set_state(indoc!(
8117 r#"abcd
8118 ef«ghˇ»
8119 ijkl
8120 «mˇ»nop"#
8121 ));
8122
8123 cx.update_editor(|editor, window, cx| {
8124 editor.add_selection_above(&Default::default(), window, cx);
8125 });
8126
8127 // test multiple selections expand in the same direction
8128 cx.assert_editor_state(indoc!(
8129 r#"ab«cdˇ»
8130 ef«ghˇ»
8131 «iˇ»jkl
8132 «mˇ»nop"#
8133 ));
8134
8135 cx.update_editor(|editor, window, cx| {
8136 editor.add_selection_above(&Default::default(), window, cx);
8137 });
8138
8139 // test multiple selection upward overflow
8140 cx.assert_editor_state(indoc!(
8141 r#"ab«cdˇ»
8142 «eˇ»f«ghˇ»
8143 «iˇ»jkl
8144 «mˇ»nop"#
8145 ));
8146
8147 cx.update_editor(|editor, window, cx| {
8148 editor.add_selection_below(&Default::default(), window, cx);
8149 });
8150
8151 // test multiple selection retrieves back correctly
8152 cx.assert_editor_state(indoc!(
8153 r#"abcd
8154 ef«ghˇ»
8155 «iˇ»jkl
8156 «mˇ»nop"#
8157 ));
8158
8159 cx.update_editor(|editor, window, cx| {
8160 editor.add_selection_below(&Default::default(), window, cx);
8161 });
8162
8163 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
8164 cx.assert_editor_state(indoc!(
8165 r#"abcd
8166 ef«ghˇ»
8167 ij«klˇ»
8168 «mˇ»nop"#
8169 ));
8170
8171 cx.update_editor(|editor, window, cx| {
8172 editor.undo_selection(&Default::default(), window, cx);
8173 });
8174
8175 // test undo
8176 cx.assert_editor_state(indoc!(
8177 r#"abcd
8178 ef«ghˇ»
8179 «iˇ»jkl
8180 «mˇ»nop"#
8181 ));
8182
8183 cx.update_editor(|editor, window, cx| {
8184 editor.redo_selection(&Default::default(), window, cx);
8185 });
8186
8187 // test redo
8188 cx.assert_editor_state(indoc!(
8189 r#"abcd
8190 ef«ghˇ»
8191 ij«klˇ»
8192 «mˇ»nop"#
8193 ));
8194}
8195
8196#[gpui::test]
8197async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
8198 init_test(cx, |_| {});
8199 let mut cx = EditorTestContext::new(cx).await;
8200
8201 cx.set_state(indoc!(
8202 r#"line onˇe
8203 liˇne two
8204 line three
8205 line four"#
8206 ));
8207
8208 cx.update_editor(|editor, window, cx| {
8209 editor.add_selection_below(&Default::default(), window, cx);
8210 editor.add_selection_below(&Default::default(), window, cx);
8211 editor.add_selection_below(&Default::default(), window, cx);
8212 });
8213
8214 // initial state with two multi cursor groups
8215 cx.assert_editor_state(indoc!(
8216 r#"line onˇe
8217 liˇne twˇo
8218 liˇne thˇree
8219 liˇne foˇur"#
8220 ));
8221
8222 // add single cursor in middle - simulate opt click
8223 cx.update_editor(|editor, window, cx| {
8224 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
8225 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8226 editor.end_selection(window, cx);
8227 });
8228
8229 cx.assert_editor_state(indoc!(
8230 r#"line onˇe
8231 liˇne twˇo
8232 liˇneˇ thˇree
8233 liˇne foˇur"#
8234 ));
8235
8236 cx.update_editor(|editor, window, cx| {
8237 editor.add_selection_above(&Default::default(), window, cx);
8238 });
8239
8240 // test new added selection expands above and existing selection shrinks
8241 cx.assert_editor_state(indoc!(
8242 r#"line onˇe
8243 liˇneˇ twˇo
8244 liˇneˇ thˇree
8245 line four"#
8246 ));
8247
8248 cx.update_editor(|editor, window, cx| {
8249 editor.add_selection_above(&Default::default(), window, cx);
8250 });
8251
8252 // test new added selection expands above and existing selection shrinks
8253 cx.assert_editor_state(indoc!(
8254 r#"lineˇ onˇe
8255 liˇneˇ twˇo
8256 lineˇ three
8257 line four"#
8258 ));
8259
8260 // intial state with two selection groups
8261 cx.set_state(indoc!(
8262 r#"abcd
8263 ef«ghˇ»
8264 ijkl
8265 «mˇ»nop"#
8266 ));
8267
8268 cx.update_editor(|editor, window, cx| {
8269 editor.add_selection_above(&Default::default(), window, cx);
8270 editor.add_selection_above(&Default::default(), window, cx);
8271 });
8272
8273 cx.assert_editor_state(indoc!(
8274 r#"ab«cdˇ»
8275 «eˇ»f«ghˇ»
8276 «iˇ»jkl
8277 «mˇ»nop"#
8278 ));
8279
8280 // add single selection in middle - simulate opt drag
8281 cx.update_editor(|editor, window, cx| {
8282 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
8283 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8284 editor.update_selection(
8285 DisplayPoint::new(DisplayRow(2), 4),
8286 0,
8287 gpui::Point::<f32>::default(),
8288 window,
8289 cx,
8290 );
8291 editor.end_selection(window, cx);
8292 });
8293
8294 cx.assert_editor_state(indoc!(
8295 r#"ab«cdˇ»
8296 «eˇ»f«ghˇ»
8297 «iˇ»jk«lˇ»
8298 «mˇ»nop"#
8299 ));
8300
8301 cx.update_editor(|editor, window, cx| {
8302 editor.add_selection_below(&Default::default(), window, cx);
8303 });
8304
8305 // test new added selection expands below, others shrinks from above
8306 cx.assert_editor_state(indoc!(
8307 r#"abcd
8308 ef«ghˇ»
8309 «iˇ»jk«lˇ»
8310 «mˇ»no«pˇ»"#
8311 ));
8312}
8313
8314#[gpui::test]
8315async fn test_select_next(cx: &mut TestAppContext) {
8316 init_test(cx, |_| {});
8317
8318 let mut cx = EditorTestContext::new(cx).await;
8319 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8320
8321 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8322 .unwrap();
8323 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8324
8325 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8326 .unwrap();
8327 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8328
8329 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8330 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8331
8332 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8333 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8334
8335 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8336 .unwrap();
8337 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8338
8339 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8340 .unwrap();
8341 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8342
8343 // Test selection direction should be preserved
8344 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8345
8346 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8347 .unwrap();
8348 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
8349}
8350
8351#[gpui::test]
8352async fn test_select_all_matches(cx: &mut TestAppContext) {
8353 init_test(cx, |_| {});
8354
8355 let mut cx = EditorTestContext::new(cx).await;
8356
8357 // Test caret-only selections
8358 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8359 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8360 .unwrap();
8361 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8362
8363 // Test left-to-right selections
8364 cx.set_state("abc\n«abcˇ»\nabc");
8365 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8366 .unwrap();
8367 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
8368
8369 // Test right-to-left selections
8370 cx.set_state("abc\n«ˇabc»\nabc");
8371 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8372 .unwrap();
8373 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
8374
8375 // Test selecting whitespace with caret selection
8376 cx.set_state("abc\nˇ abc\nabc");
8377 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8378 .unwrap();
8379 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8380
8381 // Test selecting whitespace with left-to-right selection
8382 cx.set_state("abc\n«ˇ »abc\nabc");
8383 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8384 .unwrap();
8385 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
8386
8387 // Test no matches with right-to-left selection
8388 cx.set_state("abc\n« ˇ»abc\nabc");
8389 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8390 .unwrap();
8391 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8392
8393 // Test with a single word and clip_at_line_ends=true (#29823)
8394 cx.set_state("aˇbc");
8395 cx.update_editor(|e, window, cx| {
8396 e.set_clip_at_line_ends(true, cx);
8397 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8398 e.set_clip_at_line_ends(false, cx);
8399 });
8400 cx.assert_editor_state("«abcˇ»");
8401}
8402
8403#[gpui::test]
8404async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
8405 init_test(cx, |_| {});
8406
8407 let mut cx = EditorTestContext::new(cx).await;
8408
8409 let large_body_1 = "\nd".repeat(200);
8410 let large_body_2 = "\ne".repeat(200);
8411
8412 cx.set_state(&format!(
8413 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
8414 ));
8415 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
8416 let scroll_position = editor.scroll_position(cx);
8417 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
8418 scroll_position
8419 });
8420
8421 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8422 .unwrap();
8423 cx.assert_editor_state(&format!(
8424 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
8425 ));
8426 let scroll_position_after_selection =
8427 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
8428 assert_eq!(
8429 initial_scroll_position, scroll_position_after_selection,
8430 "Scroll position should not change after selecting all matches"
8431 );
8432}
8433
8434#[gpui::test]
8435async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
8436 init_test(cx, |_| {});
8437
8438 let mut cx = EditorLspTestContext::new_rust(
8439 lsp::ServerCapabilities {
8440 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8441 ..Default::default()
8442 },
8443 cx,
8444 )
8445 .await;
8446
8447 cx.set_state(indoc! {"
8448 line 1
8449 line 2
8450 linˇe 3
8451 line 4
8452 line 5
8453 "});
8454
8455 // Make an edit
8456 cx.update_editor(|editor, window, cx| {
8457 editor.handle_input("X", window, cx);
8458 });
8459
8460 // Move cursor to a different position
8461 cx.update_editor(|editor, window, cx| {
8462 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8463 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
8464 });
8465 });
8466
8467 cx.assert_editor_state(indoc! {"
8468 line 1
8469 line 2
8470 linXe 3
8471 line 4
8472 liˇne 5
8473 "});
8474
8475 cx.lsp
8476 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8477 Ok(Some(vec![lsp::TextEdit::new(
8478 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8479 "PREFIX ".to_string(),
8480 )]))
8481 });
8482
8483 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
8484 .unwrap()
8485 .await
8486 .unwrap();
8487
8488 cx.assert_editor_state(indoc! {"
8489 PREFIX line 1
8490 line 2
8491 linXe 3
8492 line 4
8493 liˇne 5
8494 "});
8495
8496 // Undo formatting
8497 cx.update_editor(|editor, window, cx| {
8498 editor.undo(&Default::default(), window, cx);
8499 });
8500
8501 // Verify cursor moved back to position after edit
8502 cx.assert_editor_state(indoc! {"
8503 line 1
8504 line 2
8505 linXˇe 3
8506 line 4
8507 line 5
8508 "});
8509}
8510
8511#[gpui::test]
8512async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) {
8513 init_test(cx, |_| {});
8514
8515 let mut cx = EditorTestContext::new(cx).await;
8516
8517 let provider = cx.new(|_| FakeEditPredictionProvider::default());
8518 cx.update_editor(|editor, window, cx| {
8519 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
8520 });
8521
8522 cx.set_state(indoc! {"
8523 line 1
8524 line 2
8525 linˇe 3
8526 line 4
8527 line 5
8528 line 6
8529 line 7
8530 line 8
8531 line 9
8532 line 10
8533 "});
8534
8535 let snapshot = cx.buffer_snapshot();
8536 let edit_position = snapshot.anchor_after(Point::new(2, 4));
8537
8538 cx.update(|_, cx| {
8539 provider.update(cx, |provider, _| {
8540 provider.set_edit_prediction(Some(edit_prediction::EditPrediction::Local {
8541 id: None,
8542 edits: vec![(edit_position..edit_position, "X".into())],
8543 edit_preview: None,
8544 }))
8545 })
8546 });
8547
8548 cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
8549 cx.update_editor(|editor, window, cx| {
8550 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
8551 });
8552
8553 cx.assert_editor_state(indoc! {"
8554 line 1
8555 line 2
8556 lineXˇ 3
8557 line 4
8558 line 5
8559 line 6
8560 line 7
8561 line 8
8562 line 9
8563 line 10
8564 "});
8565
8566 cx.update_editor(|editor, window, cx| {
8567 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8568 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
8569 });
8570 });
8571
8572 cx.assert_editor_state(indoc! {"
8573 line 1
8574 line 2
8575 lineX 3
8576 line 4
8577 line 5
8578 line 6
8579 line 7
8580 line 8
8581 line 9
8582 liˇne 10
8583 "});
8584
8585 cx.update_editor(|editor, window, cx| {
8586 editor.undo(&Default::default(), window, cx);
8587 });
8588
8589 cx.assert_editor_state(indoc! {"
8590 line 1
8591 line 2
8592 lineˇ 3
8593 line 4
8594 line 5
8595 line 6
8596 line 7
8597 line 8
8598 line 9
8599 line 10
8600 "});
8601}
8602
8603#[gpui::test]
8604async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
8605 init_test(cx, |_| {});
8606
8607 let mut cx = EditorTestContext::new(cx).await;
8608 cx.set_state(
8609 r#"let foo = 2;
8610lˇet foo = 2;
8611let fooˇ = 2;
8612let foo = 2;
8613let foo = ˇ2;"#,
8614 );
8615
8616 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8617 .unwrap();
8618 cx.assert_editor_state(
8619 r#"let foo = 2;
8620«letˇ» foo = 2;
8621let «fooˇ» = 2;
8622let foo = 2;
8623let foo = «2ˇ»;"#,
8624 );
8625
8626 // noop for multiple selections with different contents
8627 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8628 .unwrap();
8629 cx.assert_editor_state(
8630 r#"let foo = 2;
8631«letˇ» foo = 2;
8632let «fooˇ» = 2;
8633let foo = 2;
8634let foo = «2ˇ»;"#,
8635 );
8636
8637 // Test last selection direction should be preserved
8638 cx.set_state(
8639 r#"let foo = 2;
8640let foo = 2;
8641let «fooˇ» = 2;
8642let «ˇfoo» = 2;
8643let foo = 2;"#,
8644 );
8645
8646 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8647 .unwrap();
8648 cx.assert_editor_state(
8649 r#"let foo = 2;
8650let foo = 2;
8651let «fooˇ» = 2;
8652let «ˇfoo» = 2;
8653let «ˇfoo» = 2;"#,
8654 );
8655}
8656
8657#[gpui::test]
8658async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
8659 init_test(cx, |_| {});
8660
8661 let mut cx =
8662 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
8663
8664 cx.assert_editor_state(indoc! {"
8665 ˇbbb
8666 ccc
8667
8668 bbb
8669 ccc
8670 "});
8671 cx.dispatch_action(SelectPrevious::default());
8672 cx.assert_editor_state(indoc! {"
8673 «bbbˇ»
8674 ccc
8675
8676 bbb
8677 ccc
8678 "});
8679 cx.dispatch_action(SelectPrevious::default());
8680 cx.assert_editor_state(indoc! {"
8681 «bbbˇ»
8682 ccc
8683
8684 «bbbˇ»
8685 ccc
8686 "});
8687}
8688
8689#[gpui::test]
8690async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
8691 init_test(cx, |_| {});
8692
8693 let mut cx = EditorTestContext::new(cx).await;
8694 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8695
8696 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8697 .unwrap();
8698 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8699
8700 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8701 .unwrap();
8702 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8703
8704 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8705 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8706
8707 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8708 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8709
8710 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8711 .unwrap();
8712 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
8713
8714 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8715 .unwrap();
8716 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8717}
8718
8719#[gpui::test]
8720async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
8721 init_test(cx, |_| {});
8722
8723 let mut cx = EditorTestContext::new(cx).await;
8724 cx.set_state("aˇ");
8725
8726 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8727 .unwrap();
8728 cx.assert_editor_state("«aˇ»");
8729 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8730 .unwrap();
8731 cx.assert_editor_state("«aˇ»");
8732}
8733
8734#[gpui::test]
8735async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
8736 init_test(cx, |_| {});
8737
8738 let mut cx = EditorTestContext::new(cx).await;
8739 cx.set_state(
8740 r#"let foo = 2;
8741lˇet foo = 2;
8742let fooˇ = 2;
8743let foo = 2;
8744let foo = ˇ2;"#,
8745 );
8746
8747 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8748 .unwrap();
8749 cx.assert_editor_state(
8750 r#"let foo = 2;
8751«letˇ» foo = 2;
8752let «fooˇ» = 2;
8753let foo = 2;
8754let foo = «2ˇ»;"#,
8755 );
8756
8757 // noop for multiple selections with different contents
8758 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8759 .unwrap();
8760 cx.assert_editor_state(
8761 r#"let foo = 2;
8762«letˇ» foo = 2;
8763let «fooˇ» = 2;
8764let foo = 2;
8765let foo = «2ˇ»;"#,
8766 );
8767}
8768
8769#[gpui::test]
8770async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
8771 init_test(cx, |_| {});
8772
8773 let mut cx = EditorTestContext::new(cx).await;
8774 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8775
8776 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8777 .unwrap();
8778 // selection direction is preserved
8779 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8780
8781 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8782 .unwrap();
8783 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8784
8785 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8786 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8787
8788 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8789 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8790
8791 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8792 .unwrap();
8793 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
8794
8795 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8796 .unwrap();
8797 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
8798}
8799
8800#[gpui::test]
8801async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
8802 init_test(cx, |_| {});
8803
8804 let language = Arc::new(Language::new(
8805 LanguageConfig::default(),
8806 Some(tree_sitter_rust::LANGUAGE.into()),
8807 ));
8808
8809 let text = r#"
8810 use mod1::mod2::{mod3, mod4};
8811
8812 fn fn_1(param1: bool, param2: &str) {
8813 let var1 = "text";
8814 }
8815 "#
8816 .unindent();
8817
8818 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8819 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8820 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8821
8822 editor
8823 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8824 .await;
8825
8826 editor.update_in(cx, |editor, window, cx| {
8827 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8828 s.select_display_ranges([
8829 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
8830 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
8831 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
8832 ]);
8833 });
8834 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8835 });
8836 editor.update(cx, |editor, cx| {
8837 assert_text_with_selections(
8838 editor,
8839 indoc! {r#"
8840 use mod1::mod2::{mod3, «mod4ˇ»};
8841
8842 fn fn_1«ˇ(param1: bool, param2: &str)» {
8843 let var1 = "«ˇtext»";
8844 }
8845 "#},
8846 cx,
8847 );
8848 });
8849
8850 editor.update_in(cx, |editor, window, cx| {
8851 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8852 });
8853 editor.update(cx, |editor, cx| {
8854 assert_text_with_selections(
8855 editor,
8856 indoc! {r#"
8857 use mod1::mod2::«{mod3, mod4}ˇ»;
8858
8859 «ˇfn fn_1(param1: bool, param2: &str) {
8860 let var1 = "text";
8861 }»
8862 "#},
8863 cx,
8864 );
8865 });
8866
8867 editor.update_in(cx, |editor, window, cx| {
8868 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8869 });
8870 assert_eq!(
8871 editor.update(cx, |editor, cx| editor
8872 .selections
8873 .display_ranges(&editor.display_snapshot(cx))),
8874 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8875 );
8876
8877 // Trying to expand the selected syntax node one more time has no effect.
8878 editor.update_in(cx, |editor, window, cx| {
8879 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8880 });
8881 assert_eq!(
8882 editor.update(cx, |editor, cx| editor
8883 .selections
8884 .display_ranges(&editor.display_snapshot(cx))),
8885 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8886 );
8887
8888 editor.update_in(cx, |editor, window, cx| {
8889 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8890 });
8891 editor.update(cx, |editor, cx| {
8892 assert_text_with_selections(
8893 editor,
8894 indoc! {r#"
8895 use mod1::mod2::«{mod3, mod4}ˇ»;
8896
8897 «ˇfn fn_1(param1: bool, param2: &str) {
8898 let var1 = "text";
8899 }»
8900 "#},
8901 cx,
8902 );
8903 });
8904
8905 editor.update_in(cx, |editor, window, cx| {
8906 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8907 });
8908 editor.update(cx, |editor, cx| {
8909 assert_text_with_selections(
8910 editor,
8911 indoc! {r#"
8912 use mod1::mod2::{mod3, «mod4ˇ»};
8913
8914 fn fn_1«ˇ(param1: bool, param2: &str)» {
8915 let var1 = "«ˇtext»";
8916 }
8917 "#},
8918 cx,
8919 );
8920 });
8921
8922 editor.update_in(cx, |editor, window, cx| {
8923 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8924 });
8925 editor.update(cx, |editor, cx| {
8926 assert_text_with_selections(
8927 editor,
8928 indoc! {r#"
8929 use mod1::mod2::{mod3, moˇd4};
8930
8931 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
8932 let var1 = "teˇxt";
8933 }
8934 "#},
8935 cx,
8936 );
8937 });
8938
8939 // Trying to shrink the selected syntax node one more time has no effect.
8940 editor.update_in(cx, |editor, window, cx| {
8941 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8942 });
8943 editor.update_in(cx, |editor, _, cx| {
8944 assert_text_with_selections(
8945 editor,
8946 indoc! {r#"
8947 use mod1::mod2::{mod3, moˇd4};
8948
8949 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
8950 let var1 = "teˇxt";
8951 }
8952 "#},
8953 cx,
8954 );
8955 });
8956
8957 // Ensure that we keep expanding the selection if the larger selection starts or ends within
8958 // a fold.
8959 editor.update_in(cx, |editor, window, cx| {
8960 editor.fold_creases(
8961 vec![
8962 Crease::simple(
8963 Point::new(0, 21)..Point::new(0, 24),
8964 FoldPlaceholder::test(),
8965 ),
8966 Crease::simple(
8967 Point::new(3, 20)..Point::new(3, 22),
8968 FoldPlaceholder::test(),
8969 ),
8970 ],
8971 true,
8972 window,
8973 cx,
8974 );
8975 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8976 });
8977 editor.update(cx, |editor, cx| {
8978 assert_text_with_selections(
8979 editor,
8980 indoc! {r#"
8981 use mod1::mod2::«{mod3, mod4}ˇ»;
8982
8983 fn fn_1«ˇ(param1: bool, param2: &str)» {
8984 let var1 = "«ˇtext»";
8985 }
8986 "#},
8987 cx,
8988 );
8989 });
8990}
8991
8992#[gpui::test]
8993async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
8994 init_test(cx, |_| {});
8995
8996 let language = Arc::new(Language::new(
8997 LanguageConfig::default(),
8998 Some(tree_sitter_rust::LANGUAGE.into()),
8999 ));
9000
9001 let text = "let a = 2;";
9002
9003 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9004 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9005 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9006
9007 editor
9008 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9009 .await;
9010
9011 // Test case 1: Cursor at end of word
9012 editor.update_in(cx, |editor, window, cx| {
9013 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9014 s.select_display_ranges([
9015 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
9016 ]);
9017 });
9018 });
9019 editor.update(cx, |editor, cx| {
9020 assert_text_with_selections(editor, "let aˇ = 2;", cx);
9021 });
9022 editor.update_in(cx, |editor, window, cx| {
9023 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9024 });
9025 editor.update(cx, |editor, cx| {
9026 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
9027 });
9028 editor.update_in(cx, |editor, window, cx| {
9029 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9030 });
9031 editor.update(cx, |editor, cx| {
9032 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
9033 });
9034
9035 // Test case 2: Cursor at end of statement
9036 editor.update_in(cx, |editor, window, cx| {
9037 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9038 s.select_display_ranges([
9039 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
9040 ]);
9041 });
9042 });
9043 editor.update(cx, |editor, cx| {
9044 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
9045 });
9046 editor.update_in(cx, |editor, window, cx| {
9047 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9048 });
9049 editor.update(cx, |editor, cx| {
9050 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
9051 });
9052}
9053
9054#[gpui::test]
9055async fn test_select_larger_syntax_node_for_cursor_at_symbol(cx: &mut TestAppContext) {
9056 init_test(cx, |_| {});
9057
9058 let language = Arc::new(Language::new(
9059 LanguageConfig {
9060 name: "JavaScript".into(),
9061 ..Default::default()
9062 },
9063 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
9064 ));
9065
9066 let text = r#"
9067 let a = {
9068 key: "value",
9069 };
9070 "#
9071 .unindent();
9072
9073 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9074 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9075 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9076
9077 editor
9078 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9079 .await;
9080
9081 // Test case 1: Cursor after '{'
9082 editor.update_in(cx, |editor, window, cx| {
9083 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9084 s.select_display_ranges([
9085 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 9)
9086 ]);
9087 });
9088 });
9089 editor.update(cx, |editor, cx| {
9090 assert_text_with_selections(
9091 editor,
9092 indoc! {r#"
9093 let a = {ˇ
9094 key: "value",
9095 };
9096 "#},
9097 cx,
9098 );
9099 });
9100 editor.update_in(cx, |editor, window, cx| {
9101 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9102 });
9103 editor.update(cx, |editor, cx| {
9104 assert_text_with_selections(
9105 editor,
9106 indoc! {r#"
9107 let a = «ˇ{
9108 key: "value",
9109 }»;
9110 "#},
9111 cx,
9112 );
9113 });
9114
9115 // Test case 2: Cursor after ':'
9116 editor.update_in(cx, |editor, window, cx| {
9117 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9118 s.select_display_ranges([
9119 DisplayPoint::new(DisplayRow(1), 8)..DisplayPoint::new(DisplayRow(1), 8)
9120 ]);
9121 });
9122 });
9123 editor.update(cx, |editor, cx| {
9124 assert_text_with_selections(
9125 editor,
9126 indoc! {r#"
9127 let a = {
9128 key:ˇ "value",
9129 };
9130 "#},
9131 cx,
9132 );
9133 });
9134 editor.update_in(cx, |editor, window, cx| {
9135 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9136 });
9137 editor.update(cx, |editor, cx| {
9138 assert_text_with_selections(
9139 editor,
9140 indoc! {r#"
9141 let a = {
9142 «ˇkey: "value"»,
9143 };
9144 "#},
9145 cx,
9146 );
9147 });
9148 editor.update_in(cx, |editor, window, cx| {
9149 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9150 });
9151 editor.update(cx, |editor, cx| {
9152 assert_text_with_selections(
9153 editor,
9154 indoc! {r#"
9155 let a = «ˇ{
9156 key: "value",
9157 }»;
9158 "#},
9159 cx,
9160 );
9161 });
9162
9163 // Test case 3: Cursor after ','
9164 editor.update_in(cx, |editor, window, cx| {
9165 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9166 s.select_display_ranges([
9167 DisplayPoint::new(DisplayRow(1), 17)..DisplayPoint::new(DisplayRow(1), 17)
9168 ]);
9169 });
9170 });
9171 editor.update(cx, |editor, cx| {
9172 assert_text_with_selections(
9173 editor,
9174 indoc! {r#"
9175 let a = {
9176 key: "value",ˇ
9177 };
9178 "#},
9179 cx,
9180 );
9181 });
9182 editor.update_in(cx, |editor, window, cx| {
9183 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9184 });
9185 editor.update(cx, |editor, cx| {
9186 assert_text_with_selections(
9187 editor,
9188 indoc! {r#"
9189 let a = «ˇ{
9190 key: "value",
9191 }»;
9192 "#},
9193 cx,
9194 );
9195 });
9196
9197 // Test case 4: Cursor after ';'
9198 editor.update_in(cx, |editor, window, cx| {
9199 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9200 s.select_display_ranges([
9201 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)
9202 ]);
9203 });
9204 });
9205 editor.update(cx, |editor, cx| {
9206 assert_text_with_selections(
9207 editor,
9208 indoc! {r#"
9209 let a = {
9210 key: "value",
9211 };ˇ
9212 "#},
9213 cx,
9214 );
9215 });
9216 editor.update_in(cx, |editor, window, cx| {
9217 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9218 });
9219 editor.update(cx, |editor, cx| {
9220 assert_text_with_selections(
9221 editor,
9222 indoc! {r#"
9223 «ˇlet a = {
9224 key: "value",
9225 };
9226 »"#},
9227 cx,
9228 );
9229 });
9230}
9231
9232#[gpui::test]
9233async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
9234 init_test(cx, |_| {});
9235
9236 let language = Arc::new(Language::new(
9237 LanguageConfig::default(),
9238 Some(tree_sitter_rust::LANGUAGE.into()),
9239 ));
9240
9241 let text = r#"
9242 use mod1::mod2::{mod3, mod4};
9243
9244 fn fn_1(param1: bool, param2: &str) {
9245 let var1 = "hello world";
9246 }
9247 "#
9248 .unindent();
9249
9250 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9251 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9252 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9253
9254 editor
9255 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9256 .await;
9257
9258 // Test 1: Cursor on a letter of a string word
9259 editor.update_in(cx, |editor, window, cx| {
9260 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9261 s.select_display_ranges([
9262 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
9263 ]);
9264 });
9265 });
9266 editor.update_in(cx, |editor, window, cx| {
9267 assert_text_with_selections(
9268 editor,
9269 indoc! {r#"
9270 use mod1::mod2::{mod3, mod4};
9271
9272 fn fn_1(param1: bool, param2: &str) {
9273 let var1 = "hˇello world";
9274 }
9275 "#},
9276 cx,
9277 );
9278 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9279 assert_text_with_selections(
9280 editor,
9281 indoc! {r#"
9282 use mod1::mod2::{mod3, mod4};
9283
9284 fn fn_1(param1: bool, param2: &str) {
9285 let var1 = "«ˇhello» world";
9286 }
9287 "#},
9288 cx,
9289 );
9290 });
9291
9292 // Test 2: Partial selection within a word
9293 editor.update_in(cx, |editor, window, cx| {
9294 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9295 s.select_display_ranges([
9296 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
9297 ]);
9298 });
9299 });
9300 editor.update_in(cx, |editor, window, cx| {
9301 assert_text_with_selections(
9302 editor,
9303 indoc! {r#"
9304 use mod1::mod2::{mod3, mod4};
9305
9306 fn fn_1(param1: bool, param2: &str) {
9307 let var1 = "h«elˇ»lo world";
9308 }
9309 "#},
9310 cx,
9311 );
9312 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9313 assert_text_with_selections(
9314 editor,
9315 indoc! {r#"
9316 use mod1::mod2::{mod3, mod4};
9317
9318 fn fn_1(param1: bool, param2: &str) {
9319 let var1 = "«ˇhello» world";
9320 }
9321 "#},
9322 cx,
9323 );
9324 });
9325
9326 // Test 3: Complete word already selected
9327 editor.update_in(cx, |editor, window, cx| {
9328 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9329 s.select_display_ranges([
9330 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
9331 ]);
9332 });
9333 });
9334 editor.update_in(cx, |editor, window, cx| {
9335 assert_text_with_selections(
9336 editor,
9337 indoc! {r#"
9338 use mod1::mod2::{mod3, mod4};
9339
9340 fn fn_1(param1: bool, param2: &str) {
9341 let var1 = "«helloˇ» world";
9342 }
9343 "#},
9344 cx,
9345 );
9346 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9347 assert_text_with_selections(
9348 editor,
9349 indoc! {r#"
9350 use mod1::mod2::{mod3, mod4};
9351
9352 fn fn_1(param1: bool, param2: &str) {
9353 let var1 = "«hello worldˇ»";
9354 }
9355 "#},
9356 cx,
9357 );
9358 });
9359
9360 // Test 4: Selection spanning across words
9361 editor.update_in(cx, |editor, window, cx| {
9362 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9363 s.select_display_ranges([
9364 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
9365 ]);
9366 });
9367 });
9368 editor.update_in(cx, |editor, window, cx| {
9369 assert_text_with_selections(
9370 editor,
9371 indoc! {r#"
9372 use mod1::mod2::{mod3, mod4};
9373
9374 fn fn_1(param1: bool, param2: &str) {
9375 let var1 = "hel«lo woˇ»rld";
9376 }
9377 "#},
9378 cx,
9379 );
9380 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9381 assert_text_with_selections(
9382 editor,
9383 indoc! {r#"
9384 use mod1::mod2::{mod3, mod4};
9385
9386 fn fn_1(param1: bool, param2: &str) {
9387 let var1 = "«ˇhello world»";
9388 }
9389 "#},
9390 cx,
9391 );
9392 });
9393
9394 // Test 5: Expansion beyond string
9395 editor.update_in(cx, |editor, window, cx| {
9396 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9397 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9398 assert_text_with_selections(
9399 editor,
9400 indoc! {r#"
9401 use mod1::mod2::{mod3, mod4};
9402
9403 fn fn_1(param1: bool, param2: &str) {
9404 «ˇlet var1 = "hello world";»
9405 }
9406 "#},
9407 cx,
9408 );
9409 });
9410}
9411
9412#[gpui::test]
9413async fn test_unwrap_syntax_nodes(cx: &mut gpui::TestAppContext) {
9414 init_test(cx, |_| {});
9415
9416 let mut cx = EditorTestContext::new(cx).await;
9417
9418 let language = Arc::new(Language::new(
9419 LanguageConfig::default(),
9420 Some(tree_sitter_rust::LANGUAGE.into()),
9421 ));
9422
9423 cx.update_buffer(|buffer, cx| {
9424 buffer.set_language(Some(language), cx);
9425 });
9426
9427 cx.set_state(indoc! { r#"use mod1::{mod2::{«mod3ˇ», mod4}, mod5::{mod6, «mod7ˇ»}};"# });
9428 cx.update_editor(|editor, window, cx| {
9429 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9430 });
9431
9432 cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# });
9433
9434 cx.set_state(indoc! { r#"fn a() {
9435 // what
9436 // a
9437 // ˇlong
9438 // method
9439 // I
9440 // sure
9441 // hope
9442 // it
9443 // works
9444 }"# });
9445
9446 let buffer = cx.update_multibuffer(|multibuffer, _| multibuffer.as_singleton().unwrap());
9447 let multi_buffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
9448 cx.update(|_, cx| {
9449 multi_buffer.update(cx, |multi_buffer, cx| {
9450 multi_buffer.set_excerpts_for_path(
9451 PathKey::for_buffer(&buffer, cx),
9452 buffer,
9453 [Point::new(1, 0)..Point::new(1, 0)],
9454 3,
9455 cx,
9456 );
9457 });
9458 });
9459
9460 let editor2 = cx.new_window_entity(|window, cx| {
9461 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
9462 });
9463
9464 let mut cx = EditorTestContext::for_editor_in(editor2, &mut cx).await;
9465 cx.update_editor(|editor, window, cx| {
9466 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
9467 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)]);
9468 })
9469 });
9470
9471 cx.assert_editor_state(indoc! { "
9472 fn a() {
9473 // what
9474 // a
9475 ˇ // long
9476 // method"});
9477
9478 cx.update_editor(|editor, window, cx| {
9479 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9480 });
9481
9482 // Although we could potentially make the action work when the syntax node
9483 // is half-hidden, it seems a bit dangerous as you can't easily tell what it
9484 // did. Maybe we could also expand the excerpt to contain the range?
9485 cx.assert_editor_state(indoc! { "
9486 fn a() {
9487 // what
9488 // a
9489 ˇ // long
9490 // method"});
9491}
9492
9493#[gpui::test]
9494async fn test_fold_function_bodies(cx: &mut TestAppContext) {
9495 init_test(cx, |_| {});
9496
9497 let base_text = r#"
9498 impl A {
9499 // this is an uncommitted comment
9500
9501 fn b() {
9502 c();
9503 }
9504
9505 // this is another uncommitted comment
9506
9507 fn d() {
9508 // e
9509 // f
9510 }
9511 }
9512
9513 fn g() {
9514 // h
9515 }
9516 "#
9517 .unindent();
9518
9519 let text = r#"
9520 ˇimpl A {
9521
9522 fn b() {
9523 c();
9524 }
9525
9526 fn d() {
9527 // e
9528 // f
9529 }
9530 }
9531
9532 fn g() {
9533 // h
9534 }
9535 "#
9536 .unindent();
9537
9538 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9539 cx.set_state(&text);
9540 cx.set_head_text(&base_text);
9541 cx.update_editor(|editor, window, cx| {
9542 editor.expand_all_diff_hunks(&Default::default(), window, cx);
9543 });
9544
9545 cx.assert_state_with_diff(
9546 "
9547 ˇimpl A {
9548 - // this is an uncommitted comment
9549
9550 fn b() {
9551 c();
9552 }
9553
9554 - // this is another uncommitted comment
9555 -
9556 fn d() {
9557 // e
9558 // f
9559 }
9560 }
9561
9562 fn g() {
9563 // h
9564 }
9565 "
9566 .unindent(),
9567 );
9568
9569 let expected_display_text = "
9570 impl A {
9571 // this is an uncommitted comment
9572
9573 fn b() {
9574 ⋯
9575 }
9576
9577 // this is another uncommitted comment
9578
9579 fn d() {
9580 ⋯
9581 }
9582 }
9583
9584 fn g() {
9585 ⋯
9586 }
9587 "
9588 .unindent();
9589
9590 cx.update_editor(|editor, window, cx| {
9591 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
9592 assert_eq!(editor.display_text(cx), expected_display_text);
9593 });
9594}
9595
9596#[gpui::test]
9597async fn test_autoindent(cx: &mut TestAppContext) {
9598 init_test(cx, |_| {});
9599
9600 let language = Arc::new(
9601 Language::new(
9602 LanguageConfig {
9603 brackets: BracketPairConfig {
9604 pairs: vec![
9605 BracketPair {
9606 start: "{".to_string(),
9607 end: "}".to_string(),
9608 close: false,
9609 surround: false,
9610 newline: true,
9611 },
9612 BracketPair {
9613 start: "(".to_string(),
9614 end: ")".to_string(),
9615 close: false,
9616 surround: false,
9617 newline: true,
9618 },
9619 ],
9620 ..Default::default()
9621 },
9622 ..Default::default()
9623 },
9624 Some(tree_sitter_rust::LANGUAGE.into()),
9625 )
9626 .with_indents_query(
9627 r#"
9628 (_ "(" ")" @end) @indent
9629 (_ "{" "}" @end) @indent
9630 "#,
9631 )
9632 .unwrap(),
9633 );
9634
9635 let text = "fn a() {}";
9636
9637 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9638 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9639 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9640 editor
9641 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9642 .await;
9643
9644 editor.update_in(cx, |editor, window, cx| {
9645 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9646 s.select_ranges([5..5, 8..8, 9..9])
9647 });
9648 editor.newline(&Newline, window, cx);
9649 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
9650 assert_eq!(
9651 editor.selections.ranges(&editor.display_snapshot(cx)),
9652 &[
9653 Point::new(1, 4)..Point::new(1, 4),
9654 Point::new(3, 4)..Point::new(3, 4),
9655 Point::new(5, 0)..Point::new(5, 0)
9656 ]
9657 );
9658 });
9659}
9660
9661#[gpui::test]
9662async fn test_autoindent_disabled(cx: &mut TestAppContext) {
9663 init_test(cx, |settings| settings.defaults.auto_indent = Some(false));
9664
9665 let language = Arc::new(
9666 Language::new(
9667 LanguageConfig {
9668 brackets: BracketPairConfig {
9669 pairs: vec![
9670 BracketPair {
9671 start: "{".to_string(),
9672 end: "}".to_string(),
9673 close: false,
9674 surround: false,
9675 newline: true,
9676 },
9677 BracketPair {
9678 start: "(".to_string(),
9679 end: ")".to_string(),
9680 close: false,
9681 surround: false,
9682 newline: true,
9683 },
9684 ],
9685 ..Default::default()
9686 },
9687 ..Default::default()
9688 },
9689 Some(tree_sitter_rust::LANGUAGE.into()),
9690 )
9691 .with_indents_query(
9692 r#"
9693 (_ "(" ")" @end) @indent
9694 (_ "{" "}" @end) @indent
9695 "#,
9696 )
9697 .unwrap(),
9698 );
9699
9700 let text = "fn a() {}";
9701
9702 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9703 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9704 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9705 editor
9706 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9707 .await;
9708
9709 editor.update_in(cx, |editor, window, cx| {
9710 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9711 s.select_ranges([5..5, 8..8, 9..9])
9712 });
9713 editor.newline(&Newline, window, cx);
9714 assert_eq!(
9715 editor.text(cx),
9716 indoc!(
9717 "
9718 fn a(
9719
9720 ) {
9721
9722 }
9723 "
9724 )
9725 );
9726 assert_eq!(
9727 editor.selections.ranges(&editor.display_snapshot(cx)),
9728 &[
9729 Point::new(1, 0)..Point::new(1, 0),
9730 Point::new(3, 0)..Point::new(3, 0),
9731 Point::new(5, 0)..Point::new(5, 0)
9732 ]
9733 );
9734 });
9735}
9736
9737#[gpui::test]
9738async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) {
9739 init_test(cx, |settings| {
9740 settings.defaults.auto_indent = Some(true);
9741 settings.languages.0.insert(
9742 "python".into(),
9743 LanguageSettingsContent {
9744 auto_indent: Some(false),
9745 ..Default::default()
9746 },
9747 );
9748 });
9749
9750 let mut cx = EditorTestContext::new(cx).await;
9751
9752 let injected_language = Arc::new(
9753 Language::new(
9754 LanguageConfig {
9755 brackets: BracketPairConfig {
9756 pairs: vec![
9757 BracketPair {
9758 start: "{".to_string(),
9759 end: "}".to_string(),
9760 close: false,
9761 surround: false,
9762 newline: true,
9763 },
9764 BracketPair {
9765 start: "(".to_string(),
9766 end: ")".to_string(),
9767 close: true,
9768 surround: false,
9769 newline: true,
9770 },
9771 ],
9772 ..Default::default()
9773 },
9774 name: "python".into(),
9775 ..Default::default()
9776 },
9777 Some(tree_sitter_python::LANGUAGE.into()),
9778 )
9779 .with_indents_query(
9780 r#"
9781 (_ "(" ")" @end) @indent
9782 (_ "{" "}" @end) @indent
9783 "#,
9784 )
9785 .unwrap(),
9786 );
9787
9788 let language = Arc::new(
9789 Language::new(
9790 LanguageConfig {
9791 brackets: BracketPairConfig {
9792 pairs: vec![
9793 BracketPair {
9794 start: "{".to_string(),
9795 end: "}".to_string(),
9796 close: false,
9797 surround: false,
9798 newline: true,
9799 },
9800 BracketPair {
9801 start: "(".to_string(),
9802 end: ")".to_string(),
9803 close: true,
9804 surround: false,
9805 newline: true,
9806 },
9807 ],
9808 ..Default::default()
9809 },
9810 name: LanguageName::new("rust"),
9811 ..Default::default()
9812 },
9813 Some(tree_sitter_rust::LANGUAGE.into()),
9814 )
9815 .with_indents_query(
9816 r#"
9817 (_ "(" ")" @end) @indent
9818 (_ "{" "}" @end) @indent
9819 "#,
9820 )
9821 .unwrap()
9822 .with_injection_query(
9823 r#"
9824 (macro_invocation
9825 macro: (identifier) @_macro_name
9826 (token_tree) @injection.content
9827 (#set! injection.language "python"))
9828 "#,
9829 )
9830 .unwrap(),
9831 );
9832
9833 cx.language_registry().add(injected_language);
9834 cx.language_registry().add(language.clone());
9835
9836 cx.update_buffer(|buffer, cx| {
9837 buffer.set_language(Some(language), cx);
9838 });
9839
9840 cx.set_state(r#"struct A {ˇ}"#);
9841
9842 cx.update_editor(|editor, window, cx| {
9843 editor.newline(&Default::default(), window, cx);
9844 });
9845
9846 cx.assert_editor_state(indoc!(
9847 "struct A {
9848 ˇ
9849 }"
9850 ));
9851
9852 cx.set_state(r#"select_biased!(ˇ)"#);
9853
9854 cx.update_editor(|editor, window, cx| {
9855 editor.newline(&Default::default(), window, cx);
9856 editor.handle_input("def ", window, cx);
9857 editor.handle_input("(", window, cx);
9858 editor.newline(&Default::default(), window, cx);
9859 editor.handle_input("a", window, cx);
9860 });
9861
9862 cx.assert_editor_state(indoc!(
9863 "select_biased!(
9864 def (
9865 aˇ
9866 )
9867 )"
9868 ));
9869}
9870
9871#[gpui::test]
9872async fn test_autoindent_selections(cx: &mut TestAppContext) {
9873 init_test(cx, |_| {});
9874
9875 {
9876 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9877 cx.set_state(indoc! {"
9878 impl A {
9879
9880 fn b() {}
9881
9882 «fn c() {
9883
9884 }ˇ»
9885 }
9886 "});
9887
9888 cx.update_editor(|editor, window, cx| {
9889 editor.autoindent(&Default::default(), window, cx);
9890 });
9891
9892 cx.assert_editor_state(indoc! {"
9893 impl A {
9894
9895 fn b() {}
9896
9897 «fn c() {
9898
9899 }ˇ»
9900 }
9901 "});
9902 }
9903
9904 {
9905 let mut cx = EditorTestContext::new_multibuffer(
9906 cx,
9907 [indoc! { "
9908 impl A {
9909 «
9910 // a
9911 fn b(){}
9912 »
9913 «
9914 }
9915 fn c(){}
9916 »
9917 "}],
9918 );
9919
9920 let buffer = cx.update_editor(|editor, _, cx| {
9921 let buffer = editor.buffer().update(cx, |buffer, _| {
9922 buffer.all_buffers().iter().next().unwrap().clone()
9923 });
9924 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
9925 buffer
9926 });
9927
9928 cx.run_until_parked();
9929 cx.update_editor(|editor, window, cx| {
9930 editor.select_all(&Default::default(), window, cx);
9931 editor.autoindent(&Default::default(), window, cx)
9932 });
9933 cx.run_until_parked();
9934
9935 cx.update(|_, cx| {
9936 assert_eq!(
9937 buffer.read(cx).text(),
9938 indoc! { "
9939 impl A {
9940
9941 // a
9942 fn b(){}
9943
9944
9945 }
9946 fn c(){}
9947
9948 " }
9949 )
9950 });
9951 }
9952}
9953
9954#[gpui::test]
9955async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
9956 init_test(cx, |_| {});
9957
9958 let mut cx = EditorTestContext::new(cx).await;
9959
9960 let language = Arc::new(Language::new(
9961 LanguageConfig {
9962 brackets: BracketPairConfig {
9963 pairs: vec![
9964 BracketPair {
9965 start: "{".to_string(),
9966 end: "}".to_string(),
9967 close: true,
9968 surround: true,
9969 newline: true,
9970 },
9971 BracketPair {
9972 start: "(".to_string(),
9973 end: ")".to_string(),
9974 close: true,
9975 surround: true,
9976 newline: true,
9977 },
9978 BracketPair {
9979 start: "/*".to_string(),
9980 end: " */".to_string(),
9981 close: true,
9982 surround: true,
9983 newline: true,
9984 },
9985 BracketPair {
9986 start: "[".to_string(),
9987 end: "]".to_string(),
9988 close: false,
9989 surround: false,
9990 newline: true,
9991 },
9992 BracketPair {
9993 start: "\"".to_string(),
9994 end: "\"".to_string(),
9995 close: true,
9996 surround: true,
9997 newline: false,
9998 },
9999 BracketPair {
10000 start: "<".to_string(),
10001 end: ">".to_string(),
10002 close: false,
10003 surround: true,
10004 newline: true,
10005 },
10006 ],
10007 ..Default::default()
10008 },
10009 autoclose_before: "})]".to_string(),
10010 ..Default::default()
10011 },
10012 Some(tree_sitter_rust::LANGUAGE.into()),
10013 ));
10014
10015 cx.language_registry().add(language.clone());
10016 cx.update_buffer(|buffer, cx| {
10017 buffer.set_language(Some(language), cx);
10018 });
10019
10020 cx.set_state(
10021 &r#"
10022 🏀ˇ
10023 εˇ
10024 ❤️ˇ
10025 "#
10026 .unindent(),
10027 );
10028
10029 // autoclose multiple nested brackets at multiple cursors
10030 cx.update_editor(|editor, window, cx| {
10031 editor.handle_input("{", window, cx);
10032 editor.handle_input("{", window, cx);
10033 editor.handle_input("{", window, cx);
10034 });
10035 cx.assert_editor_state(
10036 &"
10037 🏀{{{ˇ}}}
10038 ε{{{ˇ}}}
10039 ❤️{{{ˇ}}}
10040 "
10041 .unindent(),
10042 );
10043
10044 // insert a different closing bracket
10045 cx.update_editor(|editor, window, cx| {
10046 editor.handle_input(")", window, cx);
10047 });
10048 cx.assert_editor_state(
10049 &"
10050 🏀{{{)ˇ}}}
10051 ε{{{)ˇ}}}
10052 ❤️{{{)ˇ}}}
10053 "
10054 .unindent(),
10055 );
10056
10057 // skip over the auto-closed brackets when typing a closing bracket
10058 cx.update_editor(|editor, window, cx| {
10059 editor.move_right(&MoveRight, window, cx);
10060 editor.handle_input("}", window, cx);
10061 editor.handle_input("}", window, cx);
10062 editor.handle_input("}", window, cx);
10063 });
10064 cx.assert_editor_state(
10065 &"
10066 🏀{{{)}}}}ˇ
10067 ε{{{)}}}}ˇ
10068 ❤️{{{)}}}}ˇ
10069 "
10070 .unindent(),
10071 );
10072
10073 // autoclose multi-character pairs
10074 cx.set_state(
10075 &"
10076 ˇ
10077 ˇ
10078 "
10079 .unindent(),
10080 );
10081 cx.update_editor(|editor, window, cx| {
10082 editor.handle_input("/", window, cx);
10083 editor.handle_input("*", window, cx);
10084 });
10085 cx.assert_editor_state(
10086 &"
10087 /*ˇ */
10088 /*ˇ */
10089 "
10090 .unindent(),
10091 );
10092
10093 // one cursor autocloses a multi-character pair, one cursor
10094 // does not autoclose.
10095 cx.set_state(
10096 &"
10097 /ˇ
10098 ˇ
10099 "
10100 .unindent(),
10101 );
10102 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
10103 cx.assert_editor_state(
10104 &"
10105 /*ˇ */
10106 *ˇ
10107 "
10108 .unindent(),
10109 );
10110
10111 // Don't autoclose if the next character isn't whitespace and isn't
10112 // listed in the language's "autoclose_before" section.
10113 cx.set_state("ˇa b");
10114 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10115 cx.assert_editor_state("{ˇa b");
10116
10117 // Don't autoclose if `close` is false for the bracket pair
10118 cx.set_state("ˇ");
10119 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
10120 cx.assert_editor_state("[ˇ");
10121
10122 // Surround with brackets if text is selected
10123 cx.set_state("«aˇ» b");
10124 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10125 cx.assert_editor_state("{«aˇ»} b");
10126
10127 // Autoclose when not immediately after a word character
10128 cx.set_state("a ˇ");
10129 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10130 cx.assert_editor_state("a \"ˇ\"");
10131
10132 // Autoclose pair where the start and end characters are the same
10133 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10134 cx.assert_editor_state("a \"\"ˇ");
10135
10136 // Don't autoclose when immediately after a word character
10137 cx.set_state("aˇ");
10138 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10139 cx.assert_editor_state("a\"ˇ");
10140
10141 // Do autoclose when after a non-word character
10142 cx.set_state("{ˇ");
10143 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10144 cx.assert_editor_state("{\"ˇ\"");
10145
10146 // Non identical pairs autoclose regardless of preceding character
10147 cx.set_state("aˇ");
10148 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10149 cx.assert_editor_state("a{ˇ}");
10150
10151 // Don't autoclose pair if autoclose is disabled
10152 cx.set_state("ˇ");
10153 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
10154 cx.assert_editor_state("<ˇ");
10155
10156 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
10157 cx.set_state("«aˇ» b");
10158 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
10159 cx.assert_editor_state("<«aˇ»> b");
10160}
10161
10162#[gpui::test]
10163async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
10164 init_test(cx, |settings| {
10165 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10166 });
10167
10168 let mut cx = EditorTestContext::new(cx).await;
10169
10170 let language = Arc::new(Language::new(
10171 LanguageConfig {
10172 brackets: BracketPairConfig {
10173 pairs: vec![
10174 BracketPair {
10175 start: "{".to_string(),
10176 end: "}".to_string(),
10177 close: true,
10178 surround: true,
10179 newline: true,
10180 },
10181 BracketPair {
10182 start: "(".to_string(),
10183 end: ")".to_string(),
10184 close: true,
10185 surround: true,
10186 newline: true,
10187 },
10188 BracketPair {
10189 start: "[".to_string(),
10190 end: "]".to_string(),
10191 close: false,
10192 surround: false,
10193 newline: true,
10194 },
10195 ],
10196 ..Default::default()
10197 },
10198 autoclose_before: "})]".to_string(),
10199 ..Default::default()
10200 },
10201 Some(tree_sitter_rust::LANGUAGE.into()),
10202 ));
10203
10204 cx.language_registry().add(language.clone());
10205 cx.update_buffer(|buffer, cx| {
10206 buffer.set_language(Some(language), cx);
10207 });
10208
10209 cx.set_state(
10210 &"
10211 ˇ
10212 ˇ
10213 ˇ
10214 "
10215 .unindent(),
10216 );
10217
10218 // ensure only matching closing brackets are skipped over
10219 cx.update_editor(|editor, window, cx| {
10220 editor.handle_input("}", window, cx);
10221 editor.move_left(&MoveLeft, window, cx);
10222 editor.handle_input(")", window, cx);
10223 editor.move_left(&MoveLeft, window, cx);
10224 });
10225 cx.assert_editor_state(
10226 &"
10227 ˇ)}
10228 ˇ)}
10229 ˇ)}
10230 "
10231 .unindent(),
10232 );
10233
10234 // skip-over closing brackets at multiple cursors
10235 cx.update_editor(|editor, window, cx| {
10236 editor.handle_input(")", window, cx);
10237 editor.handle_input("}", window, cx);
10238 });
10239 cx.assert_editor_state(
10240 &"
10241 )}ˇ
10242 )}ˇ
10243 )}ˇ
10244 "
10245 .unindent(),
10246 );
10247
10248 // ignore non-close brackets
10249 cx.update_editor(|editor, window, cx| {
10250 editor.handle_input("]", window, cx);
10251 editor.move_left(&MoveLeft, window, cx);
10252 editor.handle_input("]", window, cx);
10253 });
10254 cx.assert_editor_state(
10255 &"
10256 )}]ˇ]
10257 )}]ˇ]
10258 )}]ˇ]
10259 "
10260 .unindent(),
10261 );
10262}
10263
10264#[gpui::test]
10265async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
10266 init_test(cx, |_| {});
10267
10268 let mut cx = EditorTestContext::new(cx).await;
10269
10270 let html_language = Arc::new(
10271 Language::new(
10272 LanguageConfig {
10273 name: "HTML".into(),
10274 brackets: BracketPairConfig {
10275 pairs: vec![
10276 BracketPair {
10277 start: "<".into(),
10278 end: ">".into(),
10279 close: true,
10280 ..Default::default()
10281 },
10282 BracketPair {
10283 start: "{".into(),
10284 end: "}".into(),
10285 close: true,
10286 ..Default::default()
10287 },
10288 BracketPair {
10289 start: "(".into(),
10290 end: ")".into(),
10291 close: true,
10292 ..Default::default()
10293 },
10294 ],
10295 ..Default::default()
10296 },
10297 autoclose_before: "})]>".into(),
10298 ..Default::default()
10299 },
10300 Some(tree_sitter_html::LANGUAGE.into()),
10301 )
10302 .with_injection_query(
10303 r#"
10304 (script_element
10305 (raw_text) @injection.content
10306 (#set! injection.language "javascript"))
10307 "#,
10308 )
10309 .unwrap(),
10310 );
10311
10312 let javascript_language = Arc::new(Language::new(
10313 LanguageConfig {
10314 name: "JavaScript".into(),
10315 brackets: BracketPairConfig {
10316 pairs: vec![
10317 BracketPair {
10318 start: "/*".into(),
10319 end: " */".into(),
10320 close: true,
10321 ..Default::default()
10322 },
10323 BracketPair {
10324 start: "{".into(),
10325 end: "}".into(),
10326 close: true,
10327 ..Default::default()
10328 },
10329 BracketPair {
10330 start: "(".into(),
10331 end: ")".into(),
10332 close: true,
10333 ..Default::default()
10334 },
10335 ],
10336 ..Default::default()
10337 },
10338 autoclose_before: "})]>".into(),
10339 ..Default::default()
10340 },
10341 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
10342 ));
10343
10344 cx.language_registry().add(html_language.clone());
10345 cx.language_registry().add(javascript_language);
10346 cx.executor().run_until_parked();
10347
10348 cx.update_buffer(|buffer, cx| {
10349 buffer.set_language(Some(html_language), cx);
10350 });
10351
10352 cx.set_state(
10353 &r#"
10354 <body>ˇ
10355 <script>
10356 var x = 1;ˇ
10357 </script>
10358 </body>ˇ
10359 "#
10360 .unindent(),
10361 );
10362
10363 // Precondition: different languages are active at different locations.
10364 cx.update_editor(|editor, window, cx| {
10365 let snapshot = editor.snapshot(window, cx);
10366 let cursors = editor
10367 .selections
10368 .ranges::<usize>(&editor.display_snapshot(cx));
10369 let languages = cursors
10370 .iter()
10371 .map(|c| snapshot.language_at(c.start).unwrap().name())
10372 .collect::<Vec<_>>();
10373 assert_eq!(
10374 languages,
10375 &["HTML".into(), "JavaScript".into(), "HTML".into()]
10376 );
10377 });
10378
10379 // Angle brackets autoclose in HTML, but not JavaScript.
10380 cx.update_editor(|editor, window, cx| {
10381 editor.handle_input("<", window, cx);
10382 editor.handle_input("a", window, cx);
10383 });
10384 cx.assert_editor_state(
10385 &r#"
10386 <body><aˇ>
10387 <script>
10388 var x = 1;<aˇ
10389 </script>
10390 </body><aˇ>
10391 "#
10392 .unindent(),
10393 );
10394
10395 // Curly braces and parens autoclose in both HTML and JavaScript.
10396 cx.update_editor(|editor, window, cx| {
10397 editor.handle_input(" b=", window, cx);
10398 editor.handle_input("{", window, cx);
10399 editor.handle_input("c", window, cx);
10400 editor.handle_input("(", window, cx);
10401 });
10402 cx.assert_editor_state(
10403 &r#"
10404 <body><a b={c(ˇ)}>
10405 <script>
10406 var x = 1;<a b={c(ˇ)}
10407 </script>
10408 </body><a b={c(ˇ)}>
10409 "#
10410 .unindent(),
10411 );
10412
10413 // Brackets that were already autoclosed are skipped.
10414 cx.update_editor(|editor, window, cx| {
10415 editor.handle_input(")", window, cx);
10416 editor.handle_input("d", window, cx);
10417 editor.handle_input("}", window, cx);
10418 });
10419 cx.assert_editor_state(
10420 &r#"
10421 <body><a b={c()d}ˇ>
10422 <script>
10423 var x = 1;<a b={c()d}ˇ
10424 </script>
10425 </body><a b={c()d}ˇ>
10426 "#
10427 .unindent(),
10428 );
10429 cx.update_editor(|editor, window, cx| {
10430 editor.handle_input(">", window, cx);
10431 });
10432 cx.assert_editor_state(
10433 &r#"
10434 <body><a b={c()d}>ˇ
10435 <script>
10436 var x = 1;<a b={c()d}>ˇ
10437 </script>
10438 </body><a b={c()d}>ˇ
10439 "#
10440 .unindent(),
10441 );
10442
10443 // Reset
10444 cx.set_state(
10445 &r#"
10446 <body>ˇ
10447 <script>
10448 var x = 1;ˇ
10449 </script>
10450 </body>ˇ
10451 "#
10452 .unindent(),
10453 );
10454
10455 cx.update_editor(|editor, window, cx| {
10456 editor.handle_input("<", window, cx);
10457 });
10458 cx.assert_editor_state(
10459 &r#"
10460 <body><ˇ>
10461 <script>
10462 var x = 1;<ˇ
10463 </script>
10464 </body><ˇ>
10465 "#
10466 .unindent(),
10467 );
10468
10469 // When backspacing, the closing angle brackets are removed.
10470 cx.update_editor(|editor, window, cx| {
10471 editor.backspace(&Backspace, window, cx);
10472 });
10473 cx.assert_editor_state(
10474 &r#"
10475 <body>ˇ
10476 <script>
10477 var x = 1;ˇ
10478 </script>
10479 </body>ˇ
10480 "#
10481 .unindent(),
10482 );
10483
10484 // Block comments autoclose in JavaScript, but not HTML.
10485 cx.update_editor(|editor, window, cx| {
10486 editor.handle_input("/", window, cx);
10487 editor.handle_input("*", window, cx);
10488 });
10489 cx.assert_editor_state(
10490 &r#"
10491 <body>/*ˇ
10492 <script>
10493 var x = 1;/*ˇ */
10494 </script>
10495 </body>/*ˇ
10496 "#
10497 .unindent(),
10498 );
10499}
10500
10501#[gpui::test]
10502async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
10503 init_test(cx, |_| {});
10504
10505 let mut cx = EditorTestContext::new(cx).await;
10506
10507 let rust_language = Arc::new(
10508 Language::new(
10509 LanguageConfig {
10510 name: "Rust".into(),
10511 brackets: serde_json::from_value(json!([
10512 { "start": "{", "end": "}", "close": true, "newline": true },
10513 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
10514 ]))
10515 .unwrap(),
10516 autoclose_before: "})]>".into(),
10517 ..Default::default()
10518 },
10519 Some(tree_sitter_rust::LANGUAGE.into()),
10520 )
10521 .with_override_query("(string_literal) @string")
10522 .unwrap(),
10523 );
10524
10525 cx.language_registry().add(rust_language.clone());
10526 cx.update_buffer(|buffer, cx| {
10527 buffer.set_language(Some(rust_language), cx);
10528 });
10529
10530 cx.set_state(
10531 &r#"
10532 let x = ˇ
10533 "#
10534 .unindent(),
10535 );
10536
10537 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
10538 cx.update_editor(|editor, window, cx| {
10539 editor.handle_input("\"", window, cx);
10540 });
10541 cx.assert_editor_state(
10542 &r#"
10543 let x = "ˇ"
10544 "#
10545 .unindent(),
10546 );
10547
10548 // Inserting another quotation mark. The cursor moves across the existing
10549 // automatically-inserted quotation mark.
10550 cx.update_editor(|editor, window, cx| {
10551 editor.handle_input("\"", window, cx);
10552 });
10553 cx.assert_editor_state(
10554 &r#"
10555 let x = ""ˇ
10556 "#
10557 .unindent(),
10558 );
10559
10560 // Reset
10561 cx.set_state(
10562 &r#"
10563 let x = ˇ
10564 "#
10565 .unindent(),
10566 );
10567
10568 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
10569 cx.update_editor(|editor, window, cx| {
10570 editor.handle_input("\"", window, cx);
10571 editor.handle_input(" ", window, cx);
10572 editor.move_left(&Default::default(), window, cx);
10573 editor.handle_input("\\", window, cx);
10574 editor.handle_input("\"", window, cx);
10575 });
10576 cx.assert_editor_state(
10577 &r#"
10578 let x = "\"ˇ "
10579 "#
10580 .unindent(),
10581 );
10582
10583 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
10584 // mark. Nothing is inserted.
10585 cx.update_editor(|editor, window, cx| {
10586 editor.move_right(&Default::default(), window, cx);
10587 editor.handle_input("\"", window, cx);
10588 });
10589 cx.assert_editor_state(
10590 &r#"
10591 let x = "\" "ˇ
10592 "#
10593 .unindent(),
10594 );
10595}
10596
10597#[gpui::test]
10598async fn test_surround_with_pair(cx: &mut TestAppContext) {
10599 init_test(cx, |_| {});
10600
10601 let language = Arc::new(Language::new(
10602 LanguageConfig {
10603 brackets: BracketPairConfig {
10604 pairs: vec![
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 ..Default::default()
10618 },
10619 ],
10620 ..Default::default()
10621 },
10622 ..Default::default()
10623 },
10624 Some(tree_sitter_rust::LANGUAGE.into()),
10625 ));
10626
10627 let text = r#"
10628 a
10629 b
10630 c
10631 "#
10632 .unindent();
10633
10634 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10635 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10636 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10637 editor
10638 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10639 .await;
10640
10641 editor.update_in(cx, |editor, window, cx| {
10642 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10643 s.select_display_ranges([
10644 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10645 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10646 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
10647 ])
10648 });
10649
10650 editor.handle_input("{", window, cx);
10651 editor.handle_input("{", window, cx);
10652 editor.handle_input("{", window, cx);
10653 assert_eq!(
10654 editor.text(cx),
10655 "
10656 {{{a}}}
10657 {{{b}}}
10658 {{{c}}}
10659 "
10660 .unindent()
10661 );
10662 assert_eq!(
10663 display_ranges(editor, cx),
10664 [
10665 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
10666 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
10667 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
10668 ]
10669 );
10670
10671 editor.undo(&Undo, window, cx);
10672 editor.undo(&Undo, window, cx);
10673 editor.undo(&Undo, window, cx);
10674 assert_eq!(
10675 editor.text(cx),
10676 "
10677 a
10678 b
10679 c
10680 "
10681 .unindent()
10682 );
10683 assert_eq!(
10684 display_ranges(editor, cx),
10685 [
10686 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10687 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10688 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10689 ]
10690 );
10691
10692 // Ensure inserting the first character of a multi-byte bracket pair
10693 // doesn't surround the selections with the bracket.
10694 editor.handle_input("/", window, cx);
10695 assert_eq!(
10696 editor.text(cx),
10697 "
10698 /
10699 /
10700 /
10701 "
10702 .unindent()
10703 );
10704 assert_eq!(
10705 display_ranges(editor, cx),
10706 [
10707 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10708 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10709 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10710 ]
10711 );
10712
10713 editor.undo(&Undo, window, cx);
10714 assert_eq!(
10715 editor.text(cx),
10716 "
10717 a
10718 b
10719 c
10720 "
10721 .unindent()
10722 );
10723 assert_eq!(
10724 display_ranges(editor, cx),
10725 [
10726 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10727 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10728 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10729 ]
10730 );
10731
10732 // Ensure inserting the last character of a multi-byte bracket pair
10733 // doesn't surround the selections with the bracket.
10734 editor.handle_input("*", window, cx);
10735 assert_eq!(
10736 editor.text(cx),
10737 "
10738 *
10739 *
10740 *
10741 "
10742 .unindent()
10743 );
10744 assert_eq!(
10745 display_ranges(editor, cx),
10746 [
10747 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10748 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10749 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10750 ]
10751 );
10752 });
10753}
10754
10755#[gpui::test]
10756async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
10757 init_test(cx, |_| {});
10758
10759 let language = Arc::new(Language::new(
10760 LanguageConfig {
10761 brackets: BracketPairConfig {
10762 pairs: vec![BracketPair {
10763 start: "{".to_string(),
10764 end: "}".to_string(),
10765 close: true,
10766 surround: true,
10767 newline: true,
10768 }],
10769 ..Default::default()
10770 },
10771 autoclose_before: "}".to_string(),
10772 ..Default::default()
10773 },
10774 Some(tree_sitter_rust::LANGUAGE.into()),
10775 ));
10776
10777 let text = r#"
10778 a
10779 b
10780 c
10781 "#
10782 .unindent();
10783
10784 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10785 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10786 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10787 editor
10788 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10789 .await;
10790
10791 editor.update_in(cx, |editor, window, cx| {
10792 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10793 s.select_ranges([
10794 Point::new(0, 1)..Point::new(0, 1),
10795 Point::new(1, 1)..Point::new(1, 1),
10796 Point::new(2, 1)..Point::new(2, 1),
10797 ])
10798 });
10799
10800 editor.handle_input("{", window, cx);
10801 editor.handle_input("{", window, cx);
10802 editor.handle_input("_", window, cx);
10803 assert_eq!(
10804 editor.text(cx),
10805 "
10806 a{{_}}
10807 b{{_}}
10808 c{{_}}
10809 "
10810 .unindent()
10811 );
10812 assert_eq!(
10813 editor
10814 .selections
10815 .ranges::<Point>(&editor.display_snapshot(cx)),
10816 [
10817 Point::new(0, 4)..Point::new(0, 4),
10818 Point::new(1, 4)..Point::new(1, 4),
10819 Point::new(2, 4)..Point::new(2, 4)
10820 ]
10821 );
10822
10823 editor.backspace(&Default::default(), window, cx);
10824 editor.backspace(&Default::default(), window, cx);
10825 assert_eq!(
10826 editor.text(cx),
10827 "
10828 a{}
10829 b{}
10830 c{}
10831 "
10832 .unindent()
10833 );
10834 assert_eq!(
10835 editor
10836 .selections
10837 .ranges::<Point>(&editor.display_snapshot(cx)),
10838 [
10839 Point::new(0, 2)..Point::new(0, 2),
10840 Point::new(1, 2)..Point::new(1, 2),
10841 Point::new(2, 2)..Point::new(2, 2)
10842 ]
10843 );
10844
10845 editor.delete_to_previous_word_start(&Default::default(), window, cx);
10846 assert_eq!(
10847 editor.text(cx),
10848 "
10849 a
10850 b
10851 c
10852 "
10853 .unindent()
10854 );
10855 assert_eq!(
10856 editor
10857 .selections
10858 .ranges::<Point>(&editor.display_snapshot(cx)),
10859 [
10860 Point::new(0, 1)..Point::new(0, 1),
10861 Point::new(1, 1)..Point::new(1, 1),
10862 Point::new(2, 1)..Point::new(2, 1)
10863 ]
10864 );
10865 });
10866}
10867
10868#[gpui::test]
10869async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
10870 init_test(cx, |settings| {
10871 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10872 });
10873
10874 let mut cx = EditorTestContext::new(cx).await;
10875
10876 let language = Arc::new(Language::new(
10877 LanguageConfig {
10878 brackets: BracketPairConfig {
10879 pairs: vec![
10880 BracketPair {
10881 start: "{".to_string(),
10882 end: "}".to_string(),
10883 close: true,
10884 surround: true,
10885 newline: true,
10886 },
10887 BracketPair {
10888 start: "(".to_string(),
10889 end: ")".to_string(),
10890 close: true,
10891 surround: true,
10892 newline: true,
10893 },
10894 BracketPair {
10895 start: "[".to_string(),
10896 end: "]".to_string(),
10897 close: false,
10898 surround: true,
10899 newline: true,
10900 },
10901 ],
10902 ..Default::default()
10903 },
10904 autoclose_before: "})]".to_string(),
10905 ..Default::default()
10906 },
10907 Some(tree_sitter_rust::LANGUAGE.into()),
10908 ));
10909
10910 cx.language_registry().add(language.clone());
10911 cx.update_buffer(|buffer, cx| {
10912 buffer.set_language(Some(language), cx);
10913 });
10914
10915 cx.set_state(
10916 &"
10917 {(ˇ)}
10918 [[ˇ]]
10919 {(ˇ)}
10920 "
10921 .unindent(),
10922 );
10923
10924 cx.update_editor(|editor, window, cx| {
10925 editor.backspace(&Default::default(), window, cx);
10926 editor.backspace(&Default::default(), window, cx);
10927 });
10928
10929 cx.assert_editor_state(
10930 &"
10931 ˇ
10932 ˇ]]
10933 ˇ
10934 "
10935 .unindent(),
10936 );
10937
10938 cx.update_editor(|editor, window, cx| {
10939 editor.handle_input("{", window, cx);
10940 editor.handle_input("{", window, cx);
10941 editor.move_right(&MoveRight, window, cx);
10942 editor.move_right(&MoveRight, window, cx);
10943 editor.move_left(&MoveLeft, window, cx);
10944 editor.move_left(&MoveLeft, window, cx);
10945 editor.backspace(&Default::default(), window, cx);
10946 });
10947
10948 cx.assert_editor_state(
10949 &"
10950 {ˇ}
10951 {ˇ}]]
10952 {ˇ}
10953 "
10954 .unindent(),
10955 );
10956
10957 cx.update_editor(|editor, window, cx| {
10958 editor.backspace(&Default::default(), window, cx);
10959 });
10960
10961 cx.assert_editor_state(
10962 &"
10963 ˇ
10964 ˇ]]
10965 ˇ
10966 "
10967 .unindent(),
10968 );
10969}
10970
10971#[gpui::test]
10972async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
10973 init_test(cx, |_| {});
10974
10975 let language = Arc::new(Language::new(
10976 LanguageConfig::default(),
10977 Some(tree_sitter_rust::LANGUAGE.into()),
10978 ));
10979
10980 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
10981 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10982 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10983 editor
10984 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10985 .await;
10986
10987 editor.update_in(cx, |editor, window, cx| {
10988 editor.set_auto_replace_emoji_shortcode(true);
10989
10990 editor.handle_input("Hello ", window, cx);
10991 editor.handle_input(":wave", window, cx);
10992 assert_eq!(editor.text(cx), "Hello :wave".unindent());
10993
10994 editor.handle_input(":", window, cx);
10995 assert_eq!(editor.text(cx), "Hello 👋".unindent());
10996
10997 editor.handle_input(" :smile", window, cx);
10998 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
10999
11000 editor.handle_input(":", window, cx);
11001 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
11002
11003 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
11004 editor.handle_input(":wave", window, cx);
11005 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
11006
11007 editor.handle_input(":", window, cx);
11008 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
11009
11010 editor.handle_input(":1", window, cx);
11011 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
11012
11013 editor.handle_input(":", window, cx);
11014 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
11015
11016 // Ensure shortcode does not get replaced when it is part of a word
11017 editor.handle_input(" Test:wave", window, cx);
11018 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
11019
11020 editor.handle_input(":", window, cx);
11021 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
11022
11023 editor.set_auto_replace_emoji_shortcode(false);
11024
11025 // Ensure shortcode does not get replaced when auto replace is off
11026 editor.handle_input(" :wave", window, cx);
11027 assert_eq!(
11028 editor.text(cx),
11029 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
11030 );
11031
11032 editor.handle_input(":", window, cx);
11033 assert_eq!(
11034 editor.text(cx),
11035 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
11036 );
11037 });
11038}
11039
11040#[gpui::test]
11041async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
11042 init_test(cx, |_| {});
11043
11044 let (text, insertion_ranges) = marked_text_ranges(
11045 indoc! {"
11046 ˇ
11047 "},
11048 false,
11049 );
11050
11051 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
11052 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11053
11054 _ = editor.update_in(cx, |editor, window, cx| {
11055 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
11056
11057 editor
11058 .insert_snippet(&insertion_ranges, snippet, window, cx)
11059 .unwrap();
11060
11061 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
11062 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
11063 assert_eq!(editor.text(cx), expected_text);
11064 assert_eq!(
11065 editor
11066 .selections
11067 .ranges::<usize>(&editor.display_snapshot(cx)),
11068 selection_ranges
11069 );
11070 }
11071
11072 assert(
11073 editor,
11074 cx,
11075 indoc! {"
11076 type «» =•
11077 "},
11078 );
11079
11080 assert!(editor.context_menu_visible(), "There should be a matches");
11081 });
11082}
11083
11084#[gpui::test]
11085async fn test_snippet_tabstop_navigation_with_placeholders(cx: &mut TestAppContext) {
11086 init_test(cx, |_| {});
11087
11088 fn assert_state(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
11089 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
11090 assert_eq!(editor.text(cx), expected_text);
11091 assert_eq!(
11092 editor
11093 .selections
11094 .ranges::<usize>(&editor.display_snapshot(cx)),
11095 selection_ranges
11096 );
11097 }
11098
11099 let (text, insertion_ranges) = marked_text_ranges(
11100 indoc! {"
11101 ˇ
11102 "},
11103 false,
11104 );
11105
11106 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
11107 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11108
11109 _ = editor.update_in(cx, |editor, window, cx| {
11110 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2; $3").unwrap();
11111
11112 editor
11113 .insert_snippet(&insertion_ranges, snippet, window, cx)
11114 .unwrap();
11115
11116 assert_state(
11117 editor,
11118 cx,
11119 indoc! {"
11120 type «» = ;•
11121 "},
11122 );
11123
11124 assert!(
11125 editor.context_menu_visible(),
11126 "Context menu should be visible for placeholder choices"
11127 );
11128
11129 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11130
11131 assert_state(
11132 editor,
11133 cx,
11134 indoc! {"
11135 type = «»;•
11136 "},
11137 );
11138
11139 assert!(
11140 !editor.context_menu_visible(),
11141 "Context menu should be hidden after moving to next tabstop"
11142 );
11143
11144 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11145
11146 assert_state(
11147 editor,
11148 cx,
11149 indoc! {"
11150 type = ; ˇ
11151 "},
11152 );
11153
11154 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11155
11156 assert_state(
11157 editor,
11158 cx,
11159 indoc! {"
11160 type = ; ˇ
11161 "},
11162 );
11163 });
11164
11165 _ = editor.update_in(cx, |editor, window, cx| {
11166 editor.select_all(&SelectAll, window, cx);
11167 editor.backspace(&Backspace, window, cx);
11168
11169 let snippet = Snippet::parse("fn ${1|,foo,bar|} = ${2:value}; $3").unwrap();
11170 let insertion_ranges = editor
11171 .selections
11172 .all(&editor.display_snapshot(cx))
11173 .iter()
11174 .map(|s| s.range())
11175 .collect::<Vec<_>>();
11176
11177 editor
11178 .insert_snippet(&insertion_ranges, snippet, window, cx)
11179 .unwrap();
11180
11181 assert_state(editor, cx, "fn «» = value;•");
11182
11183 assert!(
11184 editor.context_menu_visible(),
11185 "Context menu should be visible for placeholder choices"
11186 );
11187
11188 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11189
11190 assert_state(editor, cx, "fn = «valueˇ»;•");
11191
11192 editor.previous_snippet_tabstop(&PreviousSnippetTabstop, window, cx);
11193
11194 assert_state(editor, cx, "fn «» = value;•");
11195
11196 assert!(
11197 editor.context_menu_visible(),
11198 "Context menu should be visible again after returning to first tabstop"
11199 );
11200
11201 editor.previous_snippet_tabstop(&PreviousSnippetTabstop, window, cx);
11202
11203 assert_state(editor, cx, "fn «» = value;•");
11204 });
11205}
11206
11207#[gpui::test]
11208async fn test_snippets(cx: &mut TestAppContext) {
11209 init_test(cx, |_| {});
11210
11211 let mut cx = EditorTestContext::new(cx).await;
11212
11213 cx.set_state(indoc! {"
11214 a.ˇ b
11215 a.ˇ b
11216 a.ˇ b
11217 "});
11218
11219 cx.update_editor(|editor, window, cx| {
11220 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
11221 let insertion_ranges = editor
11222 .selections
11223 .all(&editor.display_snapshot(cx))
11224 .iter()
11225 .map(|s| s.range())
11226 .collect::<Vec<_>>();
11227 editor
11228 .insert_snippet(&insertion_ranges, snippet, window, cx)
11229 .unwrap();
11230 });
11231
11232 cx.assert_editor_state(indoc! {"
11233 a.f(«oneˇ», two, «threeˇ») b
11234 a.f(«oneˇ», two, «threeˇ») b
11235 a.f(«oneˇ», two, «threeˇ») b
11236 "});
11237
11238 // Can't move earlier than the first tab stop
11239 cx.update_editor(|editor, window, cx| {
11240 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
11241 });
11242 cx.assert_editor_state(indoc! {"
11243 a.f(«oneˇ», two, «threeˇ») b
11244 a.f(«oneˇ», two, «threeˇ») b
11245 a.f(«oneˇ», two, «threeˇ») b
11246 "});
11247
11248 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11249 cx.assert_editor_state(indoc! {"
11250 a.f(one, «twoˇ», three) b
11251 a.f(one, «twoˇ», three) b
11252 a.f(one, «twoˇ», three) b
11253 "});
11254
11255 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
11256 cx.assert_editor_state(indoc! {"
11257 a.f(«oneˇ», two, «threeˇ») b
11258 a.f(«oneˇ», two, «threeˇ») b
11259 a.f(«oneˇ», two, «threeˇ») b
11260 "});
11261
11262 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11263 cx.assert_editor_state(indoc! {"
11264 a.f(one, «twoˇ», three) b
11265 a.f(one, «twoˇ», three) b
11266 a.f(one, «twoˇ», three) b
11267 "});
11268 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11269 cx.assert_editor_state(indoc! {"
11270 a.f(one, two, three)ˇ b
11271 a.f(one, two, three)ˇ b
11272 a.f(one, two, three)ˇ b
11273 "});
11274
11275 // As soon as the last tab stop is reached, snippet state is gone
11276 cx.update_editor(|editor, window, cx| {
11277 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
11278 });
11279 cx.assert_editor_state(indoc! {"
11280 a.f(one, two, three)ˇ b
11281 a.f(one, two, three)ˇ b
11282 a.f(one, two, three)ˇ b
11283 "});
11284}
11285
11286#[gpui::test]
11287async fn test_snippet_indentation(cx: &mut TestAppContext) {
11288 init_test(cx, |_| {});
11289
11290 let mut cx = EditorTestContext::new(cx).await;
11291
11292 cx.update_editor(|editor, window, cx| {
11293 let snippet = Snippet::parse(indoc! {"
11294 /*
11295 * Multiline comment with leading indentation
11296 *
11297 * $1
11298 */
11299 $0"})
11300 .unwrap();
11301 let insertion_ranges = editor
11302 .selections
11303 .all(&editor.display_snapshot(cx))
11304 .iter()
11305 .map(|s| s.range())
11306 .collect::<Vec<_>>();
11307 editor
11308 .insert_snippet(&insertion_ranges, snippet, window, cx)
11309 .unwrap();
11310 });
11311
11312 cx.assert_editor_state(indoc! {"
11313 /*
11314 * Multiline comment with leading indentation
11315 *
11316 * ˇ
11317 */
11318 "});
11319
11320 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11321 cx.assert_editor_state(indoc! {"
11322 /*
11323 * Multiline comment with leading indentation
11324 *
11325 *•
11326 */
11327 ˇ"});
11328}
11329
11330#[gpui::test]
11331async fn test_document_format_during_save(cx: &mut TestAppContext) {
11332 init_test(cx, |_| {});
11333
11334 let fs = FakeFs::new(cx.executor());
11335 fs.insert_file(path!("/file.rs"), Default::default()).await;
11336
11337 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
11338
11339 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11340 language_registry.add(rust_lang());
11341 let mut fake_servers = language_registry.register_fake_lsp(
11342 "Rust",
11343 FakeLspAdapter {
11344 capabilities: lsp::ServerCapabilities {
11345 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11346 ..Default::default()
11347 },
11348 ..Default::default()
11349 },
11350 );
11351
11352 let buffer = project
11353 .update(cx, |project, cx| {
11354 project.open_local_buffer(path!("/file.rs"), cx)
11355 })
11356 .await
11357 .unwrap();
11358
11359 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11360 let (editor, cx) = cx.add_window_view(|window, cx| {
11361 build_editor_with_project(project.clone(), buffer, window, cx)
11362 });
11363 editor.update_in(cx, |editor, window, cx| {
11364 editor.set_text("one\ntwo\nthree\n", window, cx)
11365 });
11366 assert!(cx.read(|cx| editor.is_dirty(cx)));
11367
11368 cx.executor().start_waiting();
11369 let fake_server = fake_servers.next().await.unwrap();
11370
11371 {
11372 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11373 move |params, _| async move {
11374 assert_eq!(
11375 params.text_document.uri,
11376 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11377 );
11378 assert_eq!(params.options.tab_size, 4);
11379 Ok(Some(vec![lsp::TextEdit::new(
11380 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11381 ", ".to_string(),
11382 )]))
11383 },
11384 );
11385 let save = editor
11386 .update_in(cx, |editor, window, cx| {
11387 editor.save(
11388 SaveOptions {
11389 format: true,
11390 autosave: false,
11391 },
11392 project.clone(),
11393 window,
11394 cx,
11395 )
11396 })
11397 .unwrap();
11398 cx.executor().start_waiting();
11399 save.await;
11400
11401 assert_eq!(
11402 editor.update(cx, |editor, cx| editor.text(cx)),
11403 "one, two\nthree\n"
11404 );
11405 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11406 }
11407
11408 {
11409 editor.update_in(cx, |editor, window, cx| {
11410 editor.set_text("one\ntwo\nthree\n", window, cx)
11411 });
11412 assert!(cx.read(|cx| editor.is_dirty(cx)));
11413
11414 // Ensure we can still save even if formatting hangs.
11415 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11416 move |params, _| async move {
11417 assert_eq!(
11418 params.text_document.uri,
11419 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11420 );
11421 futures::future::pending::<()>().await;
11422 unreachable!()
11423 },
11424 );
11425 let save = editor
11426 .update_in(cx, |editor, window, cx| {
11427 editor.save(
11428 SaveOptions {
11429 format: true,
11430 autosave: false,
11431 },
11432 project.clone(),
11433 window,
11434 cx,
11435 )
11436 })
11437 .unwrap();
11438 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11439 cx.executor().start_waiting();
11440 save.await;
11441 assert_eq!(
11442 editor.update(cx, |editor, cx| editor.text(cx)),
11443 "one\ntwo\nthree\n"
11444 );
11445 }
11446
11447 // Set rust language override and assert overridden tabsize is sent to language server
11448 update_test_language_settings(cx, |settings| {
11449 settings.languages.0.insert(
11450 "Rust".into(),
11451 LanguageSettingsContent {
11452 tab_size: NonZeroU32::new(8),
11453 ..Default::default()
11454 },
11455 );
11456 });
11457
11458 {
11459 editor.update_in(cx, |editor, window, cx| {
11460 editor.set_text("somehting_new\n", window, cx)
11461 });
11462 assert!(cx.read(|cx| editor.is_dirty(cx)));
11463 let _formatting_request_signal = fake_server
11464 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
11465 assert_eq!(
11466 params.text_document.uri,
11467 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11468 );
11469 assert_eq!(params.options.tab_size, 8);
11470 Ok(Some(vec![]))
11471 });
11472 let save = editor
11473 .update_in(cx, |editor, window, cx| {
11474 editor.save(
11475 SaveOptions {
11476 format: true,
11477 autosave: false,
11478 },
11479 project.clone(),
11480 window,
11481 cx,
11482 )
11483 })
11484 .unwrap();
11485 cx.executor().start_waiting();
11486 save.await;
11487 }
11488}
11489
11490#[gpui::test]
11491async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
11492 init_test(cx, |settings| {
11493 settings.defaults.ensure_final_newline_on_save = Some(false);
11494 });
11495
11496 let fs = FakeFs::new(cx.executor());
11497 fs.insert_file(path!("/file.txt"), "foo".into()).await;
11498
11499 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
11500
11501 let buffer = project
11502 .update(cx, |project, cx| {
11503 project.open_local_buffer(path!("/file.txt"), cx)
11504 })
11505 .await
11506 .unwrap();
11507
11508 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11509 let (editor, cx) = cx.add_window_view(|window, cx| {
11510 build_editor_with_project(project.clone(), buffer, window, cx)
11511 });
11512 editor.update_in(cx, |editor, window, cx| {
11513 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
11514 s.select_ranges([0..0])
11515 });
11516 });
11517 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11518
11519 editor.update_in(cx, |editor, window, cx| {
11520 editor.handle_input("\n", window, cx)
11521 });
11522 cx.run_until_parked();
11523 save(&editor, &project, cx).await;
11524 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11525
11526 editor.update_in(cx, |editor, window, cx| {
11527 editor.undo(&Default::default(), window, cx);
11528 });
11529 save(&editor, &project, cx).await;
11530 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11531
11532 editor.update_in(cx, |editor, window, cx| {
11533 editor.redo(&Default::default(), window, cx);
11534 });
11535 cx.run_until_parked();
11536 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11537
11538 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
11539 let save = editor
11540 .update_in(cx, |editor, window, cx| {
11541 editor.save(
11542 SaveOptions {
11543 format: true,
11544 autosave: false,
11545 },
11546 project.clone(),
11547 window,
11548 cx,
11549 )
11550 })
11551 .unwrap();
11552 cx.executor().start_waiting();
11553 save.await;
11554 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11555 }
11556}
11557
11558#[gpui::test]
11559async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
11560 init_test(cx, |_| {});
11561
11562 let cols = 4;
11563 let rows = 10;
11564 let sample_text_1 = sample_text(rows, cols, 'a');
11565 assert_eq!(
11566 sample_text_1,
11567 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
11568 );
11569 let sample_text_2 = sample_text(rows, cols, 'l');
11570 assert_eq!(
11571 sample_text_2,
11572 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
11573 );
11574 let sample_text_3 = sample_text(rows, cols, 'v');
11575 assert_eq!(
11576 sample_text_3,
11577 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
11578 );
11579
11580 let fs = FakeFs::new(cx.executor());
11581 fs.insert_tree(
11582 path!("/a"),
11583 json!({
11584 "main.rs": sample_text_1,
11585 "other.rs": sample_text_2,
11586 "lib.rs": sample_text_3,
11587 }),
11588 )
11589 .await;
11590
11591 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11592 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11593 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11594
11595 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11596 language_registry.add(rust_lang());
11597 let mut fake_servers = language_registry.register_fake_lsp(
11598 "Rust",
11599 FakeLspAdapter {
11600 capabilities: lsp::ServerCapabilities {
11601 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11602 ..Default::default()
11603 },
11604 ..Default::default()
11605 },
11606 );
11607
11608 let worktree = project.update(cx, |project, cx| {
11609 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
11610 assert_eq!(worktrees.len(), 1);
11611 worktrees.pop().unwrap()
11612 });
11613 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11614
11615 let buffer_1 = project
11616 .update(cx, |project, cx| {
11617 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
11618 })
11619 .await
11620 .unwrap();
11621 let buffer_2 = project
11622 .update(cx, |project, cx| {
11623 project.open_buffer((worktree_id, rel_path("other.rs")), cx)
11624 })
11625 .await
11626 .unwrap();
11627 let buffer_3 = project
11628 .update(cx, |project, cx| {
11629 project.open_buffer((worktree_id, rel_path("lib.rs")), cx)
11630 })
11631 .await
11632 .unwrap();
11633
11634 let multi_buffer = cx.new(|cx| {
11635 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11636 multi_buffer.push_excerpts(
11637 buffer_1.clone(),
11638 [
11639 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11640 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11641 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11642 ],
11643 cx,
11644 );
11645 multi_buffer.push_excerpts(
11646 buffer_2.clone(),
11647 [
11648 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11649 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11650 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11651 ],
11652 cx,
11653 );
11654 multi_buffer.push_excerpts(
11655 buffer_3.clone(),
11656 [
11657 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11658 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11659 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11660 ],
11661 cx,
11662 );
11663 multi_buffer
11664 });
11665 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
11666 Editor::new(
11667 EditorMode::full(),
11668 multi_buffer,
11669 Some(project.clone()),
11670 window,
11671 cx,
11672 )
11673 });
11674
11675 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11676 editor.change_selections(
11677 SelectionEffects::scroll(Autoscroll::Next),
11678 window,
11679 cx,
11680 |s| s.select_ranges(Some(1..2)),
11681 );
11682 editor.insert("|one|two|three|", window, cx);
11683 });
11684 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11685 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11686 editor.change_selections(
11687 SelectionEffects::scroll(Autoscroll::Next),
11688 window,
11689 cx,
11690 |s| s.select_ranges(Some(60..70)),
11691 );
11692 editor.insert("|four|five|six|", window, cx);
11693 });
11694 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11695
11696 // First two buffers should be edited, but not the third one.
11697 assert_eq!(
11698 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11699 "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}",
11700 );
11701 buffer_1.update(cx, |buffer, _| {
11702 assert!(buffer.is_dirty());
11703 assert_eq!(
11704 buffer.text(),
11705 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
11706 )
11707 });
11708 buffer_2.update(cx, |buffer, _| {
11709 assert!(buffer.is_dirty());
11710 assert_eq!(
11711 buffer.text(),
11712 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
11713 )
11714 });
11715 buffer_3.update(cx, |buffer, _| {
11716 assert!(!buffer.is_dirty());
11717 assert_eq!(buffer.text(), sample_text_3,)
11718 });
11719 cx.executor().run_until_parked();
11720
11721 cx.executor().start_waiting();
11722 let save = multi_buffer_editor
11723 .update_in(cx, |editor, window, cx| {
11724 editor.save(
11725 SaveOptions {
11726 format: true,
11727 autosave: false,
11728 },
11729 project.clone(),
11730 window,
11731 cx,
11732 )
11733 })
11734 .unwrap();
11735
11736 let fake_server = fake_servers.next().await.unwrap();
11737 fake_server
11738 .server
11739 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
11740 Ok(Some(vec![lsp::TextEdit::new(
11741 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11742 format!("[{} formatted]", params.text_document.uri),
11743 )]))
11744 })
11745 .detach();
11746 save.await;
11747
11748 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
11749 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
11750 assert_eq!(
11751 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11752 uri!(
11753 "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}"
11754 ),
11755 );
11756 buffer_1.update(cx, |buffer, _| {
11757 assert!(!buffer.is_dirty());
11758 assert_eq!(
11759 buffer.text(),
11760 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
11761 )
11762 });
11763 buffer_2.update(cx, |buffer, _| {
11764 assert!(!buffer.is_dirty());
11765 assert_eq!(
11766 buffer.text(),
11767 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
11768 )
11769 });
11770 buffer_3.update(cx, |buffer, _| {
11771 assert!(!buffer.is_dirty());
11772 assert_eq!(buffer.text(), sample_text_3,)
11773 });
11774}
11775
11776#[gpui::test]
11777async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
11778 init_test(cx, |_| {});
11779
11780 let fs = FakeFs::new(cx.executor());
11781 fs.insert_tree(
11782 path!("/dir"),
11783 json!({
11784 "file1.rs": "fn main() { println!(\"hello\"); }",
11785 "file2.rs": "fn test() { println!(\"test\"); }",
11786 "file3.rs": "fn other() { println!(\"other\"); }\n",
11787 }),
11788 )
11789 .await;
11790
11791 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
11792 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11793 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11794
11795 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11796 language_registry.add(rust_lang());
11797
11798 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
11799 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11800
11801 // Open three buffers
11802 let buffer_1 = project
11803 .update(cx, |project, cx| {
11804 project.open_buffer((worktree_id, rel_path("file1.rs")), cx)
11805 })
11806 .await
11807 .unwrap();
11808 let buffer_2 = project
11809 .update(cx, |project, cx| {
11810 project.open_buffer((worktree_id, rel_path("file2.rs")), cx)
11811 })
11812 .await
11813 .unwrap();
11814 let buffer_3 = project
11815 .update(cx, |project, cx| {
11816 project.open_buffer((worktree_id, rel_path("file3.rs")), cx)
11817 })
11818 .await
11819 .unwrap();
11820
11821 // Create a multi-buffer with all three buffers
11822 let multi_buffer = cx.new(|cx| {
11823 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11824 multi_buffer.push_excerpts(
11825 buffer_1.clone(),
11826 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11827 cx,
11828 );
11829 multi_buffer.push_excerpts(
11830 buffer_2.clone(),
11831 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11832 cx,
11833 );
11834 multi_buffer.push_excerpts(
11835 buffer_3.clone(),
11836 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11837 cx,
11838 );
11839 multi_buffer
11840 });
11841
11842 let editor = cx.new_window_entity(|window, cx| {
11843 Editor::new(
11844 EditorMode::full(),
11845 multi_buffer,
11846 Some(project.clone()),
11847 window,
11848 cx,
11849 )
11850 });
11851
11852 // Edit only the first buffer
11853 editor.update_in(cx, |editor, window, cx| {
11854 editor.change_selections(
11855 SelectionEffects::scroll(Autoscroll::Next),
11856 window,
11857 cx,
11858 |s| s.select_ranges(Some(10..10)),
11859 );
11860 editor.insert("// edited", window, cx);
11861 });
11862
11863 // Verify that only buffer 1 is dirty
11864 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
11865 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11866 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11867
11868 // Get write counts after file creation (files were created with initial content)
11869 // We expect each file to have been written once during creation
11870 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
11871 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
11872 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
11873
11874 // Perform autosave
11875 let save_task = editor.update_in(cx, |editor, window, cx| {
11876 editor.save(
11877 SaveOptions {
11878 format: true,
11879 autosave: true,
11880 },
11881 project.clone(),
11882 window,
11883 cx,
11884 )
11885 });
11886 save_task.await.unwrap();
11887
11888 // Only the dirty buffer should have been saved
11889 assert_eq!(
11890 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11891 1,
11892 "Buffer 1 was dirty, so it should have been written once during autosave"
11893 );
11894 assert_eq!(
11895 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11896 0,
11897 "Buffer 2 was clean, so it should not have been written during autosave"
11898 );
11899 assert_eq!(
11900 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11901 0,
11902 "Buffer 3 was clean, so it should not have been written during autosave"
11903 );
11904
11905 // Verify buffer states after autosave
11906 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11907 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11908 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11909
11910 // Now perform a manual save (format = true)
11911 let save_task = editor.update_in(cx, |editor, window, cx| {
11912 editor.save(
11913 SaveOptions {
11914 format: true,
11915 autosave: false,
11916 },
11917 project.clone(),
11918 window,
11919 cx,
11920 )
11921 });
11922 save_task.await.unwrap();
11923
11924 // During manual save, clean buffers don't get written to disk
11925 // They just get did_save called for language server notifications
11926 assert_eq!(
11927 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11928 1,
11929 "Buffer 1 should only have been written once total (during autosave, not manual save)"
11930 );
11931 assert_eq!(
11932 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11933 0,
11934 "Buffer 2 should not have been written at all"
11935 );
11936 assert_eq!(
11937 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11938 0,
11939 "Buffer 3 should not have been written at all"
11940 );
11941}
11942
11943async fn setup_range_format_test(
11944 cx: &mut TestAppContext,
11945) -> (
11946 Entity<Project>,
11947 Entity<Editor>,
11948 &mut gpui::VisualTestContext,
11949 lsp::FakeLanguageServer,
11950) {
11951 init_test(cx, |_| {});
11952
11953 let fs = FakeFs::new(cx.executor());
11954 fs.insert_file(path!("/file.rs"), Default::default()).await;
11955
11956 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11957
11958 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11959 language_registry.add(rust_lang());
11960 let mut fake_servers = language_registry.register_fake_lsp(
11961 "Rust",
11962 FakeLspAdapter {
11963 capabilities: lsp::ServerCapabilities {
11964 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
11965 ..lsp::ServerCapabilities::default()
11966 },
11967 ..FakeLspAdapter::default()
11968 },
11969 );
11970
11971 let buffer = project
11972 .update(cx, |project, cx| {
11973 project.open_local_buffer(path!("/file.rs"), cx)
11974 })
11975 .await
11976 .unwrap();
11977
11978 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11979 let (editor, cx) = cx.add_window_view(|window, cx| {
11980 build_editor_with_project(project.clone(), buffer, window, cx)
11981 });
11982
11983 cx.executor().start_waiting();
11984 let fake_server = fake_servers.next().await.unwrap();
11985
11986 (project, editor, cx, fake_server)
11987}
11988
11989#[gpui::test]
11990async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
11991 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11992
11993 editor.update_in(cx, |editor, window, cx| {
11994 editor.set_text("one\ntwo\nthree\n", window, cx)
11995 });
11996 assert!(cx.read(|cx| editor.is_dirty(cx)));
11997
11998 let save = editor
11999 .update_in(cx, |editor, window, cx| {
12000 editor.save(
12001 SaveOptions {
12002 format: true,
12003 autosave: false,
12004 },
12005 project.clone(),
12006 window,
12007 cx,
12008 )
12009 })
12010 .unwrap();
12011 fake_server
12012 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
12013 assert_eq!(
12014 params.text_document.uri,
12015 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12016 );
12017 assert_eq!(params.options.tab_size, 4);
12018 Ok(Some(vec![lsp::TextEdit::new(
12019 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12020 ", ".to_string(),
12021 )]))
12022 })
12023 .next()
12024 .await;
12025 cx.executor().start_waiting();
12026 save.await;
12027 assert_eq!(
12028 editor.update(cx, |editor, cx| editor.text(cx)),
12029 "one, two\nthree\n"
12030 );
12031 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12032}
12033
12034#[gpui::test]
12035async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
12036 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12037
12038 editor.update_in(cx, |editor, window, cx| {
12039 editor.set_text("one\ntwo\nthree\n", window, cx)
12040 });
12041 assert!(cx.read(|cx| editor.is_dirty(cx)));
12042
12043 // Test that save still works when formatting hangs
12044 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
12045 move |params, _| async move {
12046 assert_eq!(
12047 params.text_document.uri,
12048 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12049 );
12050 futures::future::pending::<()>().await;
12051 unreachable!()
12052 },
12053 );
12054 let save = editor
12055 .update_in(cx, |editor, window, cx| {
12056 editor.save(
12057 SaveOptions {
12058 format: true,
12059 autosave: false,
12060 },
12061 project.clone(),
12062 window,
12063 cx,
12064 )
12065 })
12066 .unwrap();
12067 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
12068 cx.executor().start_waiting();
12069 save.await;
12070 assert_eq!(
12071 editor.update(cx, |editor, cx| editor.text(cx)),
12072 "one\ntwo\nthree\n"
12073 );
12074 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12075}
12076
12077#[gpui::test]
12078async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
12079 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12080
12081 // Buffer starts clean, no formatting should be requested
12082 let save = editor
12083 .update_in(cx, |editor, window, cx| {
12084 editor.save(
12085 SaveOptions {
12086 format: false,
12087 autosave: false,
12088 },
12089 project.clone(),
12090 window,
12091 cx,
12092 )
12093 })
12094 .unwrap();
12095 let _pending_format_request = fake_server
12096 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
12097 panic!("Should not be invoked");
12098 })
12099 .next();
12100 cx.executor().start_waiting();
12101 save.await;
12102 cx.run_until_parked();
12103}
12104
12105#[gpui::test]
12106async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
12107 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12108
12109 // Set Rust language override and assert overridden tabsize is sent to language server
12110 update_test_language_settings(cx, |settings| {
12111 settings.languages.0.insert(
12112 "Rust".into(),
12113 LanguageSettingsContent {
12114 tab_size: NonZeroU32::new(8),
12115 ..Default::default()
12116 },
12117 );
12118 });
12119
12120 editor.update_in(cx, |editor, window, cx| {
12121 editor.set_text("something_new\n", window, cx)
12122 });
12123 assert!(cx.read(|cx| editor.is_dirty(cx)));
12124 let save = editor
12125 .update_in(cx, |editor, window, cx| {
12126 editor.save(
12127 SaveOptions {
12128 format: true,
12129 autosave: false,
12130 },
12131 project.clone(),
12132 window,
12133 cx,
12134 )
12135 })
12136 .unwrap();
12137 fake_server
12138 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
12139 assert_eq!(
12140 params.text_document.uri,
12141 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12142 );
12143 assert_eq!(params.options.tab_size, 8);
12144 Ok(Some(Vec::new()))
12145 })
12146 .next()
12147 .await;
12148 save.await;
12149}
12150
12151#[gpui::test]
12152async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
12153 init_test(cx, |settings| {
12154 settings.defaults.formatter = Some(FormatterList::Single(Formatter::LanguageServer(
12155 settings::LanguageServerFormatterSpecifier::Current,
12156 )))
12157 });
12158
12159 let fs = FakeFs::new(cx.executor());
12160 fs.insert_file(path!("/file.rs"), Default::default()).await;
12161
12162 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12163
12164 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12165 language_registry.add(Arc::new(Language::new(
12166 LanguageConfig {
12167 name: "Rust".into(),
12168 matcher: LanguageMatcher {
12169 path_suffixes: vec!["rs".to_string()],
12170 ..Default::default()
12171 },
12172 ..LanguageConfig::default()
12173 },
12174 Some(tree_sitter_rust::LANGUAGE.into()),
12175 )));
12176 update_test_language_settings(cx, |settings| {
12177 // Enable Prettier formatting for the same buffer, and ensure
12178 // LSP is called instead of Prettier.
12179 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
12180 });
12181 let mut fake_servers = language_registry.register_fake_lsp(
12182 "Rust",
12183 FakeLspAdapter {
12184 capabilities: lsp::ServerCapabilities {
12185 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12186 ..Default::default()
12187 },
12188 ..Default::default()
12189 },
12190 );
12191
12192 let buffer = project
12193 .update(cx, |project, cx| {
12194 project.open_local_buffer(path!("/file.rs"), cx)
12195 })
12196 .await
12197 .unwrap();
12198
12199 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12200 let (editor, cx) = cx.add_window_view(|window, cx| {
12201 build_editor_with_project(project.clone(), buffer, window, cx)
12202 });
12203 editor.update_in(cx, |editor, window, cx| {
12204 editor.set_text("one\ntwo\nthree\n", window, cx)
12205 });
12206
12207 cx.executor().start_waiting();
12208 let fake_server = fake_servers.next().await.unwrap();
12209
12210 let format = editor
12211 .update_in(cx, |editor, window, cx| {
12212 editor.perform_format(
12213 project.clone(),
12214 FormatTrigger::Manual,
12215 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12216 window,
12217 cx,
12218 )
12219 })
12220 .unwrap();
12221 fake_server
12222 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
12223 assert_eq!(
12224 params.text_document.uri,
12225 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12226 );
12227 assert_eq!(params.options.tab_size, 4);
12228 Ok(Some(vec![lsp::TextEdit::new(
12229 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12230 ", ".to_string(),
12231 )]))
12232 })
12233 .next()
12234 .await;
12235 cx.executor().start_waiting();
12236 format.await;
12237 assert_eq!(
12238 editor.update(cx, |editor, cx| editor.text(cx)),
12239 "one, two\nthree\n"
12240 );
12241
12242 editor.update_in(cx, |editor, window, cx| {
12243 editor.set_text("one\ntwo\nthree\n", window, cx)
12244 });
12245 // Ensure we don't lock if formatting hangs.
12246 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12247 move |params, _| async move {
12248 assert_eq!(
12249 params.text_document.uri,
12250 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12251 );
12252 futures::future::pending::<()>().await;
12253 unreachable!()
12254 },
12255 );
12256 let format = editor
12257 .update_in(cx, |editor, window, cx| {
12258 editor.perform_format(
12259 project,
12260 FormatTrigger::Manual,
12261 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12262 window,
12263 cx,
12264 )
12265 })
12266 .unwrap();
12267 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
12268 cx.executor().start_waiting();
12269 format.await;
12270 assert_eq!(
12271 editor.update(cx, |editor, cx| editor.text(cx)),
12272 "one\ntwo\nthree\n"
12273 );
12274}
12275
12276#[gpui::test]
12277async fn test_multiple_formatters(cx: &mut TestAppContext) {
12278 init_test(cx, |settings| {
12279 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
12280 settings.defaults.formatter = Some(FormatterList::Vec(vec![
12281 Formatter::LanguageServer(settings::LanguageServerFormatterSpecifier::Current),
12282 Formatter::CodeAction("code-action-1".into()),
12283 Formatter::CodeAction("code-action-2".into()),
12284 ]))
12285 });
12286
12287 let fs = FakeFs::new(cx.executor());
12288 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
12289 .await;
12290
12291 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12292 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12293 language_registry.add(rust_lang());
12294
12295 let mut fake_servers = language_registry.register_fake_lsp(
12296 "Rust",
12297 FakeLspAdapter {
12298 capabilities: lsp::ServerCapabilities {
12299 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12300 execute_command_provider: Some(lsp::ExecuteCommandOptions {
12301 commands: vec!["the-command-for-code-action-1".into()],
12302 ..Default::default()
12303 }),
12304 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12305 ..Default::default()
12306 },
12307 ..Default::default()
12308 },
12309 );
12310
12311 let buffer = project
12312 .update(cx, |project, cx| {
12313 project.open_local_buffer(path!("/file.rs"), cx)
12314 })
12315 .await
12316 .unwrap();
12317
12318 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12319 let (editor, cx) = cx.add_window_view(|window, cx| {
12320 build_editor_with_project(project.clone(), buffer, window, cx)
12321 });
12322
12323 cx.executor().start_waiting();
12324
12325 let fake_server = fake_servers.next().await.unwrap();
12326 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12327 move |_params, _| async move {
12328 Ok(Some(vec![lsp::TextEdit::new(
12329 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12330 "applied-formatting\n".to_string(),
12331 )]))
12332 },
12333 );
12334 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12335 move |params, _| async move {
12336 let requested_code_actions = params.context.only.expect("Expected code action request");
12337 assert_eq!(requested_code_actions.len(), 1);
12338
12339 let uri = lsp::Uri::from_file_path(path!("/file.rs")).unwrap();
12340 let code_action = match requested_code_actions[0].as_str() {
12341 "code-action-1" => lsp::CodeAction {
12342 kind: Some("code-action-1".into()),
12343 edit: Some(lsp::WorkspaceEdit::new(
12344 [(
12345 uri,
12346 vec![lsp::TextEdit::new(
12347 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12348 "applied-code-action-1-edit\n".to_string(),
12349 )],
12350 )]
12351 .into_iter()
12352 .collect(),
12353 )),
12354 command: Some(lsp::Command {
12355 command: "the-command-for-code-action-1".into(),
12356 ..Default::default()
12357 }),
12358 ..Default::default()
12359 },
12360 "code-action-2" => lsp::CodeAction {
12361 kind: Some("code-action-2".into()),
12362 edit: Some(lsp::WorkspaceEdit::new(
12363 [(
12364 uri,
12365 vec![lsp::TextEdit::new(
12366 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12367 "applied-code-action-2-edit\n".to_string(),
12368 )],
12369 )]
12370 .into_iter()
12371 .collect(),
12372 )),
12373 ..Default::default()
12374 },
12375 req => panic!("Unexpected code action request: {:?}", req),
12376 };
12377 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12378 code_action,
12379 )]))
12380 },
12381 );
12382
12383 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
12384 move |params, _| async move { Ok(params) }
12385 });
12386
12387 let command_lock = Arc::new(futures::lock::Mutex::new(()));
12388 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
12389 let fake = fake_server.clone();
12390 let lock = command_lock.clone();
12391 move |params, _| {
12392 assert_eq!(params.command, "the-command-for-code-action-1");
12393 let fake = fake.clone();
12394 let lock = lock.clone();
12395 async move {
12396 lock.lock().await;
12397 fake.server
12398 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
12399 label: None,
12400 edit: lsp::WorkspaceEdit {
12401 changes: Some(
12402 [(
12403 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
12404 vec![lsp::TextEdit {
12405 range: lsp::Range::new(
12406 lsp::Position::new(0, 0),
12407 lsp::Position::new(0, 0),
12408 ),
12409 new_text: "applied-code-action-1-command\n".into(),
12410 }],
12411 )]
12412 .into_iter()
12413 .collect(),
12414 ),
12415 ..Default::default()
12416 },
12417 })
12418 .await
12419 .into_response()
12420 .unwrap();
12421 Ok(Some(json!(null)))
12422 }
12423 }
12424 });
12425
12426 cx.executor().start_waiting();
12427 editor
12428 .update_in(cx, |editor, window, cx| {
12429 editor.perform_format(
12430 project.clone(),
12431 FormatTrigger::Manual,
12432 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12433 window,
12434 cx,
12435 )
12436 })
12437 .unwrap()
12438 .await;
12439 editor.update(cx, |editor, cx| {
12440 assert_eq!(
12441 editor.text(cx),
12442 r#"
12443 applied-code-action-2-edit
12444 applied-code-action-1-command
12445 applied-code-action-1-edit
12446 applied-formatting
12447 one
12448 two
12449 three
12450 "#
12451 .unindent()
12452 );
12453 });
12454
12455 editor.update_in(cx, |editor, window, cx| {
12456 editor.undo(&Default::default(), window, cx);
12457 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12458 });
12459
12460 // Perform a manual edit while waiting for an LSP command
12461 // that's being run as part of a formatting code action.
12462 let lock_guard = command_lock.lock().await;
12463 let format = editor
12464 .update_in(cx, |editor, window, cx| {
12465 editor.perform_format(
12466 project.clone(),
12467 FormatTrigger::Manual,
12468 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12469 window,
12470 cx,
12471 )
12472 })
12473 .unwrap();
12474 cx.run_until_parked();
12475 editor.update(cx, |editor, cx| {
12476 assert_eq!(
12477 editor.text(cx),
12478 r#"
12479 applied-code-action-1-edit
12480 applied-formatting
12481 one
12482 two
12483 three
12484 "#
12485 .unindent()
12486 );
12487
12488 editor.buffer.update(cx, |buffer, cx| {
12489 let ix = buffer.len(cx);
12490 buffer.edit([(ix..ix, "edited\n")], None, cx);
12491 });
12492 });
12493
12494 // Allow the LSP command to proceed. Because the buffer was edited,
12495 // the second code action will not be run.
12496 drop(lock_guard);
12497 format.await;
12498 editor.update_in(cx, |editor, window, cx| {
12499 assert_eq!(
12500 editor.text(cx),
12501 r#"
12502 applied-code-action-1-command
12503 applied-code-action-1-edit
12504 applied-formatting
12505 one
12506 two
12507 three
12508 edited
12509 "#
12510 .unindent()
12511 );
12512
12513 // The manual edit is undone first, because it is the last thing the user did
12514 // (even though the command completed afterwards).
12515 editor.undo(&Default::default(), window, cx);
12516 assert_eq!(
12517 editor.text(cx),
12518 r#"
12519 applied-code-action-1-command
12520 applied-code-action-1-edit
12521 applied-formatting
12522 one
12523 two
12524 three
12525 "#
12526 .unindent()
12527 );
12528
12529 // All the formatting (including the command, which completed after the manual edit)
12530 // is undone together.
12531 editor.undo(&Default::default(), window, cx);
12532 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12533 });
12534}
12535
12536#[gpui::test]
12537async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
12538 init_test(cx, |settings| {
12539 settings.defaults.formatter = Some(FormatterList::Vec(vec![Formatter::LanguageServer(
12540 settings::LanguageServerFormatterSpecifier::Current,
12541 )]))
12542 });
12543
12544 let fs = FakeFs::new(cx.executor());
12545 fs.insert_file(path!("/file.ts"), Default::default()).await;
12546
12547 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12548
12549 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12550 language_registry.add(Arc::new(Language::new(
12551 LanguageConfig {
12552 name: "TypeScript".into(),
12553 matcher: LanguageMatcher {
12554 path_suffixes: vec!["ts".to_string()],
12555 ..Default::default()
12556 },
12557 ..LanguageConfig::default()
12558 },
12559 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12560 )));
12561 update_test_language_settings(cx, |settings| {
12562 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
12563 });
12564 let mut fake_servers = language_registry.register_fake_lsp(
12565 "TypeScript",
12566 FakeLspAdapter {
12567 capabilities: lsp::ServerCapabilities {
12568 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12569 ..Default::default()
12570 },
12571 ..Default::default()
12572 },
12573 );
12574
12575 let buffer = project
12576 .update(cx, |project, cx| {
12577 project.open_local_buffer(path!("/file.ts"), cx)
12578 })
12579 .await
12580 .unwrap();
12581
12582 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12583 let (editor, cx) = cx.add_window_view(|window, cx| {
12584 build_editor_with_project(project.clone(), buffer, window, cx)
12585 });
12586 editor.update_in(cx, |editor, window, cx| {
12587 editor.set_text(
12588 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12589 window,
12590 cx,
12591 )
12592 });
12593
12594 cx.executor().start_waiting();
12595 let fake_server = fake_servers.next().await.unwrap();
12596
12597 let format = editor
12598 .update_in(cx, |editor, window, cx| {
12599 editor.perform_code_action_kind(
12600 project.clone(),
12601 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12602 window,
12603 cx,
12604 )
12605 })
12606 .unwrap();
12607 fake_server
12608 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
12609 assert_eq!(
12610 params.text_document.uri,
12611 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12612 );
12613 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12614 lsp::CodeAction {
12615 title: "Organize Imports".to_string(),
12616 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
12617 edit: Some(lsp::WorkspaceEdit {
12618 changes: Some(
12619 [(
12620 params.text_document.uri.clone(),
12621 vec![lsp::TextEdit::new(
12622 lsp::Range::new(
12623 lsp::Position::new(1, 0),
12624 lsp::Position::new(2, 0),
12625 ),
12626 "".to_string(),
12627 )],
12628 )]
12629 .into_iter()
12630 .collect(),
12631 ),
12632 ..Default::default()
12633 }),
12634 ..Default::default()
12635 },
12636 )]))
12637 })
12638 .next()
12639 .await;
12640 cx.executor().start_waiting();
12641 format.await;
12642 assert_eq!(
12643 editor.update(cx, |editor, cx| editor.text(cx)),
12644 "import { a } from 'module';\n\nconst x = a;\n"
12645 );
12646
12647 editor.update_in(cx, |editor, window, cx| {
12648 editor.set_text(
12649 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12650 window,
12651 cx,
12652 )
12653 });
12654 // Ensure we don't lock if code action hangs.
12655 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12656 move |params, _| async move {
12657 assert_eq!(
12658 params.text_document.uri,
12659 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12660 );
12661 futures::future::pending::<()>().await;
12662 unreachable!()
12663 },
12664 );
12665 let format = editor
12666 .update_in(cx, |editor, window, cx| {
12667 editor.perform_code_action_kind(
12668 project,
12669 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12670 window,
12671 cx,
12672 )
12673 })
12674 .unwrap();
12675 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
12676 cx.executor().start_waiting();
12677 format.await;
12678 assert_eq!(
12679 editor.update(cx, |editor, cx| editor.text(cx)),
12680 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
12681 );
12682}
12683
12684#[gpui::test]
12685async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
12686 init_test(cx, |_| {});
12687
12688 let mut cx = EditorLspTestContext::new_rust(
12689 lsp::ServerCapabilities {
12690 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12691 ..Default::default()
12692 },
12693 cx,
12694 )
12695 .await;
12696
12697 cx.set_state(indoc! {"
12698 one.twoˇ
12699 "});
12700
12701 // The format request takes a long time. When it completes, it inserts
12702 // a newline and an indent before the `.`
12703 cx.lsp
12704 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
12705 let executor = cx.background_executor().clone();
12706 async move {
12707 executor.timer(Duration::from_millis(100)).await;
12708 Ok(Some(vec![lsp::TextEdit {
12709 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
12710 new_text: "\n ".into(),
12711 }]))
12712 }
12713 });
12714
12715 // Submit a format request.
12716 let format_1 = cx
12717 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12718 .unwrap();
12719 cx.executor().run_until_parked();
12720
12721 // Submit a second format request.
12722 let format_2 = cx
12723 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12724 .unwrap();
12725 cx.executor().run_until_parked();
12726
12727 // Wait for both format requests to complete
12728 cx.executor().advance_clock(Duration::from_millis(200));
12729 cx.executor().start_waiting();
12730 format_1.await.unwrap();
12731 cx.executor().start_waiting();
12732 format_2.await.unwrap();
12733
12734 // The formatting edits only happens once.
12735 cx.assert_editor_state(indoc! {"
12736 one
12737 .twoˇ
12738 "});
12739}
12740
12741#[gpui::test]
12742async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
12743 init_test(cx, |settings| {
12744 settings.defaults.formatter = Some(FormatterList::default())
12745 });
12746
12747 let mut cx = EditorLspTestContext::new_rust(
12748 lsp::ServerCapabilities {
12749 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12750 ..Default::default()
12751 },
12752 cx,
12753 )
12754 .await;
12755
12756 // Record which buffer changes have been sent to the language server
12757 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
12758 cx.lsp
12759 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
12760 let buffer_changes = buffer_changes.clone();
12761 move |params, _| {
12762 buffer_changes.lock().extend(
12763 params
12764 .content_changes
12765 .into_iter()
12766 .map(|e| (e.range.unwrap(), e.text)),
12767 );
12768 }
12769 });
12770 // Handle formatting requests to the language server.
12771 cx.lsp
12772 .set_request_handler::<lsp::request::Formatting, _, _>({
12773 let buffer_changes = buffer_changes.clone();
12774 move |_, _| {
12775 let buffer_changes = buffer_changes.clone();
12776 // Insert blank lines between each line of the buffer.
12777 async move {
12778 // When formatting is requested, trailing whitespace has already been stripped,
12779 // and the trailing newline has already been added.
12780 assert_eq!(
12781 &buffer_changes.lock()[1..],
12782 &[
12783 (
12784 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
12785 "".into()
12786 ),
12787 (
12788 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
12789 "".into()
12790 ),
12791 (
12792 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
12793 "\n".into()
12794 ),
12795 ]
12796 );
12797
12798 Ok(Some(vec![
12799 lsp::TextEdit {
12800 range: lsp::Range::new(
12801 lsp::Position::new(1, 0),
12802 lsp::Position::new(1, 0),
12803 ),
12804 new_text: "\n".into(),
12805 },
12806 lsp::TextEdit {
12807 range: lsp::Range::new(
12808 lsp::Position::new(2, 0),
12809 lsp::Position::new(2, 0),
12810 ),
12811 new_text: "\n".into(),
12812 },
12813 ]))
12814 }
12815 }
12816 });
12817
12818 // Set up a buffer white some trailing whitespace and no trailing newline.
12819 cx.set_state(
12820 &[
12821 "one ", //
12822 "twoˇ", //
12823 "three ", //
12824 "four", //
12825 ]
12826 .join("\n"),
12827 );
12828 cx.run_until_parked();
12829
12830 // Submit a format request.
12831 let format = cx
12832 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12833 .unwrap();
12834
12835 cx.run_until_parked();
12836 // After formatting the buffer, the trailing whitespace is stripped,
12837 // a newline is appended, and the edits provided by the language server
12838 // have been applied.
12839 format.await.unwrap();
12840
12841 cx.assert_editor_state(
12842 &[
12843 "one", //
12844 "", //
12845 "twoˇ", //
12846 "", //
12847 "three", //
12848 "four", //
12849 "", //
12850 ]
12851 .join("\n"),
12852 );
12853
12854 // Undoing the formatting undoes the trailing whitespace removal, the
12855 // trailing newline, and the LSP edits.
12856 cx.update_buffer(|buffer, cx| buffer.undo(cx));
12857 cx.assert_editor_state(
12858 &[
12859 "one ", //
12860 "twoˇ", //
12861 "three ", //
12862 "four", //
12863 ]
12864 .join("\n"),
12865 );
12866}
12867
12868#[gpui::test]
12869async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
12870 cx: &mut TestAppContext,
12871) {
12872 init_test(cx, |_| {});
12873
12874 cx.update(|cx| {
12875 cx.update_global::<SettingsStore, _>(|settings, cx| {
12876 settings.update_user_settings(cx, |settings| {
12877 settings.editor.auto_signature_help = Some(true);
12878 });
12879 });
12880 });
12881
12882 let mut cx = EditorLspTestContext::new_rust(
12883 lsp::ServerCapabilities {
12884 signature_help_provider: Some(lsp::SignatureHelpOptions {
12885 ..Default::default()
12886 }),
12887 ..Default::default()
12888 },
12889 cx,
12890 )
12891 .await;
12892
12893 let language = Language::new(
12894 LanguageConfig {
12895 name: "Rust".into(),
12896 brackets: BracketPairConfig {
12897 pairs: vec![
12898 BracketPair {
12899 start: "{".to_string(),
12900 end: "}".to_string(),
12901 close: true,
12902 surround: true,
12903 newline: true,
12904 },
12905 BracketPair {
12906 start: "(".to_string(),
12907 end: ")".to_string(),
12908 close: true,
12909 surround: true,
12910 newline: true,
12911 },
12912 BracketPair {
12913 start: "/*".to_string(),
12914 end: " */".to_string(),
12915 close: true,
12916 surround: true,
12917 newline: true,
12918 },
12919 BracketPair {
12920 start: "[".to_string(),
12921 end: "]".to_string(),
12922 close: false,
12923 surround: false,
12924 newline: true,
12925 },
12926 BracketPair {
12927 start: "\"".to_string(),
12928 end: "\"".to_string(),
12929 close: true,
12930 surround: true,
12931 newline: false,
12932 },
12933 BracketPair {
12934 start: "<".to_string(),
12935 end: ">".to_string(),
12936 close: false,
12937 surround: true,
12938 newline: true,
12939 },
12940 ],
12941 ..Default::default()
12942 },
12943 autoclose_before: "})]".to_string(),
12944 ..Default::default()
12945 },
12946 Some(tree_sitter_rust::LANGUAGE.into()),
12947 );
12948 let language = Arc::new(language);
12949
12950 cx.language_registry().add(language.clone());
12951 cx.update_buffer(|buffer, cx| {
12952 buffer.set_language(Some(language), cx);
12953 });
12954
12955 cx.set_state(
12956 &r#"
12957 fn main() {
12958 sampleˇ
12959 }
12960 "#
12961 .unindent(),
12962 );
12963
12964 cx.update_editor(|editor, window, cx| {
12965 editor.handle_input("(", window, cx);
12966 });
12967 cx.assert_editor_state(
12968 &"
12969 fn main() {
12970 sample(ˇ)
12971 }
12972 "
12973 .unindent(),
12974 );
12975
12976 let mocked_response = lsp::SignatureHelp {
12977 signatures: vec![lsp::SignatureInformation {
12978 label: "fn sample(param1: u8, param2: u8)".to_string(),
12979 documentation: None,
12980 parameters: Some(vec![
12981 lsp::ParameterInformation {
12982 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12983 documentation: None,
12984 },
12985 lsp::ParameterInformation {
12986 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12987 documentation: None,
12988 },
12989 ]),
12990 active_parameter: None,
12991 }],
12992 active_signature: Some(0),
12993 active_parameter: Some(0),
12994 };
12995 handle_signature_help_request(&mut cx, mocked_response).await;
12996
12997 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12998 .await;
12999
13000 cx.editor(|editor, _, _| {
13001 let signature_help_state = editor.signature_help_state.popover().cloned();
13002 let signature = signature_help_state.unwrap();
13003 assert_eq!(
13004 signature.signatures[signature.current_signature].label,
13005 "fn sample(param1: u8, param2: u8)"
13006 );
13007 });
13008}
13009
13010#[gpui::test]
13011async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
13012 init_test(cx, |_| {});
13013
13014 cx.update(|cx| {
13015 cx.update_global::<SettingsStore, _>(|settings, cx| {
13016 settings.update_user_settings(cx, |settings| {
13017 settings.editor.auto_signature_help = Some(false);
13018 settings.editor.show_signature_help_after_edits = Some(false);
13019 });
13020 });
13021 });
13022
13023 let mut cx = EditorLspTestContext::new_rust(
13024 lsp::ServerCapabilities {
13025 signature_help_provider: Some(lsp::SignatureHelpOptions {
13026 ..Default::default()
13027 }),
13028 ..Default::default()
13029 },
13030 cx,
13031 )
13032 .await;
13033
13034 let language = Language::new(
13035 LanguageConfig {
13036 name: "Rust".into(),
13037 brackets: BracketPairConfig {
13038 pairs: vec![
13039 BracketPair {
13040 start: "{".to_string(),
13041 end: "}".to_string(),
13042 close: true,
13043 surround: true,
13044 newline: true,
13045 },
13046 BracketPair {
13047 start: "(".to_string(),
13048 end: ")".to_string(),
13049 close: true,
13050 surround: true,
13051 newline: true,
13052 },
13053 BracketPair {
13054 start: "/*".to_string(),
13055 end: " */".to_string(),
13056 close: true,
13057 surround: true,
13058 newline: true,
13059 },
13060 BracketPair {
13061 start: "[".to_string(),
13062 end: "]".to_string(),
13063 close: false,
13064 surround: false,
13065 newline: true,
13066 },
13067 BracketPair {
13068 start: "\"".to_string(),
13069 end: "\"".to_string(),
13070 close: true,
13071 surround: true,
13072 newline: false,
13073 },
13074 BracketPair {
13075 start: "<".to_string(),
13076 end: ">".to_string(),
13077 close: false,
13078 surround: true,
13079 newline: true,
13080 },
13081 ],
13082 ..Default::default()
13083 },
13084 autoclose_before: "})]".to_string(),
13085 ..Default::default()
13086 },
13087 Some(tree_sitter_rust::LANGUAGE.into()),
13088 );
13089 let language = Arc::new(language);
13090
13091 cx.language_registry().add(language.clone());
13092 cx.update_buffer(|buffer, cx| {
13093 buffer.set_language(Some(language), cx);
13094 });
13095
13096 // Ensure that signature_help is not called when no signature help is enabled.
13097 cx.set_state(
13098 &r#"
13099 fn main() {
13100 sampleˇ
13101 }
13102 "#
13103 .unindent(),
13104 );
13105 cx.update_editor(|editor, window, cx| {
13106 editor.handle_input("(", window, cx);
13107 });
13108 cx.assert_editor_state(
13109 &"
13110 fn main() {
13111 sample(ˇ)
13112 }
13113 "
13114 .unindent(),
13115 );
13116 cx.editor(|editor, _, _| {
13117 assert!(editor.signature_help_state.task().is_none());
13118 });
13119
13120 let mocked_response = lsp::SignatureHelp {
13121 signatures: vec![lsp::SignatureInformation {
13122 label: "fn sample(param1: u8, param2: u8)".to_string(),
13123 documentation: None,
13124 parameters: Some(vec![
13125 lsp::ParameterInformation {
13126 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13127 documentation: None,
13128 },
13129 lsp::ParameterInformation {
13130 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13131 documentation: None,
13132 },
13133 ]),
13134 active_parameter: None,
13135 }],
13136 active_signature: Some(0),
13137 active_parameter: Some(0),
13138 };
13139
13140 // Ensure that signature_help is called when enabled afte edits
13141 cx.update(|_, cx| {
13142 cx.update_global::<SettingsStore, _>(|settings, cx| {
13143 settings.update_user_settings(cx, |settings| {
13144 settings.editor.auto_signature_help = Some(false);
13145 settings.editor.show_signature_help_after_edits = Some(true);
13146 });
13147 });
13148 });
13149 cx.set_state(
13150 &r#"
13151 fn main() {
13152 sampleˇ
13153 }
13154 "#
13155 .unindent(),
13156 );
13157 cx.update_editor(|editor, window, cx| {
13158 editor.handle_input("(", window, cx);
13159 });
13160 cx.assert_editor_state(
13161 &"
13162 fn main() {
13163 sample(ˇ)
13164 }
13165 "
13166 .unindent(),
13167 );
13168 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13169 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13170 .await;
13171 cx.update_editor(|editor, _, _| {
13172 let signature_help_state = editor.signature_help_state.popover().cloned();
13173 assert!(signature_help_state.is_some());
13174 let signature = signature_help_state.unwrap();
13175 assert_eq!(
13176 signature.signatures[signature.current_signature].label,
13177 "fn sample(param1: u8, param2: u8)"
13178 );
13179 editor.signature_help_state = SignatureHelpState::default();
13180 });
13181
13182 // Ensure that signature_help is called when auto signature help override is enabled
13183 cx.update(|_, cx| {
13184 cx.update_global::<SettingsStore, _>(|settings, cx| {
13185 settings.update_user_settings(cx, |settings| {
13186 settings.editor.auto_signature_help = Some(true);
13187 settings.editor.show_signature_help_after_edits = Some(false);
13188 });
13189 });
13190 });
13191 cx.set_state(
13192 &r#"
13193 fn main() {
13194 sampleˇ
13195 }
13196 "#
13197 .unindent(),
13198 );
13199 cx.update_editor(|editor, window, cx| {
13200 editor.handle_input("(", window, cx);
13201 });
13202 cx.assert_editor_state(
13203 &"
13204 fn main() {
13205 sample(ˇ)
13206 }
13207 "
13208 .unindent(),
13209 );
13210 handle_signature_help_request(&mut cx, mocked_response).await;
13211 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13212 .await;
13213 cx.editor(|editor, _, _| {
13214 let signature_help_state = editor.signature_help_state.popover().cloned();
13215 assert!(signature_help_state.is_some());
13216 let signature = signature_help_state.unwrap();
13217 assert_eq!(
13218 signature.signatures[signature.current_signature].label,
13219 "fn sample(param1: u8, param2: u8)"
13220 );
13221 });
13222}
13223
13224#[gpui::test]
13225async fn test_signature_help(cx: &mut TestAppContext) {
13226 init_test(cx, |_| {});
13227 cx.update(|cx| {
13228 cx.update_global::<SettingsStore, _>(|settings, cx| {
13229 settings.update_user_settings(cx, |settings| {
13230 settings.editor.auto_signature_help = Some(true);
13231 });
13232 });
13233 });
13234
13235 let mut cx = EditorLspTestContext::new_rust(
13236 lsp::ServerCapabilities {
13237 signature_help_provider: Some(lsp::SignatureHelpOptions {
13238 ..Default::default()
13239 }),
13240 ..Default::default()
13241 },
13242 cx,
13243 )
13244 .await;
13245
13246 // A test that directly calls `show_signature_help`
13247 cx.update_editor(|editor, window, cx| {
13248 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13249 });
13250
13251 let mocked_response = lsp::SignatureHelp {
13252 signatures: vec![lsp::SignatureInformation {
13253 label: "fn sample(param1: u8, param2: u8)".to_string(),
13254 documentation: None,
13255 parameters: Some(vec![
13256 lsp::ParameterInformation {
13257 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13258 documentation: None,
13259 },
13260 lsp::ParameterInformation {
13261 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13262 documentation: None,
13263 },
13264 ]),
13265 active_parameter: None,
13266 }],
13267 active_signature: Some(0),
13268 active_parameter: Some(0),
13269 };
13270 handle_signature_help_request(&mut cx, mocked_response).await;
13271
13272 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13273 .await;
13274
13275 cx.editor(|editor, _, _| {
13276 let signature_help_state = editor.signature_help_state.popover().cloned();
13277 assert!(signature_help_state.is_some());
13278 let signature = signature_help_state.unwrap();
13279 assert_eq!(
13280 signature.signatures[signature.current_signature].label,
13281 "fn sample(param1: u8, param2: u8)"
13282 );
13283 });
13284
13285 // When exiting outside from inside the brackets, `signature_help` is closed.
13286 cx.set_state(indoc! {"
13287 fn main() {
13288 sample(ˇ);
13289 }
13290
13291 fn sample(param1: u8, param2: u8) {}
13292 "});
13293
13294 cx.update_editor(|editor, window, cx| {
13295 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13296 s.select_ranges([0..0])
13297 });
13298 });
13299
13300 let mocked_response = lsp::SignatureHelp {
13301 signatures: Vec::new(),
13302 active_signature: None,
13303 active_parameter: None,
13304 };
13305 handle_signature_help_request(&mut cx, mocked_response).await;
13306
13307 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13308 .await;
13309
13310 cx.editor(|editor, _, _| {
13311 assert!(!editor.signature_help_state.is_shown());
13312 });
13313
13314 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
13315 cx.set_state(indoc! {"
13316 fn main() {
13317 sample(ˇ);
13318 }
13319
13320 fn sample(param1: u8, param2: u8) {}
13321 "});
13322
13323 let mocked_response = lsp::SignatureHelp {
13324 signatures: vec![lsp::SignatureInformation {
13325 label: "fn sample(param1: u8, param2: u8)".to_string(),
13326 documentation: None,
13327 parameters: Some(vec![
13328 lsp::ParameterInformation {
13329 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13330 documentation: None,
13331 },
13332 lsp::ParameterInformation {
13333 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13334 documentation: None,
13335 },
13336 ]),
13337 active_parameter: None,
13338 }],
13339 active_signature: Some(0),
13340 active_parameter: Some(0),
13341 };
13342 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13343 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13344 .await;
13345 cx.editor(|editor, _, _| {
13346 assert!(editor.signature_help_state.is_shown());
13347 });
13348
13349 // Restore the popover with more parameter input
13350 cx.set_state(indoc! {"
13351 fn main() {
13352 sample(param1, param2ˇ);
13353 }
13354
13355 fn sample(param1: u8, param2: u8) {}
13356 "});
13357
13358 let mocked_response = lsp::SignatureHelp {
13359 signatures: vec![lsp::SignatureInformation {
13360 label: "fn sample(param1: u8, param2: u8)".to_string(),
13361 documentation: None,
13362 parameters: Some(vec![
13363 lsp::ParameterInformation {
13364 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13365 documentation: None,
13366 },
13367 lsp::ParameterInformation {
13368 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13369 documentation: None,
13370 },
13371 ]),
13372 active_parameter: None,
13373 }],
13374 active_signature: Some(0),
13375 active_parameter: Some(1),
13376 };
13377 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13378 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13379 .await;
13380
13381 // When selecting a range, the popover is gone.
13382 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
13383 cx.update_editor(|editor, window, cx| {
13384 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13385 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13386 })
13387 });
13388 cx.assert_editor_state(indoc! {"
13389 fn main() {
13390 sample(param1, «ˇparam2»);
13391 }
13392
13393 fn sample(param1: u8, param2: u8) {}
13394 "});
13395 cx.editor(|editor, _, _| {
13396 assert!(!editor.signature_help_state.is_shown());
13397 });
13398
13399 // When unselecting again, the popover is back if within the brackets.
13400 cx.update_editor(|editor, window, cx| {
13401 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13402 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13403 })
13404 });
13405 cx.assert_editor_state(indoc! {"
13406 fn main() {
13407 sample(param1, ˇparam2);
13408 }
13409
13410 fn sample(param1: u8, param2: u8) {}
13411 "});
13412 handle_signature_help_request(&mut cx, mocked_response).await;
13413 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13414 .await;
13415 cx.editor(|editor, _, _| {
13416 assert!(editor.signature_help_state.is_shown());
13417 });
13418
13419 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
13420 cx.update_editor(|editor, window, cx| {
13421 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13422 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
13423 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13424 })
13425 });
13426 cx.assert_editor_state(indoc! {"
13427 fn main() {
13428 sample(param1, ˇparam2);
13429 }
13430
13431 fn sample(param1: u8, param2: u8) {}
13432 "});
13433
13434 let mocked_response = lsp::SignatureHelp {
13435 signatures: vec![lsp::SignatureInformation {
13436 label: "fn sample(param1: u8, param2: u8)".to_string(),
13437 documentation: None,
13438 parameters: Some(vec![
13439 lsp::ParameterInformation {
13440 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13441 documentation: None,
13442 },
13443 lsp::ParameterInformation {
13444 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13445 documentation: None,
13446 },
13447 ]),
13448 active_parameter: None,
13449 }],
13450 active_signature: Some(0),
13451 active_parameter: Some(1),
13452 };
13453 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13454 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13455 .await;
13456 cx.update_editor(|editor, _, cx| {
13457 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
13458 });
13459 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13460 .await;
13461 cx.update_editor(|editor, window, cx| {
13462 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13463 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13464 })
13465 });
13466 cx.assert_editor_state(indoc! {"
13467 fn main() {
13468 sample(param1, «ˇparam2»);
13469 }
13470
13471 fn sample(param1: u8, param2: u8) {}
13472 "});
13473 cx.update_editor(|editor, window, cx| {
13474 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13475 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13476 })
13477 });
13478 cx.assert_editor_state(indoc! {"
13479 fn main() {
13480 sample(param1, ˇparam2);
13481 }
13482
13483 fn sample(param1: u8, param2: u8) {}
13484 "});
13485 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
13486 .await;
13487}
13488
13489#[gpui::test]
13490async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
13491 init_test(cx, |_| {});
13492
13493 let mut cx = EditorLspTestContext::new_rust(
13494 lsp::ServerCapabilities {
13495 signature_help_provider: Some(lsp::SignatureHelpOptions {
13496 ..Default::default()
13497 }),
13498 ..Default::default()
13499 },
13500 cx,
13501 )
13502 .await;
13503
13504 cx.set_state(indoc! {"
13505 fn main() {
13506 overloadedˇ
13507 }
13508 "});
13509
13510 cx.update_editor(|editor, window, cx| {
13511 editor.handle_input("(", window, cx);
13512 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13513 });
13514
13515 // Mock response with 3 signatures
13516 let mocked_response = lsp::SignatureHelp {
13517 signatures: vec![
13518 lsp::SignatureInformation {
13519 label: "fn overloaded(x: i32)".to_string(),
13520 documentation: None,
13521 parameters: Some(vec![lsp::ParameterInformation {
13522 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13523 documentation: None,
13524 }]),
13525 active_parameter: None,
13526 },
13527 lsp::SignatureInformation {
13528 label: "fn overloaded(x: i32, y: i32)".to_string(),
13529 documentation: None,
13530 parameters: Some(vec![
13531 lsp::ParameterInformation {
13532 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13533 documentation: None,
13534 },
13535 lsp::ParameterInformation {
13536 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13537 documentation: None,
13538 },
13539 ]),
13540 active_parameter: None,
13541 },
13542 lsp::SignatureInformation {
13543 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
13544 documentation: None,
13545 parameters: Some(vec![
13546 lsp::ParameterInformation {
13547 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13548 documentation: None,
13549 },
13550 lsp::ParameterInformation {
13551 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13552 documentation: None,
13553 },
13554 lsp::ParameterInformation {
13555 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
13556 documentation: None,
13557 },
13558 ]),
13559 active_parameter: None,
13560 },
13561 ],
13562 active_signature: Some(1),
13563 active_parameter: Some(0),
13564 };
13565 handle_signature_help_request(&mut cx, mocked_response).await;
13566
13567 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13568 .await;
13569
13570 // Verify we have multiple signatures and the right one is selected
13571 cx.editor(|editor, _, _| {
13572 let popover = editor.signature_help_state.popover().cloned().unwrap();
13573 assert_eq!(popover.signatures.len(), 3);
13574 // active_signature was 1, so that should be the current
13575 assert_eq!(popover.current_signature, 1);
13576 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
13577 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
13578 assert_eq!(
13579 popover.signatures[2].label,
13580 "fn overloaded(x: i32, y: i32, z: i32)"
13581 );
13582 });
13583
13584 // Test navigation functionality
13585 cx.update_editor(|editor, window, cx| {
13586 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13587 });
13588
13589 cx.editor(|editor, _, _| {
13590 let popover = editor.signature_help_state.popover().cloned().unwrap();
13591 assert_eq!(popover.current_signature, 2);
13592 });
13593
13594 // Test wrap around
13595 cx.update_editor(|editor, window, cx| {
13596 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13597 });
13598
13599 cx.editor(|editor, _, _| {
13600 let popover = editor.signature_help_state.popover().cloned().unwrap();
13601 assert_eq!(popover.current_signature, 0);
13602 });
13603
13604 // Test previous navigation
13605 cx.update_editor(|editor, window, cx| {
13606 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
13607 });
13608
13609 cx.editor(|editor, _, _| {
13610 let popover = editor.signature_help_state.popover().cloned().unwrap();
13611 assert_eq!(popover.current_signature, 2);
13612 });
13613}
13614
13615#[gpui::test]
13616async fn test_completion_mode(cx: &mut TestAppContext) {
13617 init_test(cx, |_| {});
13618 let mut cx = EditorLspTestContext::new_rust(
13619 lsp::ServerCapabilities {
13620 completion_provider: Some(lsp::CompletionOptions {
13621 resolve_provider: Some(true),
13622 ..Default::default()
13623 }),
13624 ..Default::default()
13625 },
13626 cx,
13627 )
13628 .await;
13629
13630 struct Run {
13631 run_description: &'static str,
13632 initial_state: String,
13633 buffer_marked_text: String,
13634 completion_label: &'static str,
13635 completion_text: &'static str,
13636 expected_with_insert_mode: String,
13637 expected_with_replace_mode: String,
13638 expected_with_replace_subsequence_mode: String,
13639 expected_with_replace_suffix_mode: String,
13640 }
13641
13642 let runs = [
13643 Run {
13644 run_description: "Start of word matches completion text",
13645 initial_state: "before ediˇ after".into(),
13646 buffer_marked_text: "before <edi|> after".into(),
13647 completion_label: "editor",
13648 completion_text: "editor",
13649 expected_with_insert_mode: "before editorˇ after".into(),
13650 expected_with_replace_mode: "before editorˇ after".into(),
13651 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13652 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13653 },
13654 Run {
13655 run_description: "Accept same text at the middle of the word",
13656 initial_state: "before ediˇtor after".into(),
13657 buffer_marked_text: "before <edi|tor> after".into(),
13658 completion_label: "editor",
13659 completion_text: "editor",
13660 expected_with_insert_mode: "before editorˇtor after".into(),
13661 expected_with_replace_mode: "before editorˇ after".into(),
13662 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13663 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13664 },
13665 Run {
13666 run_description: "End of word matches completion text -- cursor at end",
13667 initial_state: "before torˇ after".into(),
13668 buffer_marked_text: "before <tor|> after".into(),
13669 completion_label: "editor",
13670 completion_text: "editor",
13671 expected_with_insert_mode: "before editorˇ after".into(),
13672 expected_with_replace_mode: "before editorˇ after".into(),
13673 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13674 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13675 },
13676 Run {
13677 run_description: "End of word matches completion text -- cursor at start",
13678 initial_state: "before ˇtor after".into(),
13679 buffer_marked_text: "before <|tor> after".into(),
13680 completion_label: "editor",
13681 completion_text: "editor",
13682 expected_with_insert_mode: "before editorˇtor after".into(),
13683 expected_with_replace_mode: "before editorˇ after".into(),
13684 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13685 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13686 },
13687 Run {
13688 run_description: "Prepend text containing whitespace",
13689 initial_state: "pˇfield: bool".into(),
13690 buffer_marked_text: "<p|field>: bool".into(),
13691 completion_label: "pub ",
13692 completion_text: "pub ",
13693 expected_with_insert_mode: "pub ˇfield: bool".into(),
13694 expected_with_replace_mode: "pub ˇ: bool".into(),
13695 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
13696 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
13697 },
13698 Run {
13699 run_description: "Add element to start of list",
13700 initial_state: "[element_ˇelement_2]".into(),
13701 buffer_marked_text: "[<element_|element_2>]".into(),
13702 completion_label: "element_1",
13703 completion_text: "element_1",
13704 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
13705 expected_with_replace_mode: "[element_1ˇ]".into(),
13706 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
13707 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
13708 },
13709 Run {
13710 run_description: "Add element to start of list -- first and second elements are equal",
13711 initial_state: "[elˇelement]".into(),
13712 buffer_marked_text: "[<el|element>]".into(),
13713 completion_label: "element",
13714 completion_text: "element",
13715 expected_with_insert_mode: "[elementˇelement]".into(),
13716 expected_with_replace_mode: "[elementˇ]".into(),
13717 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
13718 expected_with_replace_suffix_mode: "[elementˇ]".into(),
13719 },
13720 Run {
13721 run_description: "Ends with matching suffix",
13722 initial_state: "SubˇError".into(),
13723 buffer_marked_text: "<Sub|Error>".into(),
13724 completion_label: "SubscriptionError",
13725 completion_text: "SubscriptionError",
13726 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
13727 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13728 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13729 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
13730 },
13731 Run {
13732 run_description: "Suffix is a subsequence -- contiguous",
13733 initial_state: "SubˇErr".into(),
13734 buffer_marked_text: "<Sub|Err>".into(),
13735 completion_label: "SubscriptionError",
13736 completion_text: "SubscriptionError",
13737 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
13738 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13739 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13740 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
13741 },
13742 Run {
13743 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
13744 initial_state: "Suˇscrirr".into(),
13745 buffer_marked_text: "<Su|scrirr>".into(),
13746 completion_label: "SubscriptionError",
13747 completion_text: "SubscriptionError",
13748 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
13749 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13750 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13751 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
13752 },
13753 Run {
13754 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
13755 initial_state: "foo(indˇix)".into(),
13756 buffer_marked_text: "foo(<ind|ix>)".into(),
13757 completion_label: "node_index",
13758 completion_text: "node_index",
13759 expected_with_insert_mode: "foo(node_indexˇix)".into(),
13760 expected_with_replace_mode: "foo(node_indexˇ)".into(),
13761 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
13762 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
13763 },
13764 Run {
13765 run_description: "Replace range ends before cursor - should extend to cursor",
13766 initial_state: "before editˇo after".into(),
13767 buffer_marked_text: "before <{ed}>it|o after".into(),
13768 completion_label: "editor",
13769 completion_text: "editor",
13770 expected_with_insert_mode: "before editorˇo after".into(),
13771 expected_with_replace_mode: "before editorˇo after".into(),
13772 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
13773 expected_with_replace_suffix_mode: "before editorˇo after".into(),
13774 },
13775 Run {
13776 run_description: "Uses label for suffix matching",
13777 initial_state: "before ediˇtor after".into(),
13778 buffer_marked_text: "before <edi|tor> after".into(),
13779 completion_label: "editor",
13780 completion_text: "editor()",
13781 expected_with_insert_mode: "before editor()ˇtor after".into(),
13782 expected_with_replace_mode: "before editor()ˇ after".into(),
13783 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
13784 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
13785 },
13786 Run {
13787 run_description: "Case insensitive subsequence and suffix matching",
13788 initial_state: "before EDiˇtoR after".into(),
13789 buffer_marked_text: "before <EDi|toR> after".into(),
13790 completion_label: "editor",
13791 completion_text: "editor",
13792 expected_with_insert_mode: "before editorˇtoR after".into(),
13793 expected_with_replace_mode: "before editorˇ after".into(),
13794 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13795 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13796 },
13797 ];
13798
13799 for run in runs {
13800 let run_variations = [
13801 (LspInsertMode::Insert, run.expected_with_insert_mode),
13802 (LspInsertMode::Replace, run.expected_with_replace_mode),
13803 (
13804 LspInsertMode::ReplaceSubsequence,
13805 run.expected_with_replace_subsequence_mode,
13806 ),
13807 (
13808 LspInsertMode::ReplaceSuffix,
13809 run.expected_with_replace_suffix_mode,
13810 ),
13811 ];
13812
13813 for (lsp_insert_mode, expected_text) in run_variations {
13814 eprintln!(
13815 "run = {:?}, mode = {lsp_insert_mode:.?}",
13816 run.run_description,
13817 );
13818
13819 update_test_language_settings(&mut cx, |settings| {
13820 settings.defaults.completions = Some(CompletionSettingsContent {
13821 lsp_insert_mode: Some(lsp_insert_mode),
13822 words: Some(WordsCompletionMode::Disabled),
13823 words_min_length: Some(0),
13824 ..Default::default()
13825 });
13826 });
13827
13828 cx.set_state(&run.initial_state);
13829 cx.update_editor(|editor, window, cx| {
13830 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13831 });
13832
13833 let counter = Arc::new(AtomicUsize::new(0));
13834 handle_completion_request_with_insert_and_replace(
13835 &mut cx,
13836 &run.buffer_marked_text,
13837 vec![(run.completion_label, run.completion_text)],
13838 counter.clone(),
13839 )
13840 .await;
13841 cx.condition(|editor, _| editor.context_menu_visible())
13842 .await;
13843 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13844
13845 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13846 editor
13847 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13848 .unwrap()
13849 });
13850 cx.assert_editor_state(&expected_text);
13851 handle_resolve_completion_request(&mut cx, None).await;
13852 apply_additional_edits.await.unwrap();
13853 }
13854 }
13855}
13856
13857#[gpui::test]
13858async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
13859 init_test(cx, |_| {});
13860 let mut cx = EditorLspTestContext::new_rust(
13861 lsp::ServerCapabilities {
13862 completion_provider: Some(lsp::CompletionOptions {
13863 resolve_provider: Some(true),
13864 ..Default::default()
13865 }),
13866 ..Default::default()
13867 },
13868 cx,
13869 )
13870 .await;
13871
13872 let initial_state = "SubˇError";
13873 let buffer_marked_text = "<Sub|Error>";
13874 let completion_text = "SubscriptionError";
13875 let expected_with_insert_mode = "SubscriptionErrorˇError";
13876 let expected_with_replace_mode = "SubscriptionErrorˇ";
13877
13878 update_test_language_settings(&mut cx, |settings| {
13879 settings.defaults.completions = Some(CompletionSettingsContent {
13880 words: Some(WordsCompletionMode::Disabled),
13881 words_min_length: Some(0),
13882 // set the opposite here to ensure that the action is overriding the default behavior
13883 lsp_insert_mode: Some(LspInsertMode::Insert),
13884 ..Default::default()
13885 });
13886 });
13887
13888 cx.set_state(initial_state);
13889 cx.update_editor(|editor, window, cx| {
13890 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13891 });
13892
13893 let counter = Arc::new(AtomicUsize::new(0));
13894 handle_completion_request_with_insert_and_replace(
13895 &mut cx,
13896 buffer_marked_text,
13897 vec![(completion_text, completion_text)],
13898 counter.clone(),
13899 )
13900 .await;
13901 cx.condition(|editor, _| editor.context_menu_visible())
13902 .await;
13903 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13904
13905 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13906 editor
13907 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13908 .unwrap()
13909 });
13910 cx.assert_editor_state(expected_with_replace_mode);
13911 handle_resolve_completion_request(&mut cx, None).await;
13912 apply_additional_edits.await.unwrap();
13913
13914 update_test_language_settings(&mut cx, |settings| {
13915 settings.defaults.completions = Some(CompletionSettingsContent {
13916 words: Some(WordsCompletionMode::Disabled),
13917 words_min_length: Some(0),
13918 // set the opposite here to ensure that the action is overriding the default behavior
13919 lsp_insert_mode: Some(LspInsertMode::Replace),
13920 ..Default::default()
13921 });
13922 });
13923
13924 cx.set_state(initial_state);
13925 cx.update_editor(|editor, window, cx| {
13926 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13927 });
13928 handle_completion_request_with_insert_and_replace(
13929 &mut cx,
13930 buffer_marked_text,
13931 vec![(completion_text, completion_text)],
13932 counter.clone(),
13933 )
13934 .await;
13935 cx.condition(|editor, _| editor.context_menu_visible())
13936 .await;
13937 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
13938
13939 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13940 editor
13941 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
13942 .unwrap()
13943 });
13944 cx.assert_editor_state(expected_with_insert_mode);
13945 handle_resolve_completion_request(&mut cx, None).await;
13946 apply_additional_edits.await.unwrap();
13947}
13948
13949#[gpui::test]
13950async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
13951 init_test(cx, |_| {});
13952 let mut cx = EditorLspTestContext::new_rust(
13953 lsp::ServerCapabilities {
13954 completion_provider: Some(lsp::CompletionOptions {
13955 resolve_provider: Some(true),
13956 ..Default::default()
13957 }),
13958 ..Default::default()
13959 },
13960 cx,
13961 )
13962 .await;
13963
13964 // scenario: surrounding text matches completion text
13965 let completion_text = "to_offset";
13966 let initial_state = indoc! {"
13967 1. buf.to_offˇsuffix
13968 2. buf.to_offˇsuf
13969 3. buf.to_offˇfix
13970 4. buf.to_offˇ
13971 5. into_offˇensive
13972 6. ˇsuffix
13973 7. let ˇ //
13974 8. aaˇzz
13975 9. buf.to_off«zzzzzˇ»suffix
13976 10. buf.«ˇzzzzz»suffix
13977 11. to_off«ˇzzzzz»
13978
13979 buf.to_offˇsuffix // newest cursor
13980 "};
13981 let completion_marked_buffer = indoc! {"
13982 1. buf.to_offsuffix
13983 2. buf.to_offsuf
13984 3. buf.to_offfix
13985 4. buf.to_off
13986 5. into_offensive
13987 6. suffix
13988 7. let //
13989 8. aazz
13990 9. buf.to_offzzzzzsuffix
13991 10. buf.zzzzzsuffix
13992 11. to_offzzzzz
13993
13994 buf.<to_off|suffix> // newest cursor
13995 "};
13996 let expected = indoc! {"
13997 1. buf.to_offsetˇ
13998 2. buf.to_offsetˇsuf
13999 3. buf.to_offsetˇfix
14000 4. buf.to_offsetˇ
14001 5. into_offsetˇensive
14002 6. to_offsetˇsuffix
14003 7. let to_offsetˇ //
14004 8. aato_offsetˇzz
14005 9. buf.to_offsetˇ
14006 10. buf.to_offsetˇsuffix
14007 11. to_offsetˇ
14008
14009 buf.to_offsetˇ // newest cursor
14010 "};
14011 cx.set_state(initial_state);
14012 cx.update_editor(|editor, window, cx| {
14013 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14014 });
14015 handle_completion_request_with_insert_and_replace(
14016 &mut cx,
14017 completion_marked_buffer,
14018 vec![(completion_text, completion_text)],
14019 Arc::new(AtomicUsize::new(0)),
14020 )
14021 .await;
14022 cx.condition(|editor, _| editor.context_menu_visible())
14023 .await;
14024 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14025 editor
14026 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14027 .unwrap()
14028 });
14029 cx.assert_editor_state(expected);
14030 handle_resolve_completion_request(&mut cx, None).await;
14031 apply_additional_edits.await.unwrap();
14032
14033 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
14034 let completion_text = "foo_and_bar";
14035 let initial_state = indoc! {"
14036 1. ooanbˇ
14037 2. zooanbˇ
14038 3. ooanbˇz
14039 4. zooanbˇz
14040 5. ooanˇ
14041 6. oanbˇ
14042
14043 ooanbˇ
14044 "};
14045 let completion_marked_buffer = indoc! {"
14046 1. ooanb
14047 2. zooanb
14048 3. ooanbz
14049 4. zooanbz
14050 5. ooan
14051 6. oanb
14052
14053 <ooanb|>
14054 "};
14055 let expected = indoc! {"
14056 1. foo_and_barˇ
14057 2. zfoo_and_barˇ
14058 3. foo_and_barˇz
14059 4. zfoo_and_barˇz
14060 5. ooanfoo_and_barˇ
14061 6. oanbfoo_and_barˇ
14062
14063 foo_and_barˇ
14064 "};
14065 cx.set_state(initial_state);
14066 cx.update_editor(|editor, window, cx| {
14067 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14068 });
14069 handle_completion_request_with_insert_and_replace(
14070 &mut cx,
14071 completion_marked_buffer,
14072 vec![(completion_text, completion_text)],
14073 Arc::new(AtomicUsize::new(0)),
14074 )
14075 .await;
14076 cx.condition(|editor, _| editor.context_menu_visible())
14077 .await;
14078 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14079 editor
14080 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14081 .unwrap()
14082 });
14083 cx.assert_editor_state(expected);
14084 handle_resolve_completion_request(&mut cx, None).await;
14085 apply_additional_edits.await.unwrap();
14086
14087 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
14088 // (expects the same as if it was inserted at the end)
14089 let completion_text = "foo_and_bar";
14090 let initial_state = indoc! {"
14091 1. ooˇanb
14092 2. zooˇanb
14093 3. ooˇanbz
14094 4. zooˇanbz
14095
14096 ooˇanb
14097 "};
14098 let completion_marked_buffer = indoc! {"
14099 1. ooanb
14100 2. zooanb
14101 3. ooanbz
14102 4. zooanbz
14103
14104 <oo|anb>
14105 "};
14106 let expected = indoc! {"
14107 1. foo_and_barˇ
14108 2. zfoo_and_barˇ
14109 3. foo_and_barˇz
14110 4. zfoo_and_barˇz
14111
14112 foo_and_barˇ
14113 "};
14114 cx.set_state(initial_state);
14115 cx.update_editor(|editor, window, cx| {
14116 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14117 });
14118 handle_completion_request_with_insert_and_replace(
14119 &mut cx,
14120 completion_marked_buffer,
14121 vec![(completion_text, completion_text)],
14122 Arc::new(AtomicUsize::new(0)),
14123 )
14124 .await;
14125 cx.condition(|editor, _| editor.context_menu_visible())
14126 .await;
14127 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14128 editor
14129 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14130 .unwrap()
14131 });
14132 cx.assert_editor_state(expected);
14133 handle_resolve_completion_request(&mut cx, None).await;
14134 apply_additional_edits.await.unwrap();
14135}
14136
14137// This used to crash
14138#[gpui::test]
14139async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
14140 init_test(cx, |_| {});
14141
14142 let buffer_text = indoc! {"
14143 fn main() {
14144 10.satu;
14145
14146 //
14147 // separate cursors so they open in different excerpts (manually reproducible)
14148 //
14149
14150 10.satu20;
14151 }
14152 "};
14153 let multibuffer_text_with_selections = indoc! {"
14154 fn main() {
14155 10.satuˇ;
14156
14157 //
14158
14159 //
14160
14161 10.satuˇ20;
14162 }
14163 "};
14164 let expected_multibuffer = indoc! {"
14165 fn main() {
14166 10.saturating_sub()ˇ;
14167
14168 //
14169
14170 //
14171
14172 10.saturating_sub()ˇ;
14173 }
14174 "};
14175
14176 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
14177 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
14178
14179 let fs = FakeFs::new(cx.executor());
14180 fs.insert_tree(
14181 path!("/a"),
14182 json!({
14183 "main.rs": buffer_text,
14184 }),
14185 )
14186 .await;
14187
14188 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14189 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14190 language_registry.add(rust_lang());
14191 let mut fake_servers = language_registry.register_fake_lsp(
14192 "Rust",
14193 FakeLspAdapter {
14194 capabilities: lsp::ServerCapabilities {
14195 completion_provider: Some(lsp::CompletionOptions {
14196 resolve_provider: None,
14197 ..lsp::CompletionOptions::default()
14198 }),
14199 ..lsp::ServerCapabilities::default()
14200 },
14201 ..FakeLspAdapter::default()
14202 },
14203 );
14204 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14205 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14206 let buffer = project
14207 .update(cx, |project, cx| {
14208 project.open_local_buffer(path!("/a/main.rs"), cx)
14209 })
14210 .await
14211 .unwrap();
14212
14213 let multi_buffer = cx.new(|cx| {
14214 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
14215 multi_buffer.push_excerpts(
14216 buffer.clone(),
14217 [ExcerptRange::new(0..first_excerpt_end)],
14218 cx,
14219 );
14220 multi_buffer.push_excerpts(
14221 buffer.clone(),
14222 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
14223 cx,
14224 );
14225 multi_buffer
14226 });
14227
14228 let editor = workspace
14229 .update(cx, |_, window, cx| {
14230 cx.new(|cx| {
14231 Editor::new(
14232 EditorMode::Full {
14233 scale_ui_elements_with_buffer_font_size: false,
14234 show_active_line_background: false,
14235 sizing_behavior: SizingBehavior::Default,
14236 },
14237 multi_buffer.clone(),
14238 Some(project.clone()),
14239 window,
14240 cx,
14241 )
14242 })
14243 })
14244 .unwrap();
14245
14246 let pane = workspace
14247 .update(cx, |workspace, _, _| workspace.active_pane().clone())
14248 .unwrap();
14249 pane.update_in(cx, |pane, window, cx| {
14250 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
14251 });
14252
14253 let fake_server = fake_servers.next().await.unwrap();
14254
14255 editor.update_in(cx, |editor, window, cx| {
14256 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14257 s.select_ranges([
14258 Point::new(1, 11)..Point::new(1, 11),
14259 Point::new(7, 11)..Point::new(7, 11),
14260 ])
14261 });
14262
14263 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
14264 });
14265
14266 editor.update_in(cx, |editor, window, cx| {
14267 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14268 });
14269
14270 fake_server
14271 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14272 let completion_item = lsp::CompletionItem {
14273 label: "saturating_sub()".into(),
14274 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
14275 lsp::InsertReplaceEdit {
14276 new_text: "saturating_sub()".to_owned(),
14277 insert: lsp::Range::new(
14278 lsp::Position::new(7, 7),
14279 lsp::Position::new(7, 11),
14280 ),
14281 replace: lsp::Range::new(
14282 lsp::Position::new(7, 7),
14283 lsp::Position::new(7, 13),
14284 ),
14285 },
14286 )),
14287 ..lsp::CompletionItem::default()
14288 };
14289
14290 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
14291 })
14292 .next()
14293 .await
14294 .unwrap();
14295
14296 cx.condition(&editor, |editor, _| editor.context_menu_visible())
14297 .await;
14298
14299 editor
14300 .update_in(cx, |editor, window, cx| {
14301 editor
14302 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14303 .unwrap()
14304 })
14305 .await
14306 .unwrap();
14307
14308 editor.update(cx, |editor, cx| {
14309 assert_text_with_selections(editor, expected_multibuffer, cx);
14310 })
14311}
14312
14313#[gpui::test]
14314async fn test_completion(cx: &mut TestAppContext) {
14315 init_test(cx, |_| {});
14316
14317 let mut cx = EditorLspTestContext::new_rust(
14318 lsp::ServerCapabilities {
14319 completion_provider: Some(lsp::CompletionOptions {
14320 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14321 resolve_provider: Some(true),
14322 ..Default::default()
14323 }),
14324 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14325 ..Default::default()
14326 },
14327 cx,
14328 )
14329 .await;
14330 let counter = Arc::new(AtomicUsize::new(0));
14331
14332 cx.set_state(indoc! {"
14333 oneˇ
14334 two
14335 three
14336 "});
14337 cx.simulate_keystroke(".");
14338 handle_completion_request(
14339 indoc! {"
14340 one.|<>
14341 two
14342 three
14343 "},
14344 vec!["first_completion", "second_completion"],
14345 true,
14346 counter.clone(),
14347 &mut cx,
14348 )
14349 .await;
14350 cx.condition(|editor, _| editor.context_menu_visible())
14351 .await;
14352 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14353
14354 let _handler = handle_signature_help_request(
14355 &mut cx,
14356 lsp::SignatureHelp {
14357 signatures: vec![lsp::SignatureInformation {
14358 label: "test signature".to_string(),
14359 documentation: None,
14360 parameters: Some(vec![lsp::ParameterInformation {
14361 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
14362 documentation: None,
14363 }]),
14364 active_parameter: None,
14365 }],
14366 active_signature: None,
14367 active_parameter: None,
14368 },
14369 );
14370 cx.update_editor(|editor, window, cx| {
14371 assert!(
14372 !editor.signature_help_state.is_shown(),
14373 "No signature help was called for"
14374 );
14375 editor.show_signature_help(&ShowSignatureHelp, window, cx);
14376 });
14377 cx.run_until_parked();
14378 cx.update_editor(|editor, _, _| {
14379 assert!(
14380 !editor.signature_help_state.is_shown(),
14381 "No signature help should be shown when completions menu is open"
14382 );
14383 });
14384
14385 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14386 editor.context_menu_next(&Default::default(), window, cx);
14387 editor
14388 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14389 .unwrap()
14390 });
14391 cx.assert_editor_state(indoc! {"
14392 one.second_completionˇ
14393 two
14394 three
14395 "});
14396
14397 handle_resolve_completion_request(
14398 &mut cx,
14399 Some(vec![
14400 (
14401 //This overlaps with the primary completion edit which is
14402 //misbehavior from the LSP spec, test that we filter it out
14403 indoc! {"
14404 one.second_ˇcompletion
14405 two
14406 threeˇ
14407 "},
14408 "overlapping additional edit",
14409 ),
14410 (
14411 indoc! {"
14412 one.second_completion
14413 two
14414 threeˇ
14415 "},
14416 "\nadditional edit",
14417 ),
14418 ]),
14419 )
14420 .await;
14421 apply_additional_edits.await.unwrap();
14422 cx.assert_editor_state(indoc! {"
14423 one.second_completionˇ
14424 two
14425 three
14426 additional edit
14427 "});
14428
14429 cx.set_state(indoc! {"
14430 one.second_completion
14431 twoˇ
14432 threeˇ
14433 additional edit
14434 "});
14435 cx.simulate_keystroke(" ");
14436 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14437 cx.simulate_keystroke("s");
14438 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14439
14440 cx.assert_editor_state(indoc! {"
14441 one.second_completion
14442 two sˇ
14443 three sˇ
14444 additional edit
14445 "});
14446 handle_completion_request(
14447 indoc! {"
14448 one.second_completion
14449 two s
14450 three <s|>
14451 additional edit
14452 "},
14453 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14454 true,
14455 counter.clone(),
14456 &mut cx,
14457 )
14458 .await;
14459 cx.condition(|editor, _| editor.context_menu_visible())
14460 .await;
14461 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14462
14463 cx.simulate_keystroke("i");
14464
14465 handle_completion_request(
14466 indoc! {"
14467 one.second_completion
14468 two si
14469 three <si|>
14470 additional edit
14471 "},
14472 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14473 true,
14474 counter.clone(),
14475 &mut cx,
14476 )
14477 .await;
14478 cx.condition(|editor, _| editor.context_menu_visible())
14479 .await;
14480 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14481
14482 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14483 editor
14484 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14485 .unwrap()
14486 });
14487 cx.assert_editor_state(indoc! {"
14488 one.second_completion
14489 two sixth_completionˇ
14490 three sixth_completionˇ
14491 additional edit
14492 "});
14493
14494 apply_additional_edits.await.unwrap();
14495
14496 update_test_language_settings(&mut cx, |settings| {
14497 settings.defaults.show_completions_on_input = Some(false);
14498 });
14499 cx.set_state("editorˇ");
14500 cx.simulate_keystroke(".");
14501 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14502 cx.simulate_keystrokes("c l o");
14503 cx.assert_editor_state("editor.cloˇ");
14504 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14505 cx.update_editor(|editor, window, cx| {
14506 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14507 });
14508 handle_completion_request(
14509 "editor.<clo|>",
14510 vec!["close", "clobber"],
14511 true,
14512 counter.clone(),
14513 &mut cx,
14514 )
14515 .await;
14516 cx.condition(|editor, _| editor.context_menu_visible())
14517 .await;
14518 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
14519
14520 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14521 editor
14522 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14523 .unwrap()
14524 });
14525 cx.assert_editor_state("editor.clobberˇ");
14526 handle_resolve_completion_request(&mut cx, None).await;
14527 apply_additional_edits.await.unwrap();
14528}
14529
14530#[gpui::test]
14531async fn test_completion_reuse(cx: &mut TestAppContext) {
14532 init_test(cx, |_| {});
14533
14534 let mut cx = EditorLspTestContext::new_rust(
14535 lsp::ServerCapabilities {
14536 completion_provider: Some(lsp::CompletionOptions {
14537 trigger_characters: Some(vec![".".to_string()]),
14538 ..Default::default()
14539 }),
14540 ..Default::default()
14541 },
14542 cx,
14543 )
14544 .await;
14545
14546 let counter = Arc::new(AtomicUsize::new(0));
14547 cx.set_state("objˇ");
14548 cx.simulate_keystroke(".");
14549
14550 // Initial completion request returns complete results
14551 let is_incomplete = false;
14552 handle_completion_request(
14553 "obj.|<>",
14554 vec!["a", "ab", "abc"],
14555 is_incomplete,
14556 counter.clone(),
14557 &mut cx,
14558 )
14559 .await;
14560 cx.run_until_parked();
14561 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14562 cx.assert_editor_state("obj.ˇ");
14563 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14564
14565 // Type "a" - filters existing completions
14566 cx.simulate_keystroke("a");
14567 cx.run_until_parked();
14568 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14569 cx.assert_editor_state("obj.aˇ");
14570 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14571
14572 // Type "b" - filters existing completions
14573 cx.simulate_keystroke("b");
14574 cx.run_until_parked();
14575 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14576 cx.assert_editor_state("obj.abˇ");
14577 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14578
14579 // Type "c" - filters existing completions
14580 cx.simulate_keystroke("c");
14581 cx.run_until_parked();
14582 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14583 cx.assert_editor_state("obj.abcˇ");
14584 check_displayed_completions(vec!["abc"], &mut cx);
14585
14586 // Backspace to delete "c" - filters existing completions
14587 cx.update_editor(|editor, window, cx| {
14588 editor.backspace(&Backspace, window, cx);
14589 });
14590 cx.run_until_parked();
14591 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14592 cx.assert_editor_state("obj.abˇ");
14593 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14594
14595 // Moving cursor to the left dismisses menu.
14596 cx.update_editor(|editor, window, cx| {
14597 editor.move_left(&MoveLeft, window, cx);
14598 });
14599 cx.run_until_parked();
14600 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14601 cx.assert_editor_state("obj.aˇb");
14602 cx.update_editor(|editor, _, _| {
14603 assert_eq!(editor.context_menu_visible(), false);
14604 });
14605
14606 // Type "b" - new request
14607 cx.simulate_keystroke("b");
14608 let is_incomplete = false;
14609 handle_completion_request(
14610 "obj.<ab|>a",
14611 vec!["ab", "abc"],
14612 is_incomplete,
14613 counter.clone(),
14614 &mut cx,
14615 )
14616 .await;
14617 cx.run_until_parked();
14618 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14619 cx.assert_editor_state("obj.abˇb");
14620 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14621
14622 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
14623 cx.update_editor(|editor, window, cx| {
14624 editor.backspace(&Backspace, window, cx);
14625 });
14626 let is_incomplete = false;
14627 handle_completion_request(
14628 "obj.<a|>b",
14629 vec!["a", "ab", "abc"],
14630 is_incomplete,
14631 counter.clone(),
14632 &mut cx,
14633 )
14634 .await;
14635 cx.run_until_parked();
14636 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14637 cx.assert_editor_state("obj.aˇb");
14638 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14639
14640 // Backspace to delete "a" - dismisses menu.
14641 cx.update_editor(|editor, window, cx| {
14642 editor.backspace(&Backspace, window, cx);
14643 });
14644 cx.run_until_parked();
14645 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14646 cx.assert_editor_state("obj.ˇb");
14647 cx.update_editor(|editor, _, _| {
14648 assert_eq!(editor.context_menu_visible(), false);
14649 });
14650}
14651
14652#[gpui::test]
14653async fn test_word_completion(cx: &mut TestAppContext) {
14654 let lsp_fetch_timeout_ms = 10;
14655 init_test(cx, |language_settings| {
14656 language_settings.defaults.completions = Some(CompletionSettingsContent {
14657 words_min_length: Some(0),
14658 lsp_fetch_timeout_ms: Some(10),
14659 lsp_insert_mode: Some(LspInsertMode::Insert),
14660 ..Default::default()
14661 });
14662 });
14663
14664 let mut cx = EditorLspTestContext::new_rust(
14665 lsp::ServerCapabilities {
14666 completion_provider: Some(lsp::CompletionOptions {
14667 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14668 ..lsp::CompletionOptions::default()
14669 }),
14670 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14671 ..lsp::ServerCapabilities::default()
14672 },
14673 cx,
14674 )
14675 .await;
14676
14677 let throttle_completions = Arc::new(AtomicBool::new(false));
14678
14679 let lsp_throttle_completions = throttle_completions.clone();
14680 let _completion_requests_handler =
14681 cx.lsp
14682 .server
14683 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
14684 let lsp_throttle_completions = lsp_throttle_completions.clone();
14685 let cx = cx.clone();
14686 async move {
14687 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
14688 cx.background_executor()
14689 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
14690 .await;
14691 }
14692 Ok(Some(lsp::CompletionResponse::Array(vec![
14693 lsp::CompletionItem {
14694 label: "first".into(),
14695 ..lsp::CompletionItem::default()
14696 },
14697 lsp::CompletionItem {
14698 label: "last".into(),
14699 ..lsp::CompletionItem::default()
14700 },
14701 ])))
14702 }
14703 });
14704
14705 cx.set_state(indoc! {"
14706 oneˇ
14707 two
14708 three
14709 "});
14710 cx.simulate_keystroke(".");
14711 cx.executor().run_until_parked();
14712 cx.condition(|editor, _| editor.context_menu_visible())
14713 .await;
14714 cx.update_editor(|editor, window, cx| {
14715 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14716 {
14717 assert_eq!(
14718 completion_menu_entries(menu),
14719 &["first", "last"],
14720 "When LSP server is fast to reply, no fallback word completions are used"
14721 );
14722 } else {
14723 panic!("expected completion menu to be open");
14724 }
14725 editor.cancel(&Cancel, window, cx);
14726 });
14727 cx.executor().run_until_parked();
14728 cx.condition(|editor, _| !editor.context_menu_visible())
14729 .await;
14730
14731 throttle_completions.store(true, atomic::Ordering::Release);
14732 cx.simulate_keystroke(".");
14733 cx.executor()
14734 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
14735 cx.executor().run_until_parked();
14736 cx.condition(|editor, _| editor.context_menu_visible())
14737 .await;
14738 cx.update_editor(|editor, _, _| {
14739 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14740 {
14741 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
14742 "When LSP server is slow, document words can be shown instead, if configured accordingly");
14743 } else {
14744 panic!("expected completion menu to be open");
14745 }
14746 });
14747}
14748
14749#[gpui::test]
14750async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
14751 init_test(cx, |language_settings| {
14752 language_settings.defaults.completions = Some(CompletionSettingsContent {
14753 words: Some(WordsCompletionMode::Enabled),
14754 words_min_length: Some(0),
14755 lsp_insert_mode: Some(LspInsertMode::Insert),
14756 ..Default::default()
14757 });
14758 });
14759
14760 let mut cx = EditorLspTestContext::new_rust(
14761 lsp::ServerCapabilities {
14762 completion_provider: Some(lsp::CompletionOptions {
14763 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14764 ..lsp::CompletionOptions::default()
14765 }),
14766 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14767 ..lsp::ServerCapabilities::default()
14768 },
14769 cx,
14770 )
14771 .await;
14772
14773 let _completion_requests_handler =
14774 cx.lsp
14775 .server
14776 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14777 Ok(Some(lsp::CompletionResponse::Array(vec![
14778 lsp::CompletionItem {
14779 label: "first".into(),
14780 ..lsp::CompletionItem::default()
14781 },
14782 lsp::CompletionItem {
14783 label: "last".into(),
14784 ..lsp::CompletionItem::default()
14785 },
14786 ])))
14787 });
14788
14789 cx.set_state(indoc! {"ˇ
14790 first
14791 last
14792 second
14793 "});
14794 cx.simulate_keystroke(".");
14795 cx.executor().run_until_parked();
14796 cx.condition(|editor, _| editor.context_menu_visible())
14797 .await;
14798 cx.update_editor(|editor, _, _| {
14799 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14800 {
14801 assert_eq!(
14802 completion_menu_entries(menu),
14803 &["first", "last", "second"],
14804 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
14805 );
14806 } else {
14807 panic!("expected completion menu to be open");
14808 }
14809 });
14810}
14811
14812#[gpui::test]
14813async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
14814 init_test(cx, |language_settings| {
14815 language_settings.defaults.completions = Some(CompletionSettingsContent {
14816 words: Some(WordsCompletionMode::Disabled),
14817 words_min_length: Some(0),
14818 lsp_insert_mode: Some(LspInsertMode::Insert),
14819 ..Default::default()
14820 });
14821 });
14822
14823 let mut cx = EditorLspTestContext::new_rust(
14824 lsp::ServerCapabilities {
14825 completion_provider: Some(lsp::CompletionOptions {
14826 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14827 ..lsp::CompletionOptions::default()
14828 }),
14829 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14830 ..lsp::ServerCapabilities::default()
14831 },
14832 cx,
14833 )
14834 .await;
14835
14836 let _completion_requests_handler =
14837 cx.lsp
14838 .server
14839 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14840 panic!("LSP completions should not be queried when dealing with word completions")
14841 });
14842
14843 cx.set_state(indoc! {"ˇ
14844 first
14845 last
14846 second
14847 "});
14848 cx.update_editor(|editor, window, cx| {
14849 editor.show_word_completions(&ShowWordCompletions, window, cx);
14850 });
14851 cx.executor().run_until_parked();
14852 cx.condition(|editor, _| editor.context_menu_visible())
14853 .await;
14854 cx.update_editor(|editor, _, _| {
14855 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14856 {
14857 assert_eq!(
14858 completion_menu_entries(menu),
14859 &["first", "last", "second"],
14860 "`ShowWordCompletions` action should show word completions"
14861 );
14862 } else {
14863 panic!("expected completion menu to be open");
14864 }
14865 });
14866
14867 cx.simulate_keystroke("l");
14868 cx.executor().run_until_parked();
14869 cx.condition(|editor, _| editor.context_menu_visible())
14870 .await;
14871 cx.update_editor(|editor, _, _| {
14872 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14873 {
14874 assert_eq!(
14875 completion_menu_entries(menu),
14876 &["last"],
14877 "After showing word completions, further editing should filter them and not query the LSP"
14878 );
14879 } else {
14880 panic!("expected completion menu to be open");
14881 }
14882 });
14883}
14884
14885#[gpui::test]
14886async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
14887 init_test(cx, |language_settings| {
14888 language_settings.defaults.completions = Some(CompletionSettingsContent {
14889 words_min_length: Some(0),
14890 lsp: Some(false),
14891 lsp_insert_mode: Some(LspInsertMode::Insert),
14892 ..Default::default()
14893 });
14894 });
14895
14896 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14897
14898 cx.set_state(indoc! {"ˇ
14899 0_usize
14900 let
14901 33
14902 4.5f32
14903 "});
14904 cx.update_editor(|editor, window, cx| {
14905 editor.show_completions(&ShowCompletions::default(), window, cx);
14906 });
14907 cx.executor().run_until_parked();
14908 cx.condition(|editor, _| editor.context_menu_visible())
14909 .await;
14910 cx.update_editor(|editor, window, cx| {
14911 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14912 {
14913 assert_eq!(
14914 completion_menu_entries(menu),
14915 &["let"],
14916 "With no digits in the completion query, no digits should be in the word completions"
14917 );
14918 } else {
14919 panic!("expected completion menu to be open");
14920 }
14921 editor.cancel(&Cancel, window, cx);
14922 });
14923
14924 cx.set_state(indoc! {"3ˇ
14925 0_usize
14926 let
14927 3
14928 33.35f32
14929 "});
14930 cx.update_editor(|editor, window, cx| {
14931 editor.show_completions(&ShowCompletions::default(), window, cx);
14932 });
14933 cx.executor().run_until_parked();
14934 cx.condition(|editor, _| editor.context_menu_visible())
14935 .await;
14936 cx.update_editor(|editor, _, _| {
14937 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14938 {
14939 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
14940 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
14941 } else {
14942 panic!("expected completion menu to be open");
14943 }
14944 });
14945}
14946
14947#[gpui::test]
14948async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
14949 init_test(cx, |language_settings| {
14950 language_settings.defaults.completions = Some(CompletionSettingsContent {
14951 words: Some(WordsCompletionMode::Enabled),
14952 words_min_length: Some(3),
14953 lsp_insert_mode: Some(LspInsertMode::Insert),
14954 ..Default::default()
14955 });
14956 });
14957
14958 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14959 cx.set_state(indoc! {"ˇ
14960 wow
14961 wowen
14962 wowser
14963 "});
14964 cx.simulate_keystroke("w");
14965 cx.executor().run_until_parked();
14966 cx.update_editor(|editor, _, _| {
14967 if editor.context_menu.borrow_mut().is_some() {
14968 panic!(
14969 "expected completion menu to be hidden, as words completion threshold is not met"
14970 );
14971 }
14972 });
14973
14974 cx.update_editor(|editor, window, cx| {
14975 editor.show_word_completions(&ShowWordCompletions, window, cx);
14976 });
14977 cx.executor().run_until_parked();
14978 cx.update_editor(|editor, window, cx| {
14979 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14980 {
14981 assert_eq!(completion_menu_entries(menu), &["wowser", "wowen", "wow"], "Even though the threshold is not met, invoking word completions with an action should provide the completions");
14982 } else {
14983 panic!("expected completion menu to be open after the word completions are called with an action");
14984 }
14985
14986 editor.cancel(&Cancel, window, cx);
14987 });
14988 cx.update_editor(|editor, _, _| {
14989 if editor.context_menu.borrow_mut().is_some() {
14990 panic!("expected completion menu to be hidden after canceling");
14991 }
14992 });
14993
14994 cx.simulate_keystroke("o");
14995 cx.executor().run_until_parked();
14996 cx.update_editor(|editor, _, _| {
14997 if editor.context_menu.borrow_mut().is_some() {
14998 panic!(
14999 "expected completion menu to be hidden, as words completion threshold is not met still"
15000 );
15001 }
15002 });
15003
15004 cx.simulate_keystroke("w");
15005 cx.executor().run_until_parked();
15006 cx.update_editor(|editor, _, _| {
15007 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15008 {
15009 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
15010 } else {
15011 panic!("expected completion menu to be open after the word completions threshold is met");
15012 }
15013 });
15014}
15015
15016#[gpui::test]
15017async fn test_word_completions_disabled(cx: &mut TestAppContext) {
15018 init_test(cx, |language_settings| {
15019 language_settings.defaults.completions = Some(CompletionSettingsContent {
15020 words: Some(WordsCompletionMode::Enabled),
15021 words_min_length: Some(0),
15022 lsp_insert_mode: Some(LspInsertMode::Insert),
15023 ..Default::default()
15024 });
15025 });
15026
15027 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15028 cx.update_editor(|editor, _, _| {
15029 editor.disable_word_completions();
15030 });
15031 cx.set_state(indoc! {"ˇ
15032 wow
15033 wowen
15034 wowser
15035 "});
15036 cx.simulate_keystroke("w");
15037 cx.executor().run_until_parked();
15038 cx.update_editor(|editor, _, _| {
15039 if editor.context_menu.borrow_mut().is_some() {
15040 panic!(
15041 "expected completion menu to be hidden, as words completion are disabled for this editor"
15042 );
15043 }
15044 });
15045
15046 cx.update_editor(|editor, window, cx| {
15047 editor.show_word_completions(&ShowWordCompletions, window, cx);
15048 });
15049 cx.executor().run_until_parked();
15050 cx.update_editor(|editor, _, _| {
15051 if editor.context_menu.borrow_mut().is_some() {
15052 panic!(
15053 "expected completion menu to be hidden even if called for explicitly, as words completion are disabled for this editor"
15054 );
15055 }
15056 });
15057}
15058
15059fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
15060 let position = || lsp::Position {
15061 line: params.text_document_position.position.line,
15062 character: params.text_document_position.position.character,
15063 };
15064 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15065 range: lsp::Range {
15066 start: position(),
15067 end: position(),
15068 },
15069 new_text: text.to_string(),
15070 }))
15071}
15072
15073#[gpui::test]
15074async fn test_multiline_completion(cx: &mut TestAppContext) {
15075 init_test(cx, |_| {});
15076
15077 let fs = FakeFs::new(cx.executor());
15078 fs.insert_tree(
15079 path!("/a"),
15080 json!({
15081 "main.ts": "a",
15082 }),
15083 )
15084 .await;
15085
15086 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15087 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15088 let typescript_language = Arc::new(Language::new(
15089 LanguageConfig {
15090 name: "TypeScript".into(),
15091 matcher: LanguageMatcher {
15092 path_suffixes: vec!["ts".to_string()],
15093 ..LanguageMatcher::default()
15094 },
15095 line_comments: vec!["// ".into()],
15096 ..LanguageConfig::default()
15097 },
15098 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
15099 ));
15100 language_registry.add(typescript_language.clone());
15101 let mut fake_servers = language_registry.register_fake_lsp(
15102 "TypeScript",
15103 FakeLspAdapter {
15104 capabilities: lsp::ServerCapabilities {
15105 completion_provider: Some(lsp::CompletionOptions {
15106 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15107 ..lsp::CompletionOptions::default()
15108 }),
15109 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15110 ..lsp::ServerCapabilities::default()
15111 },
15112 // Emulate vtsls label generation
15113 label_for_completion: Some(Box::new(|item, _| {
15114 let text = if let Some(description) = item
15115 .label_details
15116 .as_ref()
15117 .and_then(|label_details| label_details.description.as_ref())
15118 {
15119 format!("{} {}", item.label, description)
15120 } else if let Some(detail) = &item.detail {
15121 format!("{} {}", item.label, detail)
15122 } else {
15123 item.label.clone()
15124 };
15125 Some(language::CodeLabel::plain(text, None))
15126 })),
15127 ..FakeLspAdapter::default()
15128 },
15129 );
15130 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15131 let cx = &mut VisualTestContext::from_window(*workspace, cx);
15132 let worktree_id = workspace
15133 .update(cx, |workspace, _window, cx| {
15134 workspace.project().update(cx, |project, cx| {
15135 project.worktrees(cx).next().unwrap().read(cx).id()
15136 })
15137 })
15138 .unwrap();
15139 let _buffer = project
15140 .update(cx, |project, cx| {
15141 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
15142 })
15143 .await
15144 .unwrap();
15145 let editor = workspace
15146 .update(cx, |workspace, window, cx| {
15147 workspace.open_path((worktree_id, rel_path("main.ts")), None, true, window, cx)
15148 })
15149 .unwrap()
15150 .await
15151 .unwrap()
15152 .downcast::<Editor>()
15153 .unwrap();
15154 let fake_server = fake_servers.next().await.unwrap();
15155
15156 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
15157 let multiline_label_2 = "a\nb\nc\n";
15158 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
15159 let multiline_description = "d\ne\nf\n";
15160 let multiline_detail_2 = "g\nh\ni\n";
15161
15162 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
15163 move |params, _| async move {
15164 Ok(Some(lsp::CompletionResponse::Array(vec![
15165 lsp::CompletionItem {
15166 label: multiline_label.to_string(),
15167 text_edit: gen_text_edit(¶ms, "new_text_1"),
15168 ..lsp::CompletionItem::default()
15169 },
15170 lsp::CompletionItem {
15171 label: "single line label 1".to_string(),
15172 detail: Some(multiline_detail.to_string()),
15173 text_edit: gen_text_edit(¶ms, "new_text_2"),
15174 ..lsp::CompletionItem::default()
15175 },
15176 lsp::CompletionItem {
15177 label: "single line label 2".to_string(),
15178 label_details: Some(lsp::CompletionItemLabelDetails {
15179 description: Some(multiline_description.to_string()),
15180 detail: None,
15181 }),
15182 text_edit: gen_text_edit(¶ms, "new_text_2"),
15183 ..lsp::CompletionItem::default()
15184 },
15185 lsp::CompletionItem {
15186 label: multiline_label_2.to_string(),
15187 detail: Some(multiline_detail_2.to_string()),
15188 text_edit: gen_text_edit(¶ms, "new_text_3"),
15189 ..lsp::CompletionItem::default()
15190 },
15191 lsp::CompletionItem {
15192 label: "Label with many spaces and \t but without newlines".to_string(),
15193 detail: Some(
15194 "Details with many spaces and \t but without newlines".to_string(),
15195 ),
15196 text_edit: gen_text_edit(¶ms, "new_text_4"),
15197 ..lsp::CompletionItem::default()
15198 },
15199 ])))
15200 },
15201 );
15202
15203 editor.update_in(cx, |editor, window, cx| {
15204 cx.focus_self(window);
15205 editor.move_to_end(&MoveToEnd, window, cx);
15206 editor.handle_input(".", window, cx);
15207 });
15208 cx.run_until_parked();
15209 completion_handle.next().await.unwrap();
15210
15211 editor.update(cx, |editor, _| {
15212 assert!(editor.context_menu_visible());
15213 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15214 {
15215 let completion_labels = menu
15216 .completions
15217 .borrow()
15218 .iter()
15219 .map(|c| c.label.text.clone())
15220 .collect::<Vec<_>>();
15221 assert_eq!(
15222 completion_labels,
15223 &[
15224 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
15225 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
15226 "single line label 2 d e f ",
15227 "a b c g h i ",
15228 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
15229 ],
15230 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
15231 );
15232
15233 for completion in menu
15234 .completions
15235 .borrow()
15236 .iter() {
15237 assert_eq!(
15238 completion.label.filter_range,
15239 0..completion.label.text.len(),
15240 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
15241 );
15242 }
15243 } else {
15244 panic!("expected completion menu to be open");
15245 }
15246 });
15247}
15248
15249#[gpui::test]
15250async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
15251 init_test(cx, |_| {});
15252 let mut cx = EditorLspTestContext::new_rust(
15253 lsp::ServerCapabilities {
15254 completion_provider: Some(lsp::CompletionOptions {
15255 trigger_characters: Some(vec![".".to_string()]),
15256 ..Default::default()
15257 }),
15258 ..Default::default()
15259 },
15260 cx,
15261 )
15262 .await;
15263 cx.lsp
15264 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15265 Ok(Some(lsp::CompletionResponse::Array(vec![
15266 lsp::CompletionItem {
15267 label: "first".into(),
15268 ..Default::default()
15269 },
15270 lsp::CompletionItem {
15271 label: "last".into(),
15272 ..Default::default()
15273 },
15274 ])))
15275 });
15276 cx.set_state("variableˇ");
15277 cx.simulate_keystroke(".");
15278 cx.executor().run_until_parked();
15279
15280 cx.update_editor(|editor, _, _| {
15281 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15282 {
15283 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
15284 } else {
15285 panic!("expected completion menu to be open");
15286 }
15287 });
15288
15289 cx.update_editor(|editor, window, cx| {
15290 editor.move_page_down(&MovePageDown::default(), window, cx);
15291 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15292 {
15293 assert!(
15294 menu.selected_item == 1,
15295 "expected PageDown to select the last item from the context menu"
15296 );
15297 } else {
15298 panic!("expected completion menu to stay open after PageDown");
15299 }
15300 });
15301
15302 cx.update_editor(|editor, window, cx| {
15303 editor.move_page_up(&MovePageUp::default(), window, cx);
15304 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15305 {
15306 assert!(
15307 menu.selected_item == 0,
15308 "expected PageUp to select the first item from the context menu"
15309 );
15310 } else {
15311 panic!("expected completion menu to stay open after PageUp");
15312 }
15313 });
15314}
15315
15316#[gpui::test]
15317async fn test_as_is_completions(cx: &mut TestAppContext) {
15318 init_test(cx, |_| {});
15319 let mut cx = EditorLspTestContext::new_rust(
15320 lsp::ServerCapabilities {
15321 completion_provider: Some(lsp::CompletionOptions {
15322 ..Default::default()
15323 }),
15324 ..Default::default()
15325 },
15326 cx,
15327 )
15328 .await;
15329 cx.lsp
15330 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15331 Ok(Some(lsp::CompletionResponse::Array(vec![
15332 lsp::CompletionItem {
15333 label: "unsafe".into(),
15334 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15335 range: lsp::Range {
15336 start: lsp::Position {
15337 line: 1,
15338 character: 2,
15339 },
15340 end: lsp::Position {
15341 line: 1,
15342 character: 3,
15343 },
15344 },
15345 new_text: "unsafe".to_string(),
15346 })),
15347 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
15348 ..Default::default()
15349 },
15350 ])))
15351 });
15352 cx.set_state("fn a() {}\n nˇ");
15353 cx.executor().run_until_parked();
15354 cx.update_editor(|editor, window, cx| {
15355 editor.show_completions(
15356 &ShowCompletions {
15357 trigger: Some("\n".into()),
15358 },
15359 window,
15360 cx,
15361 );
15362 });
15363 cx.executor().run_until_parked();
15364
15365 cx.update_editor(|editor, window, cx| {
15366 editor.confirm_completion(&Default::default(), window, cx)
15367 });
15368 cx.executor().run_until_parked();
15369 cx.assert_editor_state("fn a() {}\n unsafeˇ");
15370}
15371
15372#[gpui::test]
15373async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
15374 init_test(cx, |_| {});
15375 let language =
15376 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
15377 let mut cx = EditorLspTestContext::new(
15378 language,
15379 lsp::ServerCapabilities {
15380 completion_provider: Some(lsp::CompletionOptions {
15381 ..lsp::CompletionOptions::default()
15382 }),
15383 ..lsp::ServerCapabilities::default()
15384 },
15385 cx,
15386 )
15387 .await;
15388
15389 cx.set_state(
15390 "#ifndef BAR_H
15391#define BAR_H
15392
15393#include <stdbool.h>
15394
15395int fn_branch(bool do_branch1, bool do_branch2);
15396
15397#endif // BAR_H
15398ˇ",
15399 );
15400 cx.executor().run_until_parked();
15401 cx.update_editor(|editor, window, cx| {
15402 editor.handle_input("#", window, cx);
15403 });
15404 cx.executor().run_until_parked();
15405 cx.update_editor(|editor, window, cx| {
15406 editor.handle_input("i", window, cx);
15407 });
15408 cx.executor().run_until_parked();
15409 cx.update_editor(|editor, window, cx| {
15410 editor.handle_input("n", window, cx);
15411 });
15412 cx.executor().run_until_parked();
15413 cx.assert_editor_state(
15414 "#ifndef BAR_H
15415#define BAR_H
15416
15417#include <stdbool.h>
15418
15419int fn_branch(bool do_branch1, bool do_branch2);
15420
15421#endif // BAR_H
15422#inˇ",
15423 );
15424
15425 cx.lsp
15426 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15427 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15428 is_incomplete: false,
15429 item_defaults: None,
15430 items: vec![lsp::CompletionItem {
15431 kind: Some(lsp::CompletionItemKind::SNIPPET),
15432 label_details: Some(lsp::CompletionItemLabelDetails {
15433 detail: Some("header".to_string()),
15434 description: None,
15435 }),
15436 label: " include".to_string(),
15437 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15438 range: lsp::Range {
15439 start: lsp::Position {
15440 line: 8,
15441 character: 1,
15442 },
15443 end: lsp::Position {
15444 line: 8,
15445 character: 1,
15446 },
15447 },
15448 new_text: "include \"$0\"".to_string(),
15449 })),
15450 sort_text: Some("40b67681include".to_string()),
15451 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15452 filter_text: Some("include".to_string()),
15453 insert_text: Some("include \"$0\"".to_string()),
15454 ..lsp::CompletionItem::default()
15455 }],
15456 })))
15457 });
15458 cx.update_editor(|editor, window, cx| {
15459 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15460 });
15461 cx.executor().run_until_parked();
15462 cx.update_editor(|editor, window, cx| {
15463 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15464 });
15465 cx.executor().run_until_parked();
15466 cx.assert_editor_state(
15467 "#ifndef BAR_H
15468#define BAR_H
15469
15470#include <stdbool.h>
15471
15472int fn_branch(bool do_branch1, bool do_branch2);
15473
15474#endif // BAR_H
15475#include \"ˇ\"",
15476 );
15477
15478 cx.lsp
15479 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15480 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15481 is_incomplete: true,
15482 item_defaults: None,
15483 items: vec![lsp::CompletionItem {
15484 kind: Some(lsp::CompletionItemKind::FILE),
15485 label: "AGL/".to_string(),
15486 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15487 range: lsp::Range {
15488 start: lsp::Position {
15489 line: 8,
15490 character: 10,
15491 },
15492 end: lsp::Position {
15493 line: 8,
15494 character: 11,
15495 },
15496 },
15497 new_text: "AGL/".to_string(),
15498 })),
15499 sort_text: Some("40b67681AGL/".to_string()),
15500 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15501 filter_text: Some("AGL/".to_string()),
15502 insert_text: Some("AGL/".to_string()),
15503 ..lsp::CompletionItem::default()
15504 }],
15505 })))
15506 });
15507 cx.update_editor(|editor, window, cx| {
15508 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15509 });
15510 cx.executor().run_until_parked();
15511 cx.update_editor(|editor, window, cx| {
15512 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15513 });
15514 cx.executor().run_until_parked();
15515 cx.assert_editor_state(
15516 r##"#ifndef BAR_H
15517#define BAR_H
15518
15519#include <stdbool.h>
15520
15521int fn_branch(bool do_branch1, bool do_branch2);
15522
15523#endif // BAR_H
15524#include "AGL/ˇ"##,
15525 );
15526
15527 cx.update_editor(|editor, window, cx| {
15528 editor.handle_input("\"", window, cx);
15529 });
15530 cx.executor().run_until_parked();
15531 cx.assert_editor_state(
15532 r##"#ifndef BAR_H
15533#define BAR_H
15534
15535#include <stdbool.h>
15536
15537int fn_branch(bool do_branch1, bool do_branch2);
15538
15539#endif // BAR_H
15540#include "AGL/"ˇ"##,
15541 );
15542}
15543
15544#[gpui::test]
15545async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
15546 init_test(cx, |_| {});
15547
15548 let mut cx = EditorLspTestContext::new_rust(
15549 lsp::ServerCapabilities {
15550 completion_provider: Some(lsp::CompletionOptions {
15551 trigger_characters: Some(vec![".".to_string()]),
15552 resolve_provider: Some(true),
15553 ..Default::default()
15554 }),
15555 ..Default::default()
15556 },
15557 cx,
15558 )
15559 .await;
15560
15561 cx.set_state("fn main() { let a = 2ˇ; }");
15562 cx.simulate_keystroke(".");
15563 let completion_item = lsp::CompletionItem {
15564 label: "Some".into(),
15565 kind: Some(lsp::CompletionItemKind::SNIPPET),
15566 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15567 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15568 kind: lsp::MarkupKind::Markdown,
15569 value: "```rust\nSome(2)\n```".to_string(),
15570 })),
15571 deprecated: Some(false),
15572 sort_text: Some("Some".to_string()),
15573 filter_text: Some("Some".to_string()),
15574 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15575 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15576 range: lsp::Range {
15577 start: lsp::Position {
15578 line: 0,
15579 character: 22,
15580 },
15581 end: lsp::Position {
15582 line: 0,
15583 character: 22,
15584 },
15585 },
15586 new_text: "Some(2)".to_string(),
15587 })),
15588 additional_text_edits: Some(vec![lsp::TextEdit {
15589 range: lsp::Range {
15590 start: lsp::Position {
15591 line: 0,
15592 character: 20,
15593 },
15594 end: lsp::Position {
15595 line: 0,
15596 character: 22,
15597 },
15598 },
15599 new_text: "".to_string(),
15600 }]),
15601 ..Default::default()
15602 };
15603
15604 let closure_completion_item = completion_item.clone();
15605 let counter = Arc::new(AtomicUsize::new(0));
15606 let counter_clone = counter.clone();
15607 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15608 let task_completion_item = closure_completion_item.clone();
15609 counter_clone.fetch_add(1, atomic::Ordering::Release);
15610 async move {
15611 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15612 is_incomplete: true,
15613 item_defaults: None,
15614 items: vec![task_completion_item],
15615 })))
15616 }
15617 });
15618
15619 cx.condition(|editor, _| editor.context_menu_visible())
15620 .await;
15621 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
15622 assert!(request.next().await.is_some());
15623 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15624
15625 cx.simulate_keystrokes("S o m");
15626 cx.condition(|editor, _| editor.context_menu_visible())
15627 .await;
15628 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
15629 assert!(request.next().await.is_some());
15630 assert!(request.next().await.is_some());
15631 assert!(request.next().await.is_some());
15632 request.close();
15633 assert!(request.next().await.is_none());
15634 assert_eq!(
15635 counter.load(atomic::Ordering::Acquire),
15636 4,
15637 "With the completions menu open, only one LSP request should happen per input"
15638 );
15639}
15640
15641#[gpui::test]
15642async fn test_toggle_comment(cx: &mut TestAppContext) {
15643 init_test(cx, |_| {});
15644 let mut cx = EditorTestContext::new(cx).await;
15645 let language = Arc::new(Language::new(
15646 LanguageConfig {
15647 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15648 ..Default::default()
15649 },
15650 Some(tree_sitter_rust::LANGUAGE.into()),
15651 ));
15652 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15653
15654 // If multiple selections intersect a line, the line is only toggled once.
15655 cx.set_state(indoc! {"
15656 fn a() {
15657 «//b();
15658 ˇ»// «c();
15659 //ˇ» d();
15660 }
15661 "});
15662
15663 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15664
15665 cx.assert_editor_state(indoc! {"
15666 fn a() {
15667 «b();
15668 c();
15669 ˇ» d();
15670 }
15671 "});
15672
15673 // The comment prefix is inserted at the same column for every line in a
15674 // selection.
15675 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15676
15677 cx.assert_editor_state(indoc! {"
15678 fn a() {
15679 // «b();
15680 // c();
15681 ˇ»// d();
15682 }
15683 "});
15684
15685 // If a selection ends at the beginning of a line, that line is not toggled.
15686 cx.set_selections_state(indoc! {"
15687 fn a() {
15688 // b();
15689 «// c();
15690 ˇ» // d();
15691 }
15692 "});
15693
15694 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15695
15696 cx.assert_editor_state(indoc! {"
15697 fn a() {
15698 // b();
15699 «c();
15700 ˇ» // d();
15701 }
15702 "});
15703
15704 // If a selection span a single line and is empty, the line is toggled.
15705 cx.set_state(indoc! {"
15706 fn a() {
15707 a();
15708 b();
15709 ˇ
15710 }
15711 "});
15712
15713 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15714
15715 cx.assert_editor_state(indoc! {"
15716 fn a() {
15717 a();
15718 b();
15719 //•ˇ
15720 }
15721 "});
15722
15723 // If a selection span multiple lines, empty lines are not toggled.
15724 cx.set_state(indoc! {"
15725 fn a() {
15726 «a();
15727
15728 c();ˇ»
15729 }
15730 "});
15731
15732 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15733
15734 cx.assert_editor_state(indoc! {"
15735 fn a() {
15736 // «a();
15737
15738 // c();ˇ»
15739 }
15740 "});
15741
15742 // If a selection includes multiple comment prefixes, all lines are uncommented.
15743 cx.set_state(indoc! {"
15744 fn a() {
15745 «// a();
15746 /// b();
15747 //! c();ˇ»
15748 }
15749 "});
15750
15751 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15752
15753 cx.assert_editor_state(indoc! {"
15754 fn a() {
15755 «a();
15756 b();
15757 c();ˇ»
15758 }
15759 "});
15760}
15761
15762#[gpui::test]
15763async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
15764 init_test(cx, |_| {});
15765 let mut cx = EditorTestContext::new(cx).await;
15766 let language = Arc::new(Language::new(
15767 LanguageConfig {
15768 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15769 ..Default::default()
15770 },
15771 Some(tree_sitter_rust::LANGUAGE.into()),
15772 ));
15773 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15774
15775 let toggle_comments = &ToggleComments {
15776 advance_downwards: false,
15777 ignore_indent: true,
15778 };
15779
15780 // If multiple selections intersect a line, the line is only toggled once.
15781 cx.set_state(indoc! {"
15782 fn a() {
15783 // «b();
15784 // c();
15785 // ˇ» d();
15786 }
15787 "});
15788
15789 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15790
15791 cx.assert_editor_state(indoc! {"
15792 fn a() {
15793 «b();
15794 c();
15795 ˇ» d();
15796 }
15797 "});
15798
15799 // The comment prefix is inserted at the beginning of each line
15800 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15801
15802 cx.assert_editor_state(indoc! {"
15803 fn a() {
15804 // «b();
15805 // c();
15806 // ˇ» d();
15807 }
15808 "});
15809
15810 // If a selection ends at the beginning of a line, that line is not toggled.
15811 cx.set_selections_state(indoc! {"
15812 fn a() {
15813 // b();
15814 // «c();
15815 ˇ»// d();
15816 }
15817 "});
15818
15819 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15820
15821 cx.assert_editor_state(indoc! {"
15822 fn a() {
15823 // b();
15824 «c();
15825 ˇ»// d();
15826 }
15827 "});
15828
15829 // If a selection span a single line and is empty, the line is toggled.
15830 cx.set_state(indoc! {"
15831 fn a() {
15832 a();
15833 b();
15834 ˇ
15835 }
15836 "});
15837
15838 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15839
15840 cx.assert_editor_state(indoc! {"
15841 fn a() {
15842 a();
15843 b();
15844 //ˇ
15845 }
15846 "});
15847
15848 // If a selection span multiple lines, empty lines are not toggled.
15849 cx.set_state(indoc! {"
15850 fn a() {
15851 «a();
15852
15853 c();ˇ»
15854 }
15855 "});
15856
15857 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15858
15859 cx.assert_editor_state(indoc! {"
15860 fn a() {
15861 // «a();
15862
15863 // c();ˇ»
15864 }
15865 "});
15866
15867 // If a selection includes multiple comment prefixes, all lines are uncommented.
15868 cx.set_state(indoc! {"
15869 fn a() {
15870 // «a();
15871 /// b();
15872 //! c();ˇ»
15873 }
15874 "});
15875
15876 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15877
15878 cx.assert_editor_state(indoc! {"
15879 fn a() {
15880 «a();
15881 b();
15882 c();ˇ»
15883 }
15884 "});
15885}
15886
15887#[gpui::test]
15888async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
15889 init_test(cx, |_| {});
15890
15891 let language = Arc::new(Language::new(
15892 LanguageConfig {
15893 line_comments: vec!["// ".into()],
15894 ..Default::default()
15895 },
15896 Some(tree_sitter_rust::LANGUAGE.into()),
15897 ));
15898
15899 let mut cx = EditorTestContext::new(cx).await;
15900
15901 cx.language_registry().add(language.clone());
15902 cx.update_buffer(|buffer, cx| {
15903 buffer.set_language(Some(language), cx);
15904 });
15905
15906 let toggle_comments = &ToggleComments {
15907 advance_downwards: true,
15908 ignore_indent: false,
15909 };
15910
15911 // Single cursor on one line -> advance
15912 // Cursor moves horizontally 3 characters as well on non-blank line
15913 cx.set_state(indoc!(
15914 "fn a() {
15915 ˇdog();
15916 cat();
15917 }"
15918 ));
15919 cx.update_editor(|editor, window, cx| {
15920 editor.toggle_comments(toggle_comments, window, cx);
15921 });
15922 cx.assert_editor_state(indoc!(
15923 "fn a() {
15924 // dog();
15925 catˇ();
15926 }"
15927 ));
15928
15929 // Single selection on one line -> don't advance
15930 cx.set_state(indoc!(
15931 "fn a() {
15932 «dog()ˇ»;
15933 cat();
15934 }"
15935 ));
15936 cx.update_editor(|editor, window, cx| {
15937 editor.toggle_comments(toggle_comments, window, cx);
15938 });
15939 cx.assert_editor_state(indoc!(
15940 "fn a() {
15941 // «dog()ˇ»;
15942 cat();
15943 }"
15944 ));
15945
15946 // Multiple cursors on one line -> advance
15947 cx.set_state(indoc!(
15948 "fn a() {
15949 ˇdˇog();
15950 cat();
15951 }"
15952 ));
15953 cx.update_editor(|editor, window, cx| {
15954 editor.toggle_comments(toggle_comments, window, cx);
15955 });
15956 cx.assert_editor_state(indoc!(
15957 "fn a() {
15958 // dog();
15959 catˇ(ˇ);
15960 }"
15961 ));
15962
15963 // Multiple cursors on one line, with selection -> don't advance
15964 cx.set_state(indoc!(
15965 "fn a() {
15966 ˇdˇog«()ˇ»;
15967 cat();
15968 }"
15969 ));
15970 cx.update_editor(|editor, window, cx| {
15971 editor.toggle_comments(toggle_comments, window, cx);
15972 });
15973 cx.assert_editor_state(indoc!(
15974 "fn a() {
15975 // ˇdˇog«()ˇ»;
15976 cat();
15977 }"
15978 ));
15979
15980 // Single cursor on one line -> advance
15981 // Cursor moves to column 0 on blank line
15982 cx.set_state(indoc!(
15983 "fn a() {
15984 ˇdog();
15985
15986 cat();
15987 }"
15988 ));
15989 cx.update_editor(|editor, window, cx| {
15990 editor.toggle_comments(toggle_comments, window, cx);
15991 });
15992 cx.assert_editor_state(indoc!(
15993 "fn a() {
15994 // dog();
15995 ˇ
15996 cat();
15997 }"
15998 ));
15999
16000 // Single cursor on one line -> advance
16001 // Cursor starts and ends at column 0
16002 cx.set_state(indoc!(
16003 "fn a() {
16004 ˇ dog();
16005 cat();
16006 }"
16007 ));
16008 cx.update_editor(|editor, window, cx| {
16009 editor.toggle_comments(toggle_comments, window, cx);
16010 });
16011 cx.assert_editor_state(indoc!(
16012 "fn a() {
16013 // dog();
16014 ˇ cat();
16015 }"
16016 ));
16017}
16018
16019#[gpui::test]
16020async fn test_toggle_block_comment(cx: &mut TestAppContext) {
16021 init_test(cx, |_| {});
16022
16023 let mut cx = EditorTestContext::new(cx).await;
16024
16025 let html_language = Arc::new(
16026 Language::new(
16027 LanguageConfig {
16028 name: "HTML".into(),
16029 block_comment: Some(BlockCommentConfig {
16030 start: "<!-- ".into(),
16031 prefix: "".into(),
16032 end: " -->".into(),
16033 tab_size: 0,
16034 }),
16035 ..Default::default()
16036 },
16037 Some(tree_sitter_html::LANGUAGE.into()),
16038 )
16039 .with_injection_query(
16040 r#"
16041 (script_element
16042 (raw_text) @injection.content
16043 (#set! injection.language "javascript"))
16044 "#,
16045 )
16046 .unwrap(),
16047 );
16048
16049 let javascript_language = Arc::new(Language::new(
16050 LanguageConfig {
16051 name: "JavaScript".into(),
16052 line_comments: vec!["// ".into()],
16053 ..Default::default()
16054 },
16055 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
16056 ));
16057
16058 cx.language_registry().add(html_language.clone());
16059 cx.language_registry().add(javascript_language);
16060 cx.update_buffer(|buffer, cx| {
16061 buffer.set_language(Some(html_language), cx);
16062 });
16063
16064 // Toggle comments for empty selections
16065 cx.set_state(
16066 &r#"
16067 <p>A</p>ˇ
16068 <p>B</p>ˇ
16069 <p>C</p>ˇ
16070 "#
16071 .unindent(),
16072 );
16073 cx.update_editor(|editor, window, cx| {
16074 editor.toggle_comments(&ToggleComments::default(), window, cx)
16075 });
16076 cx.assert_editor_state(
16077 &r#"
16078 <!-- <p>A</p>ˇ -->
16079 <!-- <p>B</p>ˇ -->
16080 <!-- <p>C</p>ˇ -->
16081 "#
16082 .unindent(),
16083 );
16084 cx.update_editor(|editor, window, cx| {
16085 editor.toggle_comments(&ToggleComments::default(), window, cx)
16086 });
16087 cx.assert_editor_state(
16088 &r#"
16089 <p>A</p>ˇ
16090 <p>B</p>ˇ
16091 <p>C</p>ˇ
16092 "#
16093 .unindent(),
16094 );
16095
16096 // Toggle comments for mixture of empty and non-empty selections, where
16097 // multiple selections occupy a given line.
16098 cx.set_state(
16099 &r#"
16100 <p>A«</p>
16101 <p>ˇ»B</p>ˇ
16102 <p>C«</p>
16103 <p>ˇ»D</p>ˇ
16104 "#
16105 .unindent(),
16106 );
16107
16108 cx.update_editor(|editor, window, cx| {
16109 editor.toggle_comments(&ToggleComments::default(), window, cx)
16110 });
16111 cx.assert_editor_state(
16112 &r#"
16113 <!-- <p>A«</p>
16114 <p>ˇ»B</p>ˇ -->
16115 <!-- <p>C«</p>
16116 <p>ˇ»D</p>ˇ -->
16117 "#
16118 .unindent(),
16119 );
16120 cx.update_editor(|editor, window, cx| {
16121 editor.toggle_comments(&ToggleComments::default(), window, cx)
16122 });
16123 cx.assert_editor_state(
16124 &r#"
16125 <p>A«</p>
16126 <p>ˇ»B</p>ˇ
16127 <p>C«</p>
16128 <p>ˇ»D</p>ˇ
16129 "#
16130 .unindent(),
16131 );
16132
16133 // Toggle comments when different languages are active for different
16134 // selections.
16135 cx.set_state(
16136 &r#"
16137 ˇ<script>
16138 ˇvar x = new Y();
16139 ˇ</script>
16140 "#
16141 .unindent(),
16142 );
16143 cx.executor().run_until_parked();
16144 cx.update_editor(|editor, window, cx| {
16145 editor.toggle_comments(&ToggleComments::default(), window, cx)
16146 });
16147 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
16148 // Uncommenting and commenting from this position brings in even more wrong artifacts.
16149 cx.assert_editor_state(
16150 &r#"
16151 <!-- ˇ<script> -->
16152 // ˇvar x = new Y();
16153 <!-- ˇ</script> -->
16154 "#
16155 .unindent(),
16156 );
16157}
16158
16159#[gpui::test]
16160fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
16161 init_test(cx, |_| {});
16162
16163 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16164 let multibuffer = cx.new(|cx| {
16165 let mut multibuffer = MultiBuffer::new(ReadWrite);
16166 multibuffer.push_excerpts(
16167 buffer.clone(),
16168 [
16169 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
16170 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
16171 ],
16172 cx,
16173 );
16174 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
16175 multibuffer
16176 });
16177
16178 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
16179 editor.update_in(cx, |editor, window, cx| {
16180 assert_eq!(editor.text(cx), "aaaa\nbbbb");
16181 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16182 s.select_ranges([
16183 Point::new(0, 0)..Point::new(0, 0),
16184 Point::new(1, 0)..Point::new(1, 0),
16185 ])
16186 });
16187
16188 editor.handle_input("X", window, cx);
16189 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
16190 assert_eq!(
16191 editor.selections.ranges(&editor.display_snapshot(cx)),
16192 [
16193 Point::new(0, 1)..Point::new(0, 1),
16194 Point::new(1, 1)..Point::new(1, 1),
16195 ]
16196 );
16197
16198 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
16199 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16200 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
16201 });
16202 editor.backspace(&Default::default(), window, cx);
16203 assert_eq!(editor.text(cx), "Xa\nbbb");
16204 assert_eq!(
16205 editor.selections.ranges(&editor.display_snapshot(cx)),
16206 [Point::new(1, 0)..Point::new(1, 0)]
16207 );
16208
16209 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16210 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
16211 });
16212 editor.backspace(&Default::default(), window, cx);
16213 assert_eq!(editor.text(cx), "X\nbb");
16214 assert_eq!(
16215 editor.selections.ranges(&editor.display_snapshot(cx)),
16216 [Point::new(0, 1)..Point::new(0, 1)]
16217 );
16218 });
16219}
16220
16221#[gpui::test]
16222fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
16223 init_test(cx, |_| {});
16224
16225 let markers = vec![('[', ']').into(), ('(', ')').into()];
16226 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
16227 indoc! {"
16228 [aaaa
16229 (bbbb]
16230 cccc)",
16231 },
16232 markers.clone(),
16233 );
16234 let excerpt_ranges = markers.into_iter().map(|marker| {
16235 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
16236 ExcerptRange::new(context)
16237 });
16238 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
16239 let multibuffer = cx.new(|cx| {
16240 let mut multibuffer = MultiBuffer::new(ReadWrite);
16241 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
16242 multibuffer
16243 });
16244
16245 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
16246 editor.update_in(cx, |editor, window, cx| {
16247 let (expected_text, selection_ranges) = marked_text_ranges(
16248 indoc! {"
16249 aaaa
16250 bˇbbb
16251 bˇbbˇb
16252 cccc"
16253 },
16254 true,
16255 );
16256 assert_eq!(editor.text(cx), expected_text);
16257 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16258 s.select_ranges(selection_ranges)
16259 });
16260
16261 editor.handle_input("X", window, cx);
16262
16263 let (expected_text, expected_selections) = marked_text_ranges(
16264 indoc! {"
16265 aaaa
16266 bXˇbbXb
16267 bXˇbbXˇb
16268 cccc"
16269 },
16270 false,
16271 );
16272 assert_eq!(editor.text(cx), expected_text);
16273 assert_eq!(
16274 editor.selections.ranges(&editor.display_snapshot(cx)),
16275 expected_selections
16276 );
16277
16278 editor.newline(&Newline, window, cx);
16279 let (expected_text, expected_selections) = marked_text_ranges(
16280 indoc! {"
16281 aaaa
16282 bX
16283 ˇbbX
16284 b
16285 bX
16286 ˇbbX
16287 ˇb
16288 cccc"
16289 },
16290 false,
16291 );
16292 assert_eq!(editor.text(cx), expected_text);
16293 assert_eq!(
16294 editor.selections.ranges(&editor.display_snapshot(cx)),
16295 expected_selections
16296 );
16297 });
16298}
16299
16300#[gpui::test]
16301fn test_refresh_selections(cx: &mut TestAppContext) {
16302 init_test(cx, |_| {});
16303
16304 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16305 let mut excerpt1_id = None;
16306 let multibuffer = cx.new(|cx| {
16307 let mut multibuffer = MultiBuffer::new(ReadWrite);
16308 excerpt1_id = multibuffer
16309 .push_excerpts(
16310 buffer.clone(),
16311 [
16312 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
16313 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
16314 ],
16315 cx,
16316 )
16317 .into_iter()
16318 .next();
16319 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16320 multibuffer
16321 });
16322
16323 let editor = cx.add_window(|window, cx| {
16324 let mut editor = build_editor(multibuffer.clone(), window, cx);
16325 let snapshot = editor.snapshot(window, cx);
16326 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16327 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
16328 });
16329 editor.begin_selection(
16330 Point::new(2, 1).to_display_point(&snapshot),
16331 true,
16332 1,
16333 window,
16334 cx,
16335 );
16336 assert_eq!(
16337 editor.selections.ranges(&editor.display_snapshot(cx)),
16338 [
16339 Point::new(1, 3)..Point::new(1, 3),
16340 Point::new(2, 1)..Point::new(2, 1),
16341 ]
16342 );
16343 editor
16344 });
16345
16346 // Refreshing selections is a no-op when excerpts haven't changed.
16347 _ = editor.update(cx, |editor, window, cx| {
16348 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16349 assert_eq!(
16350 editor.selections.ranges(&editor.display_snapshot(cx)),
16351 [
16352 Point::new(1, 3)..Point::new(1, 3),
16353 Point::new(2, 1)..Point::new(2, 1),
16354 ]
16355 );
16356 });
16357
16358 multibuffer.update(cx, |multibuffer, cx| {
16359 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16360 });
16361 _ = editor.update(cx, |editor, window, cx| {
16362 // Removing an excerpt causes the first selection to become degenerate.
16363 assert_eq!(
16364 editor.selections.ranges(&editor.display_snapshot(cx)),
16365 [
16366 Point::new(0, 0)..Point::new(0, 0),
16367 Point::new(0, 1)..Point::new(0, 1)
16368 ]
16369 );
16370
16371 // Refreshing selections will relocate the first selection to the original buffer
16372 // location.
16373 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16374 assert_eq!(
16375 editor.selections.ranges(&editor.display_snapshot(cx)),
16376 [
16377 Point::new(0, 1)..Point::new(0, 1),
16378 Point::new(0, 3)..Point::new(0, 3)
16379 ]
16380 );
16381 assert!(editor.selections.pending_anchor().is_some());
16382 });
16383}
16384
16385#[gpui::test]
16386fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
16387 init_test(cx, |_| {});
16388
16389 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16390 let mut excerpt1_id = None;
16391 let multibuffer = cx.new(|cx| {
16392 let mut multibuffer = MultiBuffer::new(ReadWrite);
16393 excerpt1_id = multibuffer
16394 .push_excerpts(
16395 buffer.clone(),
16396 [
16397 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
16398 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
16399 ],
16400 cx,
16401 )
16402 .into_iter()
16403 .next();
16404 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16405 multibuffer
16406 });
16407
16408 let editor = cx.add_window(|window, cx| {
16409 let mut editor = build_editor(multibuffer.clone(), window, cx);
16410 let snapshot = editor.snapshot(window, cx);
16411 editor.begin_selection(
16412 Point::new(1, 3).to_display_point(&snapshot),
16413 false,
16414 1,
16415 window,
16416 cx,
16417 );
16418 assert_eq!(
16419 editor.selections.ranges(&editor.display_snapshot(cx)),
16420 [Point::new(1, 3)..Point::new(1, 3)]
16421 );
16422 editor
16423 });
16424
16425 multibuffer.update(cx, |multibuffer, cx| {
16426 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16427 });
16428 _ = editor.update(cx, |editor, window, cx| {
16429 assert_eq!(
16430 editor.selections.ranges(&editor.display_snapshot(cx)),
16431 [Point::new(0, 0)..Point::new(0, 0)]
16432 );
16433
16434 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
16435 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16436 assert_eq!(
16437 editor.selections.ranges(&editor.display_snapshot(cx)),
16438 [Point::new(0, 3)..Point::new(0, 3)]
16439 );
16440 assert!(editor.selections.pending_anchor().is_some());
16441 });
16442}
16443
16444#[gpui::test]
16445async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
16446 init_test(cx, |_| {});
16447
16448 let language = Arc::new(
16449 Language::new(
16450 LanguageConfig {
16451 brackets: BracketPairConfig {
16452 pairs: vec![
16453 BracketPair {
16454 start: "{".to_string(),
16455 end: "}".to_string(),
16456 close: true,
16457 surround: true,
16458 newline: true,
16459 },
16460 BracketPair {
16461 start: "/* ".to_string(),
16462 end: " */".to_string(),
16463 close: true,
16464 surround: true,
16465 newline: true,
16466 },
16467 ],
16468 ..Default::default()
16469 },
16470 ..Default::default()
16471 },
16472 Some(tree_sitter_rust::LANGUAGE.into()),
16473 )
16474 .with_indents_query("")
16475 .unwrap(),
16476 );
16477
16478 let text = concat!(
16479 "{ }\n", //
16480 " x\n", //
16481 " /* */\n", //
16482 "x\n", //
16483 "{{} }\n", //
16484 );
16485
16486 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
16487 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16488 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16489 editor
16490 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
16491 .await;
16492
16493 editor.update_in(cx, |editor, window, cx| {
16494 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16495 s.select_display_ranges([
16496 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
16497 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
16498 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
16499 ])
16500 });
16501 editor.newline(&Newline, window, cx);
16502
16503 assert_eq!(
16504 editor.buffer().read(cx).read(cx).text(),
16505 concat!(
16506 "{ \n", // Suppress rustfmt
16507 "\n", //
16508 "}\n", //
16509 " x\n", //
16510 " /* \n", //
16511 " \n", //
16512 " */\n", //
16513 "x\n", //
16514 "{{} \n", //
16515 "}\n", //
16516 )
16517 );
16518 });
16519}
16520
16521#[gpui::test]
16522fn test_highlighted_ranges(cx: &mut TestAppContext) {
16523 init_test(cx, |_| {});
16524
16525 let editor = cx.add_window(|window, cx| {
16526 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
16527 build_editor(buffer, window, cx)
16528 });
16529
16530 _ = editor.update(cx, |editor, window, cx| {
16531 struct Type1;
16532 struct Type2;
16533
16534 let buffer = editor.buffer.read(cx).snapshot(cx);
16535
16536 let anchor_range =
16537 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
16538
16539 editor.highlight_background::<Type1>(
16540 &[
16541 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
16542 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
16543 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
16544 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
16545 ],
16546 |_| Hsla::red(),
16547 cx,
16548 );
16549 editor.highlight_background::<Type2>(
16550 &[
16551 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
16552 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
16553 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
16554 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
16555 ],
16556 |_| Hsla::green(),
16557 cx,
16558 );
16559
16560 let snapshot = editor.snapshot(window, cx);
16561 let highlighted_ranges = editor.sorted_background_highlights_in_range(
16562 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
16563 &snapshot,
16564 cx.theme(),
16565 );
16566 assert_eq!(
16567 highlighted_ranges,
16568 &[
16569 (
16570 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
16571 Hsla::green(),
16572 ),
16573 (
16574 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
16575 Hsla::red(),
16576 ),
16577 (
16578 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
16579 Hsla::green(),
16580 ),
16581 (
16582 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16583 Hsla::red(),
16584 ),
16585 ]
16586 );
16587 assert_eq!(
16588 editor.sorted_background_highlights_in_range(
16589 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
16590 &snapshot,
16591 cx.theme(),
16592 ),
16593 &[(
16594 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16595 Hsla::red(),
16596 )]
16597 );
16598 });
16599}
16600
16601#[gpui::test]
16602async fn test_following(cx: &mut TestAppContext) {
16603 init_test(cx, |_| {});
16604
16605 let fs = FakeFs::new(cx.executor());
16606 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16607
16608 let buffer = project.update(cx, |project, cx| {
16609 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, false, cx);
16610 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
16611 });
16612 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
16613 let follower = cx.update(|cx| {
16614 cx.open_window(
16615 WindowOptions {
16616 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
16617 gpui::Point::new(px(0.), px(0.)),
16618 gpui::Point::new(px(10.), px(80.)),
16619 ))),
16620 ..Default::default()
16621 },
16622 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
16623 )
16624 .unwrap()
16625 });
16626
16627 let is_still_following = Rc::new(RefCell::new(true));
16628 let follower_edit_event_count = Rc::new(RefCell::new(0));
16629 let pending_update = Rc::new(RefCell::new(None));
16630 let leader_entity = leader.root(cx).unwrap();
16631 let follower_entity = follower.root(cx).unwrap();
16632 _ = follower.update(cx, {
16633 let update = pending_update.clone();
16634 let is_still_following = is_still_following.clone();
16635 let follower_edit_event_count = follower_edit_event_count.clone();
16636 |_, window, cx| {
16637 cx.subscribe_in(
16638 &leader_entity,
16639 window,
16640 move |_, leader, event, window, cx| {
16641 leader.read(cx).add_event_to_update_proto(
16642 event,
16643 &mut update.borrow_mut(),
16644 window,
16645 cx,
16646 );
16647 },
16648 )
16649 .detach();
16650
16651 cx.subscribe_in(
16652 &follower_entity,
16653 window,
16654 move |_, _, event: &EditorEvent, _window, _cx| {
16655 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
16656 *is_still_following.borrow_mut() = false;
16657 }
16658
16659 if let EditorEvent::BufferEdited = event {
16660 *follower_edit_event_count.borrow_mut() += 1;
16661 }
16662 },
16663 )
16664 .detach();
16665 }
16666 });
16667
16668 // Update the selections only
16669 _ = leader.update(cx, |leader, window, cx| {
16670 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16671 s.select_ranges([1..1])
16672 });
16673 });
16674 follower
16675 .update(cx, |follower, window, cx| {
16676 follower.apply_update_proto(
16677 &project,
16678 pending_update.borrow_mut().take().unwrap(),
16679 window,
16680 cx,
16681 )
16682 })
16683 .unwrap()
16684 .await
16685 .unwrap();
16686 _ = follower.update(cx, |follower, _, cx| {
16687 assert_eq!(
16688 follower.selections.ranges(&follower.display_snapshot(cx)),
16689 vec![1..1]
16690 );
16691 });
16692 assert!(*is_still_following.borrow());
16693 assert_eq!(*follower_edit_event_count.borrow(), 0);
16694
16695 // Update the scroll position only
16696 _ = leader.update(cx, |leader, window, cx| {
16697 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16698 });
16699 follower
16700 .update(cx, |follower, window, cx| {
16701 follower.apply_update_proto(
16702 &project,
16703 pending_update.borrow_mut().take().unwrap(),
16704 window,
16705 cx,
16706 )
16707 })
16708 .unwrap()
16709 .await
16710 .unwrap();
16711 assert_eq!(
16712 follower
16713 .update(cx, |follower, _, cx| follower.scroll_position(cx))
16714 .unwrap(),
16715 gpui::Point::new(1.5, 3.5)
16716 );
16717 assert!(*is_still_following.borrow());
16718 assert_eq!(*follower_edit_event_count.borrow(), 0);
16719
16720 // Update the selections and scroll position. The follower's scroll position is updated
16721 // via autoscroll, not via the leader's exact scroll position.
16722 _ = leader.update(cx, |leader, window, cx| {
16723 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16724 s.select_ranges([0..0])
16725 });
16726 leader.request_autoscroll(Autoscroll::newest(), cx);
16727 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16728 });
16729 follower
16730 .update(cx, |follower, window, cx| {
16731 follower.apply_update_proto(
16732 &project,
16733 pending_update.borrow_mut().take().unwrap(),
16734 window,
16735 cx,
16736 )
16737 })
16738 .unwrap()
16739 .await
16740 .unwrap();
16741 _ = follower.update(cx, |follower, _, cx| {
16742 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
16743 assert_eq!(
16744 follower.selections.ranges(&follower.display_snapshot(cx)),
16745 vec![0..0]
16746 );
16747 });
16748 assert!(*is_still_following.borrow());
16749
16750 // Creating a pending selection that precedes another selection
16751 _ = leader.update(cx, |leader, window, cx| {
16752 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16753 s.select_ranges([1..1])
16754 });
16755 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
16756 });
16757 follower
16758 .update(cx, |follower, window, cx| {
16759 follower.apply_update_proto(
16760 &project,
16761 pending_update.borrow_mut().take().unwrap(),
16762 window,
16763 cx,
16764 )
16765 })
16766 .unwrap()
16767 .await
16768 .unwrap();
16769 _ = follower.update(cx, |follower, _, cx| {
16770 assert_eq!(
16771 follower.selections.ranges(&follower.display_snapshot(cx)),
16772 vec![0..0, 1..1]
16773 );
16774 });
16775 assert!(*is_still_following.borrow());
16776
16777 // Extend the pending selection so that it surrounds another selection
16778 _ = leader.update(cx, |leader, window, cx| {
16779 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
16780 });
16781 follower
16782 .update(cx, |follower, window, cx| {
16783 follower.apply_update_proto(
16784 &project,
16785 pending_update.borrow_mut().take().unwrap(),
16786 window,
16787 cx,
16788 )
16789 })
16790 .unwrap()
16791 .await
16792 .unwrap();
16793 _ = follower.update(cx, |follower, _, cx| {
16794 assert_eq!(
16795 follower.selections.ranges(&follower.display_snapshot(cx)),
16796 vec![0..2]
16797 );
16798 });
16799
16800 // Scrolling locally breaks the follow
16801 _ = follower.update(cx, |follower, window, cx| {
16802 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
16803 follower.set_scroll_anchor(
16804 ScrollAnchor {
16805 anchor: top_anchor,
16806 offset: gpui::Point::new(0.0, 0.5),
16807 },
16808 window,
16809 cx,
16810 );
16811 });
16812 assert!(!(*is_still_following.borrow()));
16813}
16814
16815#[gpui::test]
16816async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
16817 init_test(cx, |_| {});
16818
16819 let fs = FakeFs::new(cx.executor());
16820 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16821 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16822 let pane = workspace
16823 .update(cx, |workspace, _, _| workspace.active_pane().clone())
16824 .unwrap();
16825
16826 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16827
16828 let leader = pane.update_in(cx, |_, window, cx| {
16829 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
16830 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
16831 });
16832
16833 // Start following the editor when it has no excerpts.
16834 let mut state_message =
16835 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16836 let workspace_entity = workspace.root(cx).unwrap();
16837 let follower_1 = cx
16838 .update_window(*workspace.deref(), |_, window, cx| {
16839 Editor::from_state_proto(
16840 workspace_entity,
16841 ViewId {
16842 creator: CollaboratorId::PeerId(PeerId::default()),
16843 id: 0,
16844 },
16845 &mut state_message,
16846 window,
16847 cx,
16848 )
16849 })
16850 .unwrap()
16851 .unwrap()
16852 .await
16853 .unwrap();
16854
16855 let update_message = Rc::new(RefCell::new(None));
16856 follower_1.update_in(cx, {
16857 let update = update_message.clone();
16858 |_, window, cx| {
16859 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
16860 leader.read(cx).add_event_to_update_proto(
16861 event,
16862 &mut update.borrow_mut(),
16863 window,
16864 cx,
16865 );
16866 })
16867 .detach();
16868 }
16869 });
16870
16871 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
16872 (
16873 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, false, cx),
16874 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, false, cx),
16875 )
16876 });
16877
16878 // Insert some excerpts.
16879 leader.update(cx, |leader, cx| {
16880 leader.buffer.update(cx, |multibuffer, cx| {
16881 multibuffer.set_excerpts_for_path(
16882 PathKey::with_sort_prefix(1, rel_path("b.txt").into_arc()),
16883 buffer_1.clone(),
16884 vec![
16885 Point::row_range(0..3),
16886 Point::row_range(1..6),
16887 Point::row_range(12..15),
16888 ],
16889 0,
16890 cx,
16891 );
16892 multibuffer.set_excerpts_for_path(
16893 PathKey::with_sort_prefix(1, rel_path("a.txt").into_arc()),
16894 buffer_2.clone(),
16895 vec![Point::row_range(0..6), Point::row_range(8..12)],
16896 0,
16897 cx,
16898 );
16899 });
16900 });
16901
16902 // Apply the update of adding the excerpts.
16903 follower_1
16904 .update_in(cx, |follower, window, cx| {
16905 follower.apply_update_proto(
16906 &project,
16907 update_message.borrow().clone().unwrap(),
16908 window,
16909 cx,
16910 )
16911 })
16912 .await
16913 .unwrap();
16914 assert_eq!(
16915 follower_1.update(cx, |editor, cx| editor.text(cx)),
16916 leader.update(cx, |editor, cx| editor.text(cx))
16917 );
16918 update_message.borrow_mut().take();
16919
16920 // Start following separately after it already has excerpts.
16921 let mut state_message =
16922 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16923 let workspace_entity = workspace.root(cx).unwrap();
16924 let follower_2 = cx
16925 .update_window(*workspace.deref(), |_, window, cx| {
16926 Editor::from_state_proto(
16927 workspace_entity,
16928 ViewId {
16929 creator: CollaboratorId::PeerId(PeerId::default()),
16930 id: 0,
16931 },
16932 &mut state_message,
16933 window,
16934 cx,
16935 )
16936 })
16937 .unwrap()
16938 .unwrap()
16939 .await
16940 .unwrap();
16941 assert_eq!(
16942 follower_2.update(cx, |editor, cx| editor.text(cx)),
16943 leader.update(cx, |editor, cx| editor.text(cx))
16944 );
16945
16946 // Remove some excerpts.
16947 leader.update(cx, |leader, cx| {
16948 leader.buffer.update(cx, |multibuffer, cx| {
16949 let excerpt_ids = multibuffer.excerpt_ids();
16950 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
16951 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
16952 });
16953 });
16954
16955 // Apply the update of removing the excerpts.
16956 follower_1
16957 .update_in(cx, |follower, window, cx| {
16958 follower.apply_update_proto(
16959 &project,
16960 update_message.borrow().clone().unwrap(),
16961 window,
16962 cx,
16963 )
16964 })
16965 .await
16966 .unwrap();
16967 follower_2
16968 .update_in(cx, |follower, window, cx| {
16969 follower.apply_update_proto(
16970 &project,
16971 update_message.borrow().clone().unwrap(),
16972 window,
16973 cx,
16974 )
16975 })
16976 .await
16977 .unwrap();
16978 update_message.borrow_mut().take();
16979 assert_eq!(
16980 follower_1.update(cx, |editor, cx| editor.text(cx)),
16981 leader.update(cx, |editor, cx| editor.text(cx))
16982 );
16983}
16984
16985#[gpui::test]
16986async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16987 init_test(cx, |_| {});
16988
16989 let mut cx = EditorTestContext::new(cx).await;
16990 let lsp_store =
16991 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
16992
16993 cx.set_state(indoc! {"
16994 ˇfn func(abc def: i32) -> u32 {
16995 }
16996 "});
16997
16998 cx.update(|_, cx| {
16999 lsp_store.update(cx, |lsp_store, cx| {
17000 lsp_store
17001 .update_diagnostics(
17002 LanguageServerId(0),
17003 lsp::PublishDiagnosticsParams {
17004 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
17005 version: None,
17006 diagnostics: vec![
17007 lsp::Diagnostic {
17008 range: lsp::Range::new(
17009 lsp::Position::new(0, 11),
17010 lsp::Position::new(0, 12),
17011 ),
17012 severity: Some(lsp::DiagnosticSeverity::ERROR),
17013 ..Default::default()
17014 },
17015 lsp::Diagnostic {
17016 range: lsp::Range::new(
17017 lsp::Position::new(0, 12),
17018 lsp::Position::new(0, 15),
17019 ),
17020 severity: Some(lsp::DiagnosticSeverity::ERROR),
17021 ..Default::default()
17022 },
17023 lsp::Diagnostic {
17024 range: lsp::Range::new(
17025 lsp::Position::new(0, 25),
17026 lsp::Position::new(0, 28),
17027 ),
17028 severity: Some(lsp::DiagnosticSeverity::ERROR),
17029 ..Default::default()
17030 },
17031 ],
17032 },
17033 None,
17034 DiagnosticSourceKind::Pushed,
17035 &[],
17036 cx,
17037 )
17038 .unwrap()
17039 });
17040 });
17041
17042 executor.run_until_parked();
17043
17044 cx.update_editor(|editor, window, cx| {
17045 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17046 });
17047
17048 cx.assert_editor_state(indoc! {"
17049 fn func(abc def: i32) -> ˇu32 {
17050 }
17051 "});
17052
17053 cx.update_editor(|editor, window, cx| {
17054 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17055 });
17056
17057 cx.assert_editor_state(indoc! {"
17058 fn func(abc ˇdef: i32) -> u32 {
17059 }
17060 "});
17061
17062 cx.update_editor(|editor, window, cx| {
17063 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17064 });
17065
17066 cx.assert_editor_state(indoc! {"
17067 fn func(abcˇ def: i32) -> u32 {
17068 }
17069 "});
17070
17071 cx.update_editor(|editor, window, cx| {
17072 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17073 });
17074
17075 cx.assert_editor_state(indoc! {"
17076 fn func(abc def: i32) -> ˇu32 {
17077 }
17078 "});
17079}
17080
17081#[gpui::test]
17082async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17083 init_test(cx, |_| {});
17084
17085 let mut cx = EditorTestContext::new(cx).await;
17086
17087 let diff_base = r#"
17088 use some::mod;
17089
17090 const A: u32 = 42;
17091
17092 fn main() {
17093 println!("hello");
17094
17095 println!("world");
17096 }
17097 "#
17098 .unindent();
17099
17100 // Edits are modified, removed, modified, added
17101 cx.set_state(
17102 &r#"
17103 use some::modified;
17104
17105 ˇ
17106 fn main() {
17107 println!("hello there");
17108
17109 println!("around the");
17110 println!("world");
17111 }
17112 "#
17113 .unindent(),
17114 );
17115
17116 cx.set_head_text(&diff_base);
17117 executor.run_until_parked();
17118
17119 cx.update_editor(|editor, window, cx| {
17120 //Wrap around the bottom of the buffer
17121 for _ in 0..3 {
17122 editor.go_to_next_hunk(&GoToHunk, window, cx);
17123 }
17124 });
17125
17126 cx.assert_editor_state(
17127 &r#"
17128 ˇuse some::modified;
17129
17130
17131 fn main() {
17132 println!("hello there");
17133
17134 println!("around the");
17135 println!("world");
17136 }
17137 "#
17138 .unindent(),
17139 );
17140
17141 cx.update_editor(|editor, window, cx| {
17142 //Wrap around the top of the buffer
17143 for _ in 0..2 {
17144 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17145 }
17146 });
17147
17148 cx.assert_editor_state(
17149 &r#"
17150 use some::modified;
17151
17152
17153 fn main() {
17154 ˇ println!("hello there");
17155
17156 println!("around the");
17157 println!("world");
17158 }
17159 "#
17160 .unindent(),
17161 );
17162
17163 cx.update_editor(|editor, window, cx| {
17164 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17165 });
17166
17167 cx.assert_editor_state(
17168 &r#"
17169 use some::modified;
17170
17171 ˇ
17172 fn main() {
17173 println!("hello there");
17174
17175 println!("around the");
17176 println!("world");
17177 }
17178 "#
17179 .unindent(),
17180 );
17181
17182 cx.update_editor(|editor, window, cx| {
17183 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17184 });
17185
17186 cx.assert_editor_state(
17187 &r#"
17188 ˇuse some::modified;
17189
17190
17191 fn main() {
17192 println!("hello there");
17193
17194 println!("around the");
17195 println!("world");
17196 }
17197 "#
17198 .unindent(),
17199 );
17200
17201 cx.update_editor(|editor, window, cx| {
17202 for _ in 0..2 {
17203 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17204 }
17205 });
17206
17207 cx.assert_editor_state(
17208 &r#"
17209 use some::modified;
17210
17211
17212 fn main() {
17213 ˇ println!("hello there");
17214
17215 println!("around the");
17216 println!("world");
17217 }
17218 "#
17219 .unindent(),
17220 );
17221
17222 cx.update_editor(|editor, window, cx| {
17223 editor.fold(&Fold, window, cx);
17224 });
17225
17226 cx.update_editor(|editor, window, cx| {
17227 editor.go_to_next_hunk(&GoToHunk, window, cx);
17228 });
17229
17230 cx.assert_editor_state(
17231 &r#"
17232 ˇuse some::modified;
17233
17234
17235 fn main() {
17236 println!("hello there");
17237
17238 println!("around the");
17239 println!("world");
17240 }
17241 "#
17242 .unindent(),
17243 );
17244}
17245
17246#[test]
17247fn test_split_words() {
17248 fn split(text: &str) -> Vec<&str> {
17249 split_words(text).collect()
17250 }
17251
17252 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
17253 assert_eq!(split("hello_world"), &["hello_", "world"]);
17254 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
17255 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
17256 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
17257 assert_eq!(split("helloworld"), &["helloworld"]);
17258
17259 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
17260}
17261
17262#[gpui::test]
17263async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
17264 init_test(cx, |_| {});
17265
17266 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
17267 let mut assert = |before, after| {
17268 let _state_context = cx.set_state(before);
17269 cx.run_until_parked();
17270 cx.update_editor(|editor, window, cx| {
17271 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
17272 });
17273 cx.run_until_parked();
17274 cx.assert_editor_state(after);
17275 };
17276
17277 // Outside bracket jumps to outside of matching bracket
17278 assert("console.logˇ(var);", "console.log(var)ˇ;");
17279 assert("console.log(var)ˇ;", "console.logˇ(var);");
17280
17281 // Inside bracket jumps to inside of matching bracket
17282 assert("console.log(ˇvar);", "console.log(varˇ);");
17283 assert("console.log(varˇ);", "console.log(ˇvar);");
17284
17285 // When outside a bracket and inside, favor jumping to the inside bracket
17286 assert(
17287 "console.log('foo', [1, 2, 3]ˇ);",
17288 "console.log(ˇ'foo', [1, 2, 3]);",
17289 );
17290 assert(
17291 "console.log(ˇ'foo', [1, 2, 3]);",
17292 "console.log('foo', [1, 2, 3]ˇ);",
17293 );
17294
17295 // Bias forward if two options are equally likely
17296 assert(
17297 "let result = curried_fun()ˇ();",
17298 "let result = curried_fun()()ˇ;",
17299 );
17300
17301 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
17302 assert(
17303 indoc! {"
17304 function test() {
17305 console.log('test')ˇ
17306 }"},
17307 indoc! {"
17308 function test() {
17309 console.logˇ('test')
17310 }"},
17311 );
17312}
17313
17314#[gpui::test]
17315async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
17316 init_test(cx, |_| {});
17317
17318 let fs = FakeFs::new(cx.executor());
17319 fs.insert_tree(
17320 path!("/a"),
17321 json!({
17322 "main.rs": "fn main() { let a = 5; }",
17323 "other.rs": "// Test file",
17324 }),
17325 )
17326 .await;
17327 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17328
17329 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17330 language_registry.add(Arc::new(Language::new(
17331 LanguageConfig {
17332 name: "Rust".into(),
17333 matcher: LanguageMatcher {
17334 path_suffixes: vec!["rs".to_string()],
17335 ..Default::default()
17336 },
17337 brackets: BracketPairConfig {
17338 pairs: vec![BracketPair {
17339 start: "{".to_string(),
17340 end: "}".to_string(),
17341 close: true,
17342 surround: true,
17343 newline: true,
17344 }],
17345 disabled_scopes_by_bracket_ix: Vec::new(),
17346 },
17347 ..Default::default()
17348 },
17349 Some(tree_sitter_rust::LANGUAGE.into()),
17350 )));
17351 let mut fake_servers = language_registry.register_fake_lsp(
17352 "Rust",
17353 FakeLspAdapter {
17354 capabilities: lsp::ServerCapabilities {
17355 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
17356 first_trigger_character: "{".to_string(),
17357 more_trigger_character: None,
17358 }),
17359 ..Default::default()
17360 },
17361 ..Default::default()
17362 },
17363 );
17364
17365 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17366
17367 let cx = &mut VisualTestContext::from_window(*workspace, cx);
17368
17369 let worktree_id = workspace
17370 .update(cx, |workspace, _, cx| {
17371 workspace.project().update(cx, |project, cx| {
17372 project.worktrees(cx).next().unwrap().read(cx).id()
17373 })
17374 })
17375 .unwrap();
17376
17377 let buffer = project
17378 .update(cx, |project, cx| {
17379 project.open_local_buffer(path!("/a/main.rs"), cx)
17380 })
17381 .await
17382 .unwrap();
17383 let editor_handle = workspace
17384 .update(cx, |workspace, window, cx| {
17385 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
17386 })
17387 .unwrap()
17388 .await
17389 .unwrap()
17390 .downcast::<Editor>()
17391 .unwrap();
17392
17393 cx.executor().start_waiting();
17394 let fake_server = fake_servers.next().await.unwrap();
17395
17396 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
17397 |params, _| async move {
17398 assert_eq!(
17399 params.text_document_position.text_document.uri,
17400 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
17401 );
17402 assert_eq!(
17403 params.text_document_position.position,
17404 lsp::Position::new(0, 21),
17405 );
17406
17407 Ok(Some(vec![lsp::TextEdit {
17408 new_text: "]".to_string(),
17409 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17410 }]))
17411 },
17412 );
17413
17414 editor_handle.update_in(cx, |editor, window, cx| {
17415 window.focus(&editor.focus_handle(cx));
17416 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17417 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
17418 });
17419 editor.handle_input("{", window, cx);
17420 });
17421
17422 cx.executor().run_until_parked();
17423
17424 buffer.update(cx, |buffer, _| {
17425 assert_eq!(
17426 buffer.text(),
17427 "fn main() { let a = {5}; }",
17428 "No extra braces from on type formatting should appear in the buffer"
17429 )
17430 });
17431}
17432
17433#[gpui::test(iterations = 20, seeds(31))]
17434async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
17435 init_test(cx, |_| {});
17436
17437 let mut cx = EditorLspTestContext::new_rust(
17438 lsp::ServerCapabilities {
17439 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
17440 first_trigger_character: ".".to_string(),
17441 more_trigger_character: None,
17442 }),
17443 ..Default::default()
17444 },
17445 cx,
17446 )
17447 .await;
17448
17449 cx.update_buffer(|buffer, _| {
17450 // This causes autoindent to be async.
17451 buffer.set_sync_parse_timeout(Duration::ZERO)
17452 });
17453
17454 cx.set_state("fn c() {\n d()ˇ\n}\n");
17455 cx.simulate_keystroke("\n");
17456 cx.run_until_parked();
17457
17458 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
17459 let mut request =
17460 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
17461 let buffer_cloned = buffer_cloned.clone();
17462 async move {
17463 buffer_cloned.update(&mut cx, |buffer, _| {
17464 assert_eq!(
17465 buffer.text(),
17466 "fn c() {\n d()\n .\n}\n",
17467 "OnTypeFormatting should triggered after autoindent applied"
17468 )
17469 })?;
17470
17471 Ok(Some(vec![]))
17472 }
17473 });
17474
17475 cx.simulate_keystroke(".");
17476 cx.run_until_parked();
17477
17478 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
17479 assert!(request.next().await.is_some());
17480 request.close();
17481 assert!(request.next().await.is_none());
17482}
17483
17484#[gpui::test]
17485async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
17486 init_test(cx, |_| {});
17487
17488 let fs = FakeFs::new(cx.executor());
17489 fs.insert_tree(
17490 path!("/a"),
17491 json!({
17492 "main.rs": "fn main() { let a = 5; }",
17493 "other.rs": "// Test file",
17494 }),
17495 )
17496 .await;
17497
17498 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17499
17500 let server_restarts = Arc::new(AtomicUsize::new(0));
17501 let closure_restarts = Arc::clone(&server_restarts);
17502 let language_server_name = "test language server";
17503 let language_name: LanguageName = "Rust".into();
17504
17505 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17506 language_registry.add(Arc::new(Language::new(
17507 LanguageConfig {
17508 name: language_name.clone(),
17509 matcher: LanguageMatcher {
17510 path_suffixes: vec!["rs".to_string()],
17511 ..Default::default()
17512 },
17513 ..Default::default()
17514 },
17515 Some(tree_sitter_rust::LANGUAGE.into()),
17516 )));
17517 let mut fake_servers = language_registry.register_fake_lsp(
17518 "Rust",
17519 FakeLspAdapter {
17520 name: language_server_name,
17521 initialization_options: Some(json!({
17522 "testOptionValue": true
17523 })),
17524 initializer: Some(Box::new(move |fake_server| {
17525 let task_restarts = Arc::clone(&closure_restarts);
17526 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
17527 task_restarts.fetch_add(1, atomic::Ordering::Release);
17528 futures::future::ready(Ok(()))
17529 });
17530 })),
17531 ..Default::default()
17532 },
17533 );
17534
17535 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17536 let _buffer = project
17537 .update(cx, |project, cx| {
17538 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
17539 })
17540 .await
17541 .unwrap();
17542 let _fake_server = fake_servers.next().await.unwrap();
17543 update_test_language_settings(cx, |language_settings| {
17544 language_settings.languages.0.insert(
17545 language_name.clone().0,
17546 LanguageSettingsContent {
17547 tab_size: NonZeroU32::new(8),
17548 ..Default::default()
17549 },
17550 );
17551 });
17552 cx.executor().run_until_parked();
17553 assert_eq!(
17554 server_restarts.load(atomic::Ordering::Acquire),
17555 0,
17556 "Should not restart LSP server on an unrelated change"
17557 );
17558
17559 update_test_project_settings(cx, |project_settings| {
17560 project_settings.lsp.insert(
17561 "Some other server name".into(),
17562 LspSettings {
17563 binary: None,
17564 settings: None,
17565 initialization_options: Some(json!({
17566 "some other init value": false
17567 })),
17568 enable_lsp_tasks: false,
17569 fetch: None,
17570 },
17571 );
17572 });
17573 cx.executor().run_until_parked();
17574 assert_eq!(
17575 server_restarts.load(atomic::Ordering::Acquire),
17576 0,
17577 "Should not restart LSP server on an unrelated LSP settings change"
17578 );
17579
17580 update_test_project_settings(cx, |project_settings| {
17581 project_settings.lsp.insert(
17582 language_server_name.into(),
17583 LspSettings {
17584 binary: None,
17585 settings: None,
17586 initialization_options: Some(json!({
17587 "anotherInitValue": false
17588 })),
17589 enable_lsp_tasks: false,
17590 fetch: None,
17591 },
17592 );
17593 });
17594 cx.executor().run_until_parked();
17595 assert_eq!(
17596 server_restarts.load(atomic::Ordering::Acquire),
17597 1,
17598 "Should restart LSP server on a related LSP settings change"
17599 );
17600
17601 update_test_project_settings(cx, |project_settings| {
17602 project_settings.lsp.insert(
17603 language_server_name.into(),
17604 LspSettings {
17605 binary: None,
17606 settings: None,
17607 initialization_options: Some(json!({
17608 "anotherInitValue": false
17609 })),
17610 enable_lsp_tasks: false,
17611 fetch: None,
17612 },
17613 );
17614 });
17615 cx.executor().run_until_parked();
17616 assert_eq!(
17617 server_restarts.load(atomic::Ordering::Acquire),
17618 1,
17619 "Should not restart LSP server on a related LSP settings change that is the same"
17620 );
17621
17622 update_test_project_settings(cx, |project_settings| {
17623 project_settings.lsp.insert(
17624 language_server_name.into(),
17625 LspSettings {
17626 binary: None,
17627 settings: None,
17628 initialization_options: None,
17629 enable_lsp_tasks: false,
17630 fetch: None,
17631 },
17632 );
17633 });
17634 cx.executor().run_until_parked();
17635 assert_eq!(
17636 server_restarts.load(atomic::Ordering::Acquire),
17637 2,
17638 "Should restart LSP server on another related LSP settings change"
17639 );
17640}
17641
17642#[gpui::test]
17643async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
17644 init_test(cx, |_| {});
17645
17646 let mut cx = EditorLspTestContext::new_rust(
17647 lsp::ServerCapabilities {
17648 completion_provider: Some(lsp::CompletionOptions {
17649 trigger_characters: Some(vec![".".to_string()]),
17650 resolve_provider: Some(true),
17651 ..Default::default()
17652 }),
17653 ..Default::default()
17654 },
17655 cx,
17656 )
17657 .await;
17658
17659 cx.set_state("fn main() { let a = 2ˇ; }");
17660 cx.simulate_keystroke(".");
17661 let completion_item = lsp::CompletionItem {
17662 label: "some".into(),
17663 kind: Some(lsp::CompletionItemKind::SNIPPET),
17664 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
17665 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
17666 kind: lsp::MarkupKind::Markdown,
17667 value: "```rust\nSome(2)\n```".to_string(),
17668 })),
17669 deprecated: Some(false),
17670 sort_text: Some("fffffff2".to_string()),
17671 filter_text: Some("some".to_string()),
17672 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
17673 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17674 range: lsp::Range {
17675 start: lsp::Position {
17676 line: 0,
17677 character: 22,
17678 },
17679 end: lsp::Position {
17680 line: 0,
17681 character: 22,
17682 },
17683 },
17684 new_text: "Some(2)".to_string(),
17685 })),
17686 additional_text_edits: Some(vec![lsp::TextEdit {
17687 range: lsp::Range {
17688 start: lsp::Position {
17689 line: 0,
17690 character: 20,
17691 },
17692 end: lsp::Position {
17693 line: 0,
17694 character: 22,
17695 },
17696 },
17697 new_text: "".to_string(),
17698 }]),
17699 ..Default::default()
17700 };
17701
17702 let closure_completion_item = completion_item.clone();
17703 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17704 let task_completion_item = closure_completion_item.clone();
17705 async move {
17706 Ok(Some(lsp::CompletionResponse::Array(vec![
17707 task_completion_item,
17708 ])))
17709 }
17710 });
17711
17712 request.next().await;
17713
17714 cx.condition(|editor, _| editor.context_menu_visible())
17715 .await;
17716 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
17717 editor
17718 .confirm_completion(&ConfirmCompletion::default(), window, cx)
17719 .unwrap()
17720 });
17721 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
17722
17723 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
17724 let task_completion_item = completion_item.clone();
17725 async move { Ok(task_completion_item) }
17726 })
17727 .next()
17728 .await
17729 .unwrap();
17730 apply_additional_edits.await.unwrap();
17731 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
17732}
17733
17734#[gpui::test]
17735async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
17736 init_test(cx, |_| {});
17737
17738 let mut cx = EditorLspTestContext::new_rust(
17739 lsp::ServerCapabilities {
17740 completion_provider: Some(lsp::CompletionOptions {
17741 trigger_characters: Some(vec![".".to_string()]),
17742 resolve_provider: Some(true),
17743 ..Default::default()
17744 }),
17745 ..Default::default()
17746 },
17747 cx,
17748 )
17749 .await;
17750
17751 cx.set_state("fn main() { let a = 2ˇ; }");
17752 cx.simulate_keystroke(".");
17753
17754 let item1 = lsp::CompletionItem {
17755 label: "method id()".to_string(),
17756 filter_text: Some("id".to_string()),
17757 detail: None,
17758 documentation: None,
17759 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17760 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17761 new_text: ".id".to_string(),
17762 })),
17763 ..lsp::CompletionItem::default()
17764 };
17765
17766 let item2 = lsp::CompletionItem {
17767 label: "other".to_string(),
17768 filter_text: Some("other".to_string()),
17769 detail: None,
17770 documentation: None,
17771 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17772 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17773 new_text: ".other".to_string(),
17774 })),
17775 ..lsp::CompletionItem::default()
17776 };
17777
17778 let item1 = item1.clone();
17779 cx.set_request_handler::<lsp::request::Completion, _, _>({
17780 let item1 = item1.clone();
17781 move |_, _, _| {
17782 let item1 = item1.clone();
17783 let item2 = item2.clone();
17784 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
17785 }
17786 })
17787 .next()
17788 .await;
17789
17790 cx.condition(|editor, _| editor.context_menu_visible())
17791 .await;
17792 cx.update_editor(|editor, _, _| {
17793 let context_menu = editor.context_menu.borrow_mut();
17794 let context_menu = context_menu
17795 .as_ref()
17796 .expect("Should have the context menu deployed");
17797 match context_menu {
17798 CodeContextMenu::Completions(completions_menu) => {
17799 let completions = completions_menu.completions.borrow_mut();
17800 assert_eq!(
17801 completions
17802 .iter()
17803 .map(|completion| &completion.label.text)
17804 .collect::<Vec<_>>(),
17805 vec!["method id()", "other"]
17806 )
17807 }
17808 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17809 }
17810 });
17811
17812 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
17813 let item1 = item1.clone();
17814 move |_, item_to_resolve, _| {
17815 let item1 = item1.clone();
17816 async move {
17817 if item1 == item_to_resolve {
17818 Ok(lsp::CompletionItem {
17819 label: "method id()".to_string(),
17820 filter_text: Some("id".to_string()),
17821 detail: Some("Now resolved!".to_string()),
17822 documentation: Some(lsp::Documentation::String("Docs".to_string())),
17823 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17824 range: lsp::Range::new(
17825 lsp::Position::new(0, 22),
17826 lsp::Position::new(0, 22),
17827 ),
17828 new_text: ".id".to_string(),
17829 })),
17830 ..lsp::CompletionItem::default()
17831 })
17832 } else {
17833 Ok(item_to_resolve)
17834 }
17835 }
17836 }
17837 })
17838 .next()
17839 .await
17840 .unwrap();
17841 cx.run_until_parked();
17842
17843 cx.update_editor(|editor, window, cx| {
17844 editor.context_menu_next(&Default::default(), window, cx);
17845 });
17846
17847 cx.update_editor(|editor, _, _| {
17848 let context_menu = editor.context_menu.borrow_mut();
17849 let context_menu = context_menu
17850 .as_ref()
17851 .expect("Should have the context menu deployed");
17852 match context_menu {
17853 CodeContextMenu::Completions(completions_menu) => {
17854 let completions = completions_menu.completions.borrow_mut();
17855 assert_eq!(
17856 completions
17857 .iter()
17858 .map(|completion| &completion.label.text)
17859 .collect::<Vec<_>>(),
17860 vec!["method id() Now resolved!", "other"],
17861 "Should update first completion label, but not second as the filter text did not match."
17862 );
17863 }
17864 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17865 }
17866 });
17867}
17868
17869#[gpui::test]
17870async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
17871 init_test(cx, |_| {});
17872 let mut cx = EditorLspTestContext::new_rust(
17873 lsp::ServerCapabilities {
17874 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
17875 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
17876 completion_provider: Some(lsp::CompletionOptions {
17877 resolve_provider: Some(true),
17878 ..Default::default()
17879 }),
17880 ..Default::default()
17881 },
17882 cx,
17883 )
17884 .await;
17885 cx.set_state(indoc! {"
17886 struct TestStruct {
17887 field: i32
17888 }
17889
17890 fn mainˇ() {
17891 let unused_var = 42;
17892 let test_struct = TestStruct { field: 42 };
17893 }
17894 "});
17895 let symbol_range = cx.lsp_range(indoc! {"
17896 struct TestStruct {
17897 field: i32
17898 }
17899
17900 «fn main»() {
17901 let unused_var = 42;
17902 let test_struct = TestStruct { field: 42 };
17903 }
17904 "});
17905 let mut hover_requests =
17906 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
17907 Ok(Some(lsp::Hover {
17908 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
17909 kind: lsp::MarkupKind::Markdown,
17910 value: "Function documentation".to_string(),
17911 }),
17912 range: Some(symbol_range),
17913 }))
17914 });
17915
17916 // Case 1: Test that code action menu hide hover popover
17917 cx.dispatch_action(Hover);
17918 hover_requests.next().await;
17919 cx.condition(|editor, _| editor.hover_state.visible()).await;
17920 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
17921 move |_, _, _| async move {
17922 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
17923 lsp::CodeAction {
17924 title: "Remove unused variable".to_string(),
17925 kind: Some(CodeActionKind::QUICKFIX),
17926 edit: Some(lsp::WorkspaceEdit {
17927 changes: Some(
17928 [(
17929 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
17930 vec![lsp::TextEdit {
17931 range: lsp::Range::new(
17932 lsp::Position::new(5, 4),
17933 lsp::Position::new(5, 27),
17934 ),
17935 new_text: "".to_string(),
17936 }],
17937 )]
17938 .into_iter()
17939 .collect(),
17940 ),
17941 ..Default::default()
17942 }),
17943 ..Default::default()
17944 },
17945 )]))
17946 },
17947 );
17948 cx.update_editor(|editor, window, cx| {
17949 editor.toggle_code_actions(
17950 &ToggleCodeActions {
17951 deployed_from: None,
17952 quick_launch: false,
17953 },
17954 window,
17955 cx,
17956 );
17957 });
17958 code_action_requests.next().await;
17959 cx.run_until_parked();
17960 cx.condition(|editor, _| editor.context_menu_visible())
17961 .await;
17962 cx.update_editor(|editor, _, _| {
17963 assert!(
17964 !editor.hover_state.visible(),
17965 "Hover popover should be hidden when code action menu is shown"
17966 );
17967 // Hide code actions
17968 editor.context_menu.take();
17969 });
17970
17971 // Case 2: Test that code completions hide hover popover
17972 cx.dispatch_action(Hover);
17973 hover_requests.next().await;
17974 cx.condition(|editor, _| editor.hover_state.visible()).await;
17975 let counter = Arc::new(AtomicUsize::new(0));
17976 let mut completion_requests =
17977 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17978 let counter = counter.clone();
17979 async move {
17980 counter.fetch_add(1, atomic::Ordering::Release);
17981 Ok(Some(lsp::CompletionResponse::Array(vec![
17982 lsp::CompletionItem {
17983 label: "main".into(),
17984 kind: Some(lsp::CompletionItemKind::FUNCTION),
17985 detail: Some("() -> ()".to_string()),
17986 ..Default::default()
17987 },
17988 lsp::CompletionItem {
17989 label: "TestStruct".into(),
17990 kind: Some(lsp::CompletionItemKind::STRUCT),
17991 detail: Some("struct TestStruct".to_string()),
17992 ..Default::default()
17993 },
17994 ])))
17995 }
17996 });
17997 cx.update_editor(|editor, window, cx| {
17998 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
17999 });
18000 completion_requests.next().await;
18001 cx.condition(|editor, _| editor.context_menu_visible())
18002 .await;
18003 cx.update_editor(|editor, _, _| {
18004 assert!(
18005 !editor.hover_state.visible(),
18006 "Hover popover should be hidden when completion menu is shown"
18007 );
18008 });
18009}
18010
18011#[gpui::test]
18012async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
18013 init_test(cx, |_| {});
18014
18015 let mut cx = EditorLspTestContext::new_rust(
18016 lsp::ServerCapabilities {
18017 completion_provider: Some(lsp::CompletionOptions {
18018 trigger_characters: Some(vec![".".to_string()]),
18019 resolve_provider: Some(true),
18020 ..Default::default()
18021 }),
18022 ..Default::default()
18023 },
18024 cx,
18025 )
18026 .await;
18027
18028 cx.set_state("fn main() { let a = 2ˇ; }");
18029 cx.simulate_keystroke(".");
18030
18031 let unresolved_item_1 = lsp::CompletionItem {
18032 label: "id".to_string(),
18033 filter_text: Some("id".to_string()),
18034 detail: None,
18035 documentation: None,
18036 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18037 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18038 new_text: ".id".to_string(),
18039 })),
18040 ..lsp::CompletionItem::default()
18041 };
18042 let resolved_item_1 = lsp::CompletionItem {
18043 additional_text_edits: Some(vec![lsp::TextEdit {
18044 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
18045 new_text: "!!".to_string(),
18046 }]),
18047 ..unresolved_item_1.clone()
18048 };
18049 let unresolved_item_2 = lsp::CompletionItem {
18050 label: "other".to_string(),
18051 filter_text: Some("other".to_string()),
18052 detail: None,
18053 documentation: None,
18054 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18055 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18056 new_text: ".other".to_string(),
18057 })),
18058 ..lsp::CompletionItem::default()
18059 };
18060 let resolved_item_2 = lsp::CompletionItem {
18061 additional_text_edits: Some(vec![lsp::TextEdit {
18062 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
18063 new_text: "??".to_string(),
18064 }]),
18065 ..unresolved_item_2.clone()
18066 };
18067
18068 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
18069 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
18070 cx.lsp
18071 .server
18072 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
18073 let unresolved_item_1 = unresolved_item_1.clone();
18074 let resolved_item_1 = resolved_item_1.clone();
18075 let unresolved_item_2 = unresolved_item_2.clone();
18076 let resolved_item_2 = resolved_item_2.clone();
18077 let resolve_requests_1 = resolve_requests_1.clone();
18078 let resolve_requests_2 = resolve_requests_2.clone();
18079 move |unresolved_request, _| {
18080 let unresolved_item_1 = unresolved_item_1.clone();
18081 let resolved_item_1 = resolved_item_1.clone();
18082 let unresolved_item_2 = unresolved_item_2.clone();
18083 let resolved_item_2 = resolved_item_2.clone();
18084 let resolve_requests_1 = resolve_requests_1.clone();
18085 let resolve_requests_2 = resolve_requests_2.clone();
18086 async move {
18087 if unresolved_request == unresolved_item_1 {
18088 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
18089 Ok(resolved_item_1.clone())
18090 } else if unresolved_request == unresolved_item_2 {
18091 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
18092 Ok(resolved_item_2.clone())
18093 } else {
18094 panic!("Unexpected completion item {unresolved_request:?}")
18095 }
18096 }
18097 }
18098 })
18099 .detach();
18100
18101 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18102 let unresolved_item_1 = unresolved_item_1.clone();
18103 let unresolved_item_2 = unresolved_item_2.clone();
18104 async move {
18105 Ok(Some(lsp::CompletionResponse::Array(vec![
18106 unresolved_item_1,
18107 unresolved_item_2,
18108 ])))
18109 }
18110 })
18111 .next()
18112 .await;
18113
18114 cx.condition(|editor, _| editor.context_menu_visible())
18115 .await;
18116 cx.update_editor(|editor, _, _| {
18117 let context_menu = editor.context_menu.borrow_mut();
18118 let context_menu = context_menu
18119 .as_ref()
18120 .expect("Should have the context menu deployed");
18121 match context_menu {
18122 CodeContextMenu::Completions(completions_menu) => {
18123 let completions = completions_menu.completions.borrow_mut();
18124 assert_eq!(
18125 completions
18126 .iter()
18127 .map(|completion| &completion.label.text)
18128 .collect::<Vec<_>>(),
18129 vec!["id", "other"]
18130 )
18131 }
18132 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
18133 }
18134 });
18135 cx.run_until_parked();
18136
18137 cx.update_editor(|editor, window, cx| {
18138 editor.context_menu_next(&ContextMenuNext, window, cx);
18139 });
18140 cx.run_until_parked();
18141 cx.update_editor(|editor, window, cx| {
18142 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
18143 });
18144 cx.run_until_parked();
18145 cx.update_editor(|editor, window, cx| {
18146 editor.context_menu_next(&ContextMenuNext, window, cx);
18147 });
18148 cx.run_until_parked();
18149 cx.update_editor(|editor, window, cx| {
18150 editor
18151 .compose_completion(&ComposeCompletion::default(), window, cx)
18152 .expect("No task returned")
18153 })
18154 .await
18155 .expect("Completion failed");
18156 cx.run_until_parked();
18157
18158 cx.update_editor(|editor, _, cx| {
18159 assert_eq!(
18160 resolve_requests_1.load(atomic::Ordering::Acquire),
18161 1,
18162 "Should always resolve once despite multiple selections"
18163 );
18164 assert_eq!(
18165 resolve_requests_2.load(atomic::Ordering::Acquire),
18166 1,
18167 "Should always resolve once after multiple selections and applying the completion"
18168 );
18169 assert_eq!(
18170 editor.text(cx),
18171 "fn main() { let a = ??.other; }",
18172 "Should use resolved data when applying the completion"
18173 );
18174 });
18175}
18176
18177#[gpui::test]
18178async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
18179 init_test(cx, |_| {});
18180
18181 let item_0 = lsp::CompletionItem {
18182 label: "abs".into(),
18183 insert_text: Some("abs".into()),
18184 data: Some(json!({ "very": "special"})),
18185 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
18186 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
18187 lsp::InsertReplaceEdit {
18188 new_text: "abs".to_string(),
18189 insert: lsp::Range::default(),
18190 replace: lsp::Range::default(),
18191 },
18192 )),
18193 ..lsp::CompletionItem::default()
18194 };
18195 let items = iter::once(item_0.clone())
18196 .chain((11..51).map(|i| lsp::CompletionItem {
18197 label: format!("item_{}", i),
18198 insert_text: Some(format!("item_{}", i)),
18199 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
18200 ..lsp::CompletionItem::default()
18201 }))
18202 .collect::<Vec<_>>();
18203
18204 let default_commit_characters = vec!["?".to_string()];
18205 let default_data = json!({ "default": "data"});
18206 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
18207 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
18208 let default_edit_range = lsp::Range {
18209 start: lsp::Position {
18210 line: 0,
18211 character: 5,
18212 },
18213 end: lsp::Position {
18214 line: 0,
18215 character: 5,
18216 },
18217 };
18218
18219 let mut cx = EditorLspTestContext::new_rust(
18220 lsp::ServerCapabilities {
18221 completion_provider: Some(lsp::CompletionOptions {
18222 trigger_characters: Some(vec![".".to_string()]),
18223 resolve_provider: Some(true),
18224 ..Default::default()
18225 }),
18226 ..Default::default()
18227 },
18228 cx,
18229 )
18230 .await;
18231
18232 cx.set_state("fn main() { let a = 2ˇ; }");
18233 cx.simulate_keystroke(".");
18234
18235 let completion_data = default_data.clone();
18236 let completion_characters = default_commit_characters.clone();
18237 let completion_items = items.clone();
18238 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18239 let default_data = completion_data.clone();
18240 let default_commit_characters = completion_characters.clone();
18241 let items = completion_items.clone();
18242 async move {
18243 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
18244 items,
18245 item_defaults: Some(lsp::CompletionListItemDefaults {
18246 data: Some(default_data.clone()),
18247 commit_characters: Some(default_commit_characters.clone()),
18248 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
18249 default_edit_range,
18250 )),
18251 insert_text_format: Some(default_insert_text_format),
18252 insert_text_mode: Some(default_insert_text_mode),
18253 }),
18254 ..lsp::CompletionList::default()
18255 })))
18256 }
18257 })
18258 .next()
18259 .await;
18260
18261 let resolved_items = Arc::new(Mutex::new(Vec::new()));
18262 cx.lsp
18263 .server
18264 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
18265 let closure_resolved_items = resolved_items.clone();
18266 move |item_to_resolve, _| {
18267 let closure_resolved_items = closure_resolved_items.clone();
18268 async move {
18269 closure_resolved_items.lock().push(item_to_resolve.clone());
18270 Ok(item_to_resolve)
18271 }
18272 }
18273 })
18274 .detach();
18275
18276 cx.condition(|editor, _| editor.context_menu_visible())
18277 .await;
18278 cx.run_until_parked();
18279 cx.update_editor(|editor, _, _| {
18280 let menu = editor.context_menu.borrow_mut();
18281 match menu.as_ref().expect("should have the completions menu") {
18282 CodeContextMenu::Completions(completions_menu) => {
18283 assert_eq!(
18284 completions_menu
18285 .entries
18286 .borrow()
18287 .iter()
18288 .map(|mat| mat.string.clone())
18289 .collect::<Vec<String>>(),
18290 items
18291 .iter()
18292 .map(|completion| completion.label.clone())
18293 .collect::<Vec<String>>()
18294 );
18295 }
18296 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
18297 }
18298 });
18299 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
18300 // with 4 from the end.
18301 assert_eq!(
18302 *resolved_items.lock(),
18303 [&items[0..16], &items[items.len() - 4..items.len()]]
18304 .concat()
18305 .iter()
18306 .cloned()
18307 .map(|mut item| {
18308 if item.data.is_none() {
18309 item.data = Some(default_data.clone());
18310 }
18311 item
18312 })
18313 .collect::<Vec<lsp::CompletionItem>>(),
18314 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
18315 );
18316 resolved_items.lock().clear();
18317
18318 cx.update_editor(|editor, window, cx| {
18319 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
18320 });
18321 cx.run_until_parked();
18322 // Completions that have already been resolved are skipped.
18323 assert_eq!(
18324 *resolved_items.lock(),
18325 items[items.len() - 17..items.len() - 4]
18326 .iter()
18327 .cloned()
18328 .map(|mut item| {
18329 if item.data.is_none() {
18330 item.data = Some(default_data.clone());
18331 }
18332 item
18333 })
18334 .collect::<Vec<lsp::CompletionItem>>()
18335 );
18336 resolved_items.lock().clear();
18337}
18338
18339#[gpui::test]
18340async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
18341 init_test(cx, |_| {});
18342
18343 let mut cx = EditorLspTestContext::new(
18344 Language::new(
18345 LanguageConfig {
18346 matcher: LanguageMatcher {
18347 path_suffixes: vec!["jsx".into()],
18348 ..Default::default()
18349 },
18350 overrides: [(
18351 "element".into(),
18352 LanguageConfigOverride {
18353 completion_query_characters: Override::Set(['-'].into_iter().collect()),
18354 ..Default::default()
18355 },
18356 )]
18357 .into_iter()
18358 .collect(),
18359 ..Default::default()
18360 },
18361 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
18362 )
18363 .with_override_query("(jsx_self_closing_element) @element")
18364 .unwrap(),
18365 lsp::ServerCapabilities {
18366 completion_provider: Some(lsp::CompletionOptions {
18367 trigger_characters: Some(vec![":".to_string()]),
18368 ..Default::default()
18369 }),
18370 ..Default::default()
18371 },
18372 cx,
18373 )
18374 .await;
18375
18376 cx.lsp
18377 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
18378 Ok(Some(lsp::CompletionResponse::Array(vec![
18379 lsp::CompletionItem {
18380 label: "bg-blue".into(),
18381 ..Default::default()
18382 },
18383 lsp::CompletionItem {
18384 label: "bg-red".into(),
18385 ..Default::default()
18386 },
18387 lsp::CompletionItem {
18388 label: "bg-yellow".into(),
18389 ..Default::default()
18390 },
18391 ])))
18392 });
18393
18394 cx.set_state(r#"<p class="bgˇ" />"#);
18395
18396 // Trigger completion when typing a dash, because the dash is an extra
18397 // word character in the 'element' scope, which contains the cursor.
18398 cx.simulate_keystroke("-");
18399 cx.executor().run_until_parked();
18400 cx.update_editor(|editor, _, _| {
18401 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18402 {
18403 assert_eq!(
18404 completion_menu_entries(menu),
18405 &["bg-blue", "bg-red", "bg-yellow"]
18406 );
18407 } else {
18408 panic!("expected completion menu to be open");
18409 }
18410 });
18411
18412 cx.simulate_keystroke("l");
18413 cx.executor().run_until_parked();
18414 cx.update_editor(|editor, _, _| {
18415 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18416 {
18417 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
18418 } else {
18419 panic!("expected completion menu to be open");
18420 }
18421 });
18422
18423 // When filtering completions, consider the character after the '-' to
18424 // be the start of a subword.
18425 cx.set_state(r#"<p class="yelˇ" />"#);
18426 cx.simulate_keystroke("l");
18427 cx.executor().run_until_parked();
18428 cx.update_editor(|editor, _, _| {
18429 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18430 {
18431 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
18432 } else {
18433 panic!("expected completion menu to be open");
18434 }
18435 });
18436}
18437
18438fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
18439 let entries = menu.entries.borrow();
18440 entries.iter().map(|mat| mat.string.clone()).collect()
18441}
18442
18443#[gpui::test]
18444async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
18445 init_test(cx, |settings| {
18446 settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
18447 });
18448
18449 let fs = FakeFs::new(cx.executor());
18450 fs.insert_file(path!("/file.ts"), Default::default()).await;
18451
18452 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
18453 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18454
18455 language_registry.add(Arc::new(Language::new(
18456 LanguageConfig {
18457 name: "TypeScript".into(),
18458 matcher: LanguageMatcher {
18459 path_suffixes: vec!["ts".to_string()],
18460 ..Default::default()
18461 },
18462 ..Default::default()
18463 },
18464 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
18465 )));
18466 update_test_language_settings(cx, |settings| {
18467 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
18468 });
18469
18470 let test_plugin = "test_plugin";
18471 let _ = language_registry.register_fake_lsp(
18472 "TypeScript",
18473 FakeLspAdapter {
18474 prettier_plugins: vec![test_plugin],
18475 ..Default::default()
18476 },
18477 );
18478
18479 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
18480 let buffer = project
18481 .update(cx, |project, cx| {
18482 project.open_local_buffer(path!("/file.ts"), cx)
18483 })
18484 .await
18485 .unwrap();
18486
18487 let buffer_text = "one\ntwo\nthree\n";
18488 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
18489 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
18490 editor.update_in(cx, |editor, window, cx| {
18491 editor.set_text(buffer_text, window, cx)
18492 });
18493
18494 editor
18495 .update_in(cx, |editor, window, cx| {
18496 editor.perform_format(
18497 project.clone(),
18498 FormatTrigger::Manual,
18499 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18500 window,
18501 cx,
18502 )
18503 })
18504 .unwrap()
18505 .await;
18506 assert_eq!(
18507 editor.update(cx, |editor, cx| editor.text(cx)),
18508 buffer_text.to_string() + prettier_format_suffix,
18509 "Test prettier formatting was not applied to the original buffer text",
18510 );
18511
18512 update_test_language_settings(cx, |settings| {
18513 settings.defaults.formatter = Some(FormatterList::default())
18514 });
18515 let format = editor.update_in(cx, |editor, window, cx| {
18516 editor.perform_format(
18517 project.clone(),
18518 FormatTrigger::Manual,
18519 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18520 window,
18521 cx,
18522 )
18523 });
18524 format.await.unwrap();
18525 assert_eq!(
18526 editor.update(cx, |editor, cx| editor.text(cx)),
18527 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
18528 "Autoformatting (via test prettier) was not applied to the original buffer text",
18529 );
18530}
18531
18532#[gpui::test]
18533async fn test_addition_reverts(cx: &mut TestAppContext) {
18534 init_test(cx, |_| {});
18535 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18536 let base_text = indoc! {r#"
18537 struct Row;
18538 struct Row1;
18539 struct Row2;
18540
18541 struct Row4;
18542 struct Row5;
18543 struct Row6;
18544
18545 struct Row8;
18546 struct Row9;
18547 struct Row10;"#};
18548
18549 // When addition hunks are not adjacent to carets, no hunk revert is performed
18550 assert_hunk_revert(
18551 indoc! {r#"struct Row;
18552 struct Row1;
18553 struct Row1.1;
18554 struct Row1.2;
18555 struct Row2;ˇ
18556
18557 struct Row4;
18558 struct Row5;
18559 struct Row6;
18560
18561 struct Row8;
18562 ˇstruct Row9;
18563 struct Row9.1;
18564 struct Row9.2;
18565 struct Row9.3;
18566 struct Row10;"#},
18567 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18568 indoc! {r#"struct Row;
18569 struct Row1;
18570 struct Row1.1;
18571 struct Row1.2;
18572 struct Row2;ˇ
18573
18574 struct Row4;
18575 struct Row5;
18576 struct Row6;
18577
18578 struct Row8;
18579 ˇstruct Row9;
18580 struct Row9.1;
18581 struct Row9.2;
18582 struct Row9.3;
18583 struct Row10;"#},
18584 base_text,
18585 &mut cx,
18586 );
18587 // Same for selections
18588 assert_hunk_revert(
18589 indoc! {r#"struct Row;
18590 struct Row1;
18591 struct Row2;
18592 struct Row2.1;
18593 struct Row2.2;
18594 «ˇ
18595 struct Row4;
18596 struct» Row5;
18597 «struct Row6;
18598 ˇ»
18599 struct Row9.1;
18600 struct Row9.2;
18601 struct Row9.3;
18602 struct Row8;
18603 struct Row9;
18604 struct Row10;"#},
18605 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18606 indoc! {r#"struct Row;
18607 struct Row1;
18608 struct Row2;
18609 struct Row2.1;
18610 struct Row2.2;
18611 «ˇ
18612 struct Row4;
18613 struct» Row5;
18614 «struct Row6;
18615 ˇ»
18616 struct Row9.1;
18617 struct Row9.2;
18618 struct Row9.3;
18619 struct Row8;
18620 struct Row9;
18621 struct Row10;"#},
18622 base_text,
18623 &mut cx,
18624 );
18625
18626 // When carets and selections intersect the addition hunks, those are reverted.
18627 // Adjacent carets got merged.
18628 assert_hunk_revert(
18629 indoc! {r#"struct Row;
18630 ˇ// something on the top
18631 struct Row1;
18632 struct Row2;
18633 struct Roˇw3.1;
18634 struct Row2.2;
18635 struct Row2.3;ˇ
18636
18637 struct Row4;
18638 struct ˇRow5.1;
18639 struct Row5.2;
18640 struct «Rowˇ»5.3;
18641 struct Row5;
18642 struct Row6;
18643 ˇ
18644 struct Row9.1;
18645 struct «Rowˇ»9.2;
18646 struct «ˇRow»9.3;
18647 struct Row8;
18648 struct Row9;
18649 «ˇ// something on bottom»
18650 struct Row10;"#},
18651 vec![
18652 DiffHunkStatusKind::Added,
18653 DiffHunkStatusKind::Added,
18654 DiffHunkStatusKind::Added,
18655 DiffHunkStatusKind::Added,
18656 DiffHunkStatusKind::Added,
18657 ],
18658 indoc! {r#"struct Row;
18659 ˇstruct Row1;
18660 struct Row2;
18661 ˇ
18662 struct Row4;
18663 ˇstruct Row5;
18664 struct Row6;
18665 ˇ
18666 ˇstruct Row8;
18667 struct Row9;
18668 ˇstruct Row10;"#},
18669 base_text,
18670 &mut cx,
18671 );
18672}
18673
18674#[gpui::test]
18675async fn test_modification_reverts(cx: &mut TestAppContext) {
18676 init_test(cx, |_| {});
18677 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18678 let base_text = indoc! {r#"
18679 struct Row;
18680 struct Row1;
18681 struct Row2;
18682
18683 struct Row4;
18684 struct Row5;
18685 struct Row6;
18686
18687 struct Row8;
18688 struct Row9;
18689 struct Row10;"#};
18690
18691 // Modification hunks behave the same as the addition ones.
18692 assert_hunk_revert(
18693 indoc! {r#"struct Row;
18694 struct Row1;
18695 struct Row33;
18696 ˇ
18697 struct Row4;
18698 struct Row5;
18699 struct Row6;
18700 ˇ
18701 struct Row99;
18702 struct Row9;
18703 struct Row10;"#},
18704 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18705 indoc! {r#"struct Row;
18706 struct Row1;
18707 struct Row33;
18708 ˇ
18709 struct Row4;
18710 struct Row5;
18711 struct Row6;
18712 ˇ
18713 struct Row99;
18714 struct Row9;
18715 struct Row10;"#},
18716 base_text,
18717 &mut cx,
18718 );
18719 assert_hunk_revert(
18720 indoc! {r#"struct Row;
18721 struct Row1;
18722 struct Row33;
18723 «ˇ
18724 struct Row4;
18725 struct» Row5;
18726 «struct Row6;
18727 ˇ»
18728 struct Row99;
18729 struct Row9;
18730 struct Row10;"#},
18731 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18732 indoc! {r#"struct Row;
18733 struct Row1;
18734 struct Row33;
18735 «ˇ
18736 struct Row4;
18737 struct» Row5;
18738 «struct Row6;
18739 ˇ»
18740 struct Row99;
18741 struct Row9;
18742 struct Row10;"#},
18743 base_text,
18744 &mut cx,
18745 );
18746
18747 assert_hunk_revert(
18748 indoc! {r#"ˇstruct Row1.1;
18749 struct Row1;
18750 «ˇstr»uct Row22;
18751
18752 struct ˇRow44;
18753 struct Row5;
18754 struct «Rˇ»ow66;ˇ
18755
18756 «struˇ»ct Row88;
18757 struct Row9;
18758 struct Row1011;ˇ"#},
18759 vec![
18760 DiffHunkStatusKind::Modified,
18761 DiffHunkStatusKind::Modified,
18762 DiffHunkStatusKind::Modified,
18763 DiffHunkStatusKind::Modified,
18764 DiffHunkStatusKind::Modified,
18765 DiffHunkStatusKind::Modified,
18766 ],
18767 indoc! {r#"struct Row;
18768 ˇstruct Row1;
18769 struct Row2;
18770 ˇ
18771 struct Row4;
18772 ˇstruct Row5;
18773 struct Row6;
18774 ˇ
18775 struct Row8;
18776 ˇstruct Row9;
18777 struct Row10;ˇ"#},
18778 base_text,
18779 &mut cx,
18780 );
18781}
18782
18783#[gpui::test]
18784async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
18785 init_test(cx, |_| {});
18786 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18787 let base_text = indoc! {r#"
18788 one
18789
18790 two
18791 three
18792 "#};
18793
18794 cx.set_head_text(base_text);
18795 cx.set_state("\nˇ\n");
18796 cx.executor().run_until_parked();
18797 cx.update_editor(|editor, _window, cx| {
18798 editor.expand_selected_diff_hunks(cx);
18799 });
18800 cx.executor().run_until_parked();
18801 cx.update_editor(|editor, window, cx| {
18802 editor.backspace(&Default::default(), window, cx);
18803 });
18804 cx.run_until_parked();
18805 cx.assert_state_with_diff(
18806 indoc! {r#"
18807
18808 - two
18809 - threeˇ
18810 +
18811 "#}
18812 .to_string(),
18813 );
18814}
18815
18816#[gpui::test]
18817async fn test_deletion_reverts(cx: &mut TestAppContext) {
18818 init_test(cx, |_| {});
18819 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18820 let base_text = indoc! {r#"struct Row;
18821struct Row1;
18822struct Row2;
18823
18824struct Row4;
18825struct Row5;
18826struct Row6;
18827
18828struct Row8;
18829struct Row9;
18830struct Row10;"#};
18831
18832 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
18833 assert_hunk_revert(
18834 indoc! {r#"struct Row;
18835 struct Row2;
18836
18837 ˇstruct Row4;
18838 struct Row5;
18839 struct Row6;
18840 ˇ
18841 struct Row8;
18842 struct Row10;"#},
18843 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18844 indoc! {r#"struct Row;
18845 struct Row2;
18846
18847 ˇstruct Row4;
18848 struct Row5;
18849 struct Row6;
18850 ˇ
18851 struct Row8;
18852 struct Row10;"#},
18853 base_text,
18854 &mut cx,
18855 );
18856 assert_hunk_revert(
18857 indoc! {r#"struct Row;
18858 struct Row2;
18859
18860 «ˇstruct Row4;
18861 struct» Row5;
18862 «struct Row6;
18863 ˇ»
18864 struct Row8;
18865 struct Row10;"#},
18866 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18867 indoc! {r#"struct Row;
18868 struct Row2;
18869
18870 «ˇstruct Row4;
18871 struct» Row5;
18872 «struct Row6;
18873 ˇ»
18874 struct Row8;
18875 struct Row10;"#},
18876 base_text,
18877 &mut cx,
18878 );
18879
18880 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
18881 assert_hunk_revert(
18882 indoc! {r#"struct Row;
18883 ˇstruct Row2;
18884
18885 struct Row4;
18886 struct Row5;
18887 struct Row6;
18888
18889 struct Row8;ˇ
18890 struct Row10;"#},
18891 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18892 indoc! {r#"struct Row;
18893 struct Row1;
18894 ˇstruct Row2;
18895
18896 struct Row4;
18897 struct Row5;
18898 struct Row6;
18899
18900 struct Row8;ˇ
18901 struct Row9;
18902 struct Row10;"#},
18903 base_text,
18904 &mut cx,
18905 );
18906 assert_hunk_revert(
18907 indoc! {r#"struct Row;
18908 struct Row2«ˇ;
18909 struct Row4;
18910 struct» Row5;
18911 «struct Row6;
18912
18913 struct Row8;ˇ»
18914 struct Row10;"#},
18915 vec![
18916 DiffHunkStatusKind::Deleted,
18917 DiffHunkStatusKind::Deleted,
18918 DiffHunkStatusKind::Deleted,
18919 ],
18920 indoc! {r#"struct Row;
18921 struct Row1;
18922 struct Row2«ˇ;
18923
18924 struct Row4;
18925 struct» Row5;
18926 «struct Row6;
18927
18928 struct Row8;ˇ»
18929 struct Row9;
18930 struct Row10;"#},
18931 base_text,
18932 &mut cx,
18933 );
18934}
18935
18936#[gpui::test]
18937async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
18938 init_test(cx, |_| {});
18939
18940 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
18941 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
18942 let base_text_3 =
18943 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
18944
18945 let text_1 = edit_first_char_of_every_line(base_text_1);
18946 let text_2 = edit_first_char_of_every_line(base_text_2);
18947 let text_3 = edit_first_char_of_every_line(base_text_3);
18948
18949 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
18950 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
18951 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
18952
18953 let multibuffer = cx.new(|cx| {
18954 let mut multibuffer = MultiBuffer::new(ReadWrite);
18955 multibuffer.push_excerpts(
18956 buffer_1.clone(),
18957 [
18958 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18959 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18960 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18961 ],
18962 cx,
18963 );
18964 multibuffer.push_excerpts(
18965 buffer_2.clone(),
18966 [
18967 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18968 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18969 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18970 ],
18971 cx,
18972 );
18973 multibuffer.push_excerpts(
18974 buffer_3.clone(),
18975 [
18976 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18977 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18978 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18979 ],
18980 cx,
18981 );
18982 multibuffer
18983 });
18984
18985 let fs = FakeFs::new(cx.executor());
18986 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
18987 let (editor, cx) = cx
18988 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
18989 editor.update_in(cx, |editor, _window, cx| {
18990 for (buffer, diff_base) in [
18991 (buffer_1.clone(), base_text_1),
18992 (buffer_2.clone(), base_text_2),
18993 (buffer_3.clone(), base_text_3),
18994 ] {
18995 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
18996 editor
18997 .buffer
18998 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
18999 }
19000 });
19001 cx.executor().run_until_parked();
19002
19003 editor.update_in(cx, |editor, window, cx| {
19004 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}");
19005 editor.select_all(&SelectAll, window, cx);
19006 editor.git_restore(&Default::default(), window, cx);
19007 });
19008 cx.executor().run_until_parked();
19009
19010 // When all ranges are selected, all buffer hunks are reverted.
19011 editor.update(cx, |editor, cx| {
19012 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");
19013 });
19014 buffer_1.update(cx, |buffer, _| {
19015 assert_eq!(buffer.text(), base_text_1);
19016 });
19017 buffer_2.update(cx, |buffer, _| {
19018 assert_eq!(buffer.text(), base_text_2);
19019 });
19020 buffer_3.update(cx, |buffer, _| {
19021 assert_eq!(buffer.text(), base_text_3);
19022 });
19023
19024 editor.update_in(cx, |editor, window, cx| {
19025 editor.undo(&Default::default(), window, cx);
19026 });
19027
19028 editor.update_in(cx, |editor, window, cx| {
19029 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19030 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
19031 });
19032 editor.git_restore(&Default::default(), window, cx);
19033 });
19034
19035 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
19036 // but not affect buffer_2 and its related excerpts.
19037 editor.update(cx, |editor, cx| {
19038 assert_eq!(
19039 editor.text(cx),
19040 "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}"
19041 );
19042 });
19043 buffer_1.update(cx, |buffer, _| {
19044 assert_eq!(buffer.text(), base_text_1);
19045 });
19046 buffer_2.update(cx, |buffer, _| {
19047 assert_eq!(
19048 buffer.text(),
19049 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
19050 );
19051 });
19052 buffer_3.update(cx, |buffer, _| {
19053 assert_eq!(
19054 buffer.text(),
19055 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
19056 );
19057 });
19058
19059 fn edit_first_char_of_every_line(text: &str) -> String {
19060 text.split('\n')
19061 .map(|line| format!("X{}", &line[1..]))
19062 .collect::<Vec<_>>()
19063 .join("\n")
19064 }
19065}
19066
19067#[gpui::test]
19068async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
19069 init_test(cx, |_| {});
19070
19071 let cols = 4;
19072 let rows = 10;
19073 let sample_text_1 = sample_text(rows, cols, 'a');
19074 assert_eq!(
19075 sample_text_1,
19076 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
19077 );
19078 let sample_text_2 = sample_text(rows, cols, 'l');
19079 assert_eq!(
19080 sample_text_2,
19081 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
19082 );
19083 let sample_text_3 = sample_text(rows, cols, 'v');
19084 assert_eq!(
19085 sample_text_3,
19086 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
19087 );
19088
19089 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
19090 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
19091 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
19092
19093 let multi_buffer = cx.new(|cx| {
19094 let mut multibuffer = MultiBuffer::new(ReadWrite);
19095 multibuffer.push_excerpts(
19096 buffer_1.clone(),
19097 [
19098 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19099 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19100 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19101 ],
19102 cx,
19103 );
19104 multibuffer.push_excerpts(
19105 buffer_2.clone(),
19106 [
19107 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19108 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19109 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19110 ],
19111 cx,
19112 );
19113 multibuffer.push_excerpts(
19114 buffer_3.clone(),
19115 [
19116 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19117 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19118 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19119 ],
19120 cx,
19121 );
19122 multibuffer
19123 });
19124
19125 let fs = FakeFs::new(cx.executor());
19126 fs.insert_tree(
19127 "/a",
19128 json!({
19129 "main.rs": sample_text_1,
19130 "other.rs": sample_text_2,
19131 "lib.rs": sample_text_3,
19132 }),
19133 )
19134 .await;
19135 let project = Project::test(fs, ["/a".as_ref()], cx).await;
19136 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19137 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19138 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19139 Editor::new(
19140 EditorMode::full(),
19141 multi_buffer,
19142 Some(project.clone()),
19143 window,
19144 cx,
19145 )
19146 });
19147 let multibuffer_item_id = workspace
19148 .update(cx, |workspace, window, cx| {
19149 assert!(
19150 workspace.active_item(cx).is_none(),
19151 "active item should be None before the first item is added"
19152 );
19153 workspace.add_item_to_active_pane(
19154 Box::new(multi_buffer_editor.clone()),
19155 None,
19156 true,
19157 window,
19158 cx,
19159 );
19160 let active_item = workspace
19161 .active_item(cx)
19162 .expect("should have an active item after adding the multi buffer");
19163 assert_eq!(
19164 active_item.buffer_kind(cx),
19165 ItemBufferKind::Multibuffer,
19166 "A multi buffer was expected to active after adding"
19167 );
19168 active_item.item_id()
19169 })
19170 .unwrap();
19171 cx.executor().run_until_parked();
19172
19173 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19174 editor.change_selections(
19175 SelectionEffects::scroll(Autoscroll::Next),
19176 window,
19177 cx,
19178 |s| s.select_ranges(Some(1..2)),
19179 );
19180 editor.open_excerpts(&OpenExcerpts, window, cx);
19181 });
19182 cx.executor().run_until_parked();
19183 let first_item_id = workspace
19184 .update(cx, |workspace, window, cx| {
19185 let active_item = workspace
19186 .active_item(cx)
19187 .expect("should have an active item after navigating into the 1st buffer");
19188 let first_item_id = active_item.item_id();
19189 assert_ne!(
19190 first_item_id, multibuffer_item_id,
19191 "Should navigate into the 1st buffer and activate it"
19192 );
19193 assert_eq!(
19194 active_item.buffer_kind(cx),
19195 ItemBufferKind::Singleton,
19196 "New active item should be a singleton buffer"
19197 );
19198 assert_eq!(
19199 active_item
19200 .act_as::<Editor>(cx)
19201 .expect("should have navigated into an editor for the 1st buffer")
19202 .read(cx)
19203 .text(cx),
19204 sample_text_1
19205 );
19206
19207 workspace
19208 .go_back(workspace.active_pane().downgrade(), window, cx)
19209 .detach_and_log_err(cx);
19210
19211 first_item_id
19212 })
19213 .unwrap();
19214 cx.executor().run_until_parked();
19215 workspace
19216 .update(cx, |workspace, _, cx| {
19217 let active_item = workspace
19218 .active_item(cx)
19219 .expect("should have an active item after navigating back");
19220 assert_eq!(
19221 active_item.item_id(),
19222 multibuffer_item_id,
19223 "Should navigate back to the multi buffer"
19224 );
19225 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19226 })
19227 .unwrap();
19228
19229 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19230 editor.change_selections(
19231 SelectionEffects::scroll(Autoscroll::Next),
19232 window,
19233 cx,
19234 |s| s.select_ranges(Some(39..40)),
19235 );
19236 editor.open_excerpts(&OpenExcerpts, window, cx);
19237 });
19238 cx.executor().run_until_parked();
19239 let second_item_id = workspace
19240 .update(cx, |workspace, window, cx| {
19241 let active_item = workspace
19242 .active_item(cx)
19243 .expect("should have an active item after navigating into the 2nd buffer");
19244 let second_item_id = active_item.item_id();
19245 assert_ne!(
19246 second_item_id, multibuffer_item_id,
19247 "Should navigate away from the multibuffer"
19248 );
19249 assert_ne!(
19250 second_item_id, first_item_id,
19251 "Should navigate into the 2nd buffer and activate it"
19252 );
19253 assert_eq!(
19254 active_item.buffer_kind(cx),
19255 ItemBufferKind::Singleton,
19256 "New active item should be a singleton buffer"
19257 );
19258 assert_eq!(
19259 active_item
19260 .act_as::<Editor>(cx)
19261 .expect("should have navigated into an editor")
19262 .read(cx)
19263 .text(cx),
19264 sample_text_2
19265 );
19266
19267 workspace
19268 .go_back(workspace.active_pane().downgrade(), window, cx)
19269 .detach_and_log_err(cx);
19270
19271 second_item_id
19272 })
19273 .unwrap();
19274 cx.executor().run_until_parked();
19275 workspace
19276 .update(cx, |workspace, _, cx| {
19277 let active_item = workspace
19278 .active_item(cx)
19279 .expect("should have an active item after navigating back from the 2nd buffer");
19280 assert_eq!(
19281 active_item.item_id(),
19282 multibuffer_item_id,
19283 "Should navigate back from the 2nd buffer to the multi buffer"
19284 );
19285 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19286 })
19287 .unwrap();
19288
19289 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19290 editor.change_selections(
19291 SelectionEffects::scroll(Autoscroll::Next),
19292 window,
19293 cx,
19294 |s| s.select_ranges(Some(70..70)),
19295 );
19296 editor.open_excerpts(&OpenExcerpts, window, cx);
19297 });
19298 cx.executor().run_until_parked();
19299 workspace
19300 .update(cx, |workspace, window, cx| {
19301 let active_item = workspace
19302 .active_item(cx)
19303 .expect("should have an active item after navigating into the 3rd buffer");
19304 let third_item_id = active_item.item_id();
19305 assert_ne!(
19306 third_item_id, multibuffer_item_id,
19307 "Should navigate into the 3rd buffer and activate it"
19308 );
19309 assert_ne!(third_item_id, first_item_id);
19310 assert_ne!(third_item_id, second_item_id);
19311 assert_eq!(
19312 active_item.buffer_kind(cx),
19313 ItemBufferKind::Singleton,
19314 "New active item should be a singleton buffer"
19315 );
19316 assert_eq!(
19317 active_item
19318 .act_as::<Editor>(cx)
19319 .expect("should have navigated into an editor")
19320 .read(cx)
19321 .text(cx),
19322 sample_text_3
19323 );
19324
19325 workspace
19326 .go_back(workspace.active_pane().downgrade(), window, cx)
19327 .detach_and_log_err(cx);
19328 })
19329 .unwrap();
19330 cx.executor().run_until_parked();
19331 workspace
19332 .update(cx, |workspace, _, cx| {
19333 let active_item = workspace
19334 .active_item(cx)
19335 .expect("should have an active item after navigating back from the 3rd buffer");
19336 assert_eq!(
19337 active_item.item_id(),
19338 multibuffer_item_id,
19339 "Should navigate back from the 3rd buffer to the multi buffer"
19340 );
19341 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19342 })
19343 .unwrap();
19344}
19345
19346#[gpui::test]
19347async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19348 init_test(cx, |_| {});
19349
19350 let mut cx = EditorTestContext::new(cx).await;
19351
19352 let diff_base = r#"
19353 use some::mod;
19354
19355 const A: u32 = 42;
19356
19357 fn main() {
19358 println!("hello");
19359
19360 println!("world");
19361 }
19362 "#
19363 .unindent();
19364
19365 cx.set_state(
19366 &r#"
19367 use some::modified;
19368
19369 ˇ
19370 fn main() {
19371 println!("hello there");
19372
19373 println!("around the");
19374 println!("world");
19375 }
19376 "#
19377 .unindent(),
19378 );
19379
19380 cx.set_head_text(&diff_base);
19381 executor.run_until_parked();
19382
19383 cx.update_editor(|editor, window, cx| {
19384 editor.go_to_next_hunk(&GoToHunk, window, cx);
19385 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19386 });
19387 executor.run_until_parked();
19388 cx.assert_state_with_diff(
19389 r#"
19390 use some::modified;
19391
19392
19393 fn main() {
19394 - println!("hello");
19395 + ˇ println!("hello there");
19396
19397 println!("around the");
19398 println!("world");
19399 }
19400 "#
19401 .unindent(),
19402 );
19403
19404 cx.update_editor(|editor, window, cx| {
19405 for _ in 0..2 {
19406 editor.go_to_next_hunk(&GoToHunk, window, cx);
19407 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19408 }
19409 });
19410 executor.run_until_parked();
19411 cx.assert_state_with_diff(
19412 r#"
19413 - use some::mod;
19414 + ˇuse some::modified;
19415
19416
19417 fn main() {
19418 - println!("hello");
19419 + println!("hello there");
19420
19421 + println!("around the");
19422 println!("world");
19423 }
19424 "#
19425 .unindent(),
19426 );
19427
19428 cx.update_editor(|editor, window, cx| {
19429 editor.go_to_next_hunk(&GoToHunk, window, cx);
19430 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19431 });
19432 executor.run_until_parked();
19433 cx.assert_state_with_diff(
19434 r#"
19435 - use some::mod;
19436 + use some::modified;
19437
19438 - const A: u32 = 42;
19439 ˇ
19440 fn main() {
19441 - println!("hello");
19442 + println!("hello there");
19443
19444 + println!("around the");
19445 println!("world");
19446 }
19447 "#
19448 .unindent(),
19449 );
19450
19451 cx.update_editor(|editor, window, cx| {
19452 editor.cancel(&Cancel, window, cx);
19453 });
19454
19455 cx.assert_state_with_diff(
19456 r#"
19457 use some::modified;
19458
19459 ˇ
19460 fn main() {
19461 println!("hello there");
19462
19463 println!("around the");
19464 println!("world");
19465 }
19466 "#
19467 .unindent(),
19468 );
19469}
19470
19471#[gpui::test]
19472async fn test_diff_base_change_with_expanded_diff_hunks(
19473 executor: BackgroundExecutor,
19474 cx: &mut TestAppContext,
19475) {
19476 init_test(cx, |_| {});
19477
19478 let mut cx = EditorTestContext::new(cx).await;
19479
19480 let diff_base = r#"
19481 use some::mod1;
19482 use some::mod2;
19483
19484 const A: u32 = 42;
19485 const B: u32 = 42;
19486 const C: u32 = 42;
19487
19488 fn main() {
19489 println!("hello");
19490
19491 println!("world");
19492 }
19493 "#
19494 .unindent();
19495
19496 cx.set_state(
19497 &r#"
19498 use some::mod2;
19499
19500 const A: u32 = 42;
19501 const C: u32 = 42;
19502
19503 fn main(ˇ) {
19504 //println!("hello");
19505
19506 println!("world");
19507 //
19508 //
19509 }
19510 "#
19511 .unindent(),
19512 );
19513
19514 cx.set_head_text(&diff_base);
19515 executor.run_until_parked();
19516
19517 cx.update_editor(|editor, window, cx| {
19518 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19519 });
19520 executor.run_until_parked();
19521 cx.assert_state_with_diff(
19522 r#"
19523 - use some::mod1;
19524 use some::mod2;
19525
19526 const A: u32 = 42;
19527 - const B: u32 = 42;
19528 const C: u32 = 42;
19529
19530 fn main(ˇ) {
19531 - println!("hello");
19532 + //println!("hello");
19533
19534 println!("world");
19535 + //
19536 + //
19537 }
19538 "#
19539 .unindent(),
19540 );
19541
19542 cx.set_head_text("new diff base!");
19543 executor.run_until_parked();
19544 cx.assert_state_with_diff(
19545 r#"
19546 - new diff base!
19547 + use some::mod2;
19548 +
19549 + const A: u32 = 42;
19550 + const C: u32 = 42;
19551 +
19552 + fn main(ˇ) {
19553 + //println!("hello");
19554 +
19555 + println!("world");
19556 + //
19557 + //
19558 + }
19559 "#
19560 .unindent(),
19561 );
19562}
19563
19564#[gpui::test]
19565async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
19566 init_test(cx, |_| {});
19567
19568 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19569 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19570 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19571 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19572 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
19573 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
19574
19575 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
19576 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
19577 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
19578
19579 let multi_buffer = cx.new(|cx| {
19580 let mut multibuffer = MultiBuffer::new(ReadWrite);
19581 multibuffer.push_excerpts(
19582 buffer_1.clone(),
19583 [
19584 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19585 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19586 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19587 ],
19588 cx,
19589 );
19590 multibuffer.push_excerpts(
19591 buffer_2.clone(),
19592 [
19593 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19594 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19595 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19596 ],
19597 cx,
19598 );
19599 multibuffer.push_excerpts(
19600 buffer_3.clone(),
19601 [
19602 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19603 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19604 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19605 ],
19606 cx,
19607 );
19608 multibuffer
19609 });
19610
19611 let editor =
19612 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19613 editor
19614 .update(cx, |editor, _window, cx| {
19615 for (buffer, diff_base) in [
19616 (buffer_1.clone(), file_1_old),
19617 (buffer_2.clone(), file_2_old),
19618 (buffer_3.clone(), file_3_old),
19619 ] {
19620 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
19621 editor
19622 .buffer
19623 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
19624 }
19625 })
19626 .unwrap();
19627
19628 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19629 cx.run_until_parked();
19630
19631 cx.assert_editor_state(
19632 &"
19633 ˇaaa
19634 ccc
19635 ddd
19636
19637 ggg
19638 hhh
19639
19640
19641 lll
19642 mmm
19643 NNN
19644
19645 qqq
19646 rrr
19647
19648 uuu
19649 111
19650 222
19651 333
19652
19653 666
19654 777
19655
19656 000
19657 !!!"
19658 .unindent(),
19659 );
19660
19661 cx.update_editor(|editor, window, cx| {
19662 editor.select_all(&SelectAll, window, cx);
19663 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19664 });
19665 cx.executor().run_until_parked();
19666
19667 cx.assert_state_with_diff(
19668 "
19669 «aaa
19670 - bbb
19671 ccc
19672 ddd
19673
19674 ggg
19675 hhh
19676
19677
19678 lll
19679 mmm
19680 - nnn
19681 + NNN
19682
19683 qqq
19684 rrr
19685
19686 uuu
19687 111
19688 222
19689 333
19690
19691 + 666
19692 777
19693
19694 000
19695 !!!ˇ»"
19696 .unindent(),
19697 );
19698}
19699
19700#[gpui::test]
19701async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
19702 init_test(cx, |_| {});
19703
19704 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
19705 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
19706
19707 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
19708 let multi_buffer = cx.new(|cx| {
19709 let mut multibuffer = MultiBuffer::new(ReadWrite);
19710 multibuffer.push_excerpts(
19711 buffer.clone(),
19712 [
19713 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
19714 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
19715 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
19716 ],
19717 cx,
19718 );
19719 multibuffer
19720 });
19721
19722 let editor =
19723 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19724 editor
19725 .update(cx, |editor, _window, cx| {
19726 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
19727 editor
19728 .buffer
19729 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
19730 })
19731 .unwrap();
19732
19733 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19734 cx.run_until_parked();
19735
19736 cx.update_editor(|editor, window, cx| {
19737 editor.expand_all_diff_hunks(&Default::default(), window, cx)
19738 });
19739 cx.executor().run_until_parked();
19740
19741 // When the start of a hunk coincides with the start of its excerpt,
19742 // the hunk is expanded. When the start of a hunk is earlier than
19743 // the start of its excerpt, the hunk is not expanded.
19744 cx.assert_state_with_diff(
19745 "
19746 ˇaaa
19747 - bbb
19748 + BBB
19749
19750 - ddd
19751 - eee
19752 + DDD
19753 + EEE
19754 fff
19755
19756 iii
19757 "
19758 .unindent(),
19759 );
19760}
19761
19762#[gpui::test]
19763async fn test_edits_around_expanded_insertion_hunks(
19764 executor: BackgroundExecutor,
19765 cx: &mut TestAppContext,
19766) {
19767 init_test(cx, |_| {});
19768
19769 let mut cx = EditorTestContext::new(cx).await;
19770
19771 let diff_base = r#"
19772 use some::mod1;
19773 use some::mod2;
19774
19775 const A: u32 = 42;
19776
19777 fn main() {
19778 println!("hello");
19779
19780 println!("world");
19781 }
19782 "#
19783 .unindent();
19784 executor.run_until_parked();
19785 cx.set_state(
19786 &r#"
19787 use some::mod1;
19788 use some::mod2;
19789
19790 const A: u32 = 42;
19791 const B: u32 = 42;
19792 const C: u32 = 42;
19793 ˇ
19794
19795 fn main() {
19796 println!("hello");
19797
19798 println!("world");
19799 }
19800 "#
19801 .unindent(),
19802 );
19803
19804 cx.set_head_text(&diff_base);
19805 executor.run_until_parked();
19806
19807 cx.update_editor(|editor, window, cx| {
19808 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19809 });
19810 executor.run_until_parked();
19811
19812 cx.assert_state_with_diff(
19813 r#"
19814 use some::mod1;
19815 use some::mod2;
19816
19817 const A: u32 = 42;
19818 + const B: u32 = 42;
19819 + const C: u32 = 42;
19820 + ˇ
19821
19822 fn main() {
19823 println!("hello");
19824
19825 println!("world");
19826 }
19827 "#
19828 .unindent(),
19829 );
19830
19831 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
19832 executor.run_until_parked();
19833
19834 cx.assert_state_with_diff(
19835 r#"
19836 use some::mod1;
19837 use some::mod2;
19838
19839 const A: u32 = 42;
19840 + const B: u32 = 42;
19841 + const C: u32 = 42;
19842 + const D: u32 = 42;
19843 + ˇ
19844
19845 fn main() {
19846 println!("hello");
19847
19848 println!("world");
19849 }
19850 "#
19851 .unindent(),
19852 );
19853
19854 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
19855 executor.run_until_parked();
19856
19857 cx.assert_state_with_diff(
19858 r#"
19859 use some::mod1;
19860 use some::mod2;
19861
19862 const A: u32 = 42;
19863 + const B: u32 = 42;
19864 + const C: u32 = 42;
19865 + const D: u32 = 42;
19866 + const E: u32 = 42;
19867 + ˇ
19868
19869 fn main() {
19870 println!("hello");
19871
19872 println!("world");
19873 }
19874 "#
19875 .unindent(),
19876 );
19877
19878 cx.update_editor(|editor, window, cx| {
19879 editor.delete_line(&DeleteLine, window, cx);
19880 });
19881 executor.run_until_parked();
19882
19883 cx.assert_state_with_diff(
19884 r#"
19885 use some::mod1;
19886 use some::mod2;
19887
19888 const A: u32 = 42;
19889 + const B: u32 = 42;
19890 + const C: u32 = 42;
19891 + const D: u32 = 42;
19892 + const E: u32 = 42;
19893 ˇ
19894 fn main() {
19895 println!("hello");
19896
19897 println!("world");
19898 }
19899 "#
19900 .unindent(),
19901 );
19902
19903 cx.update_editor(|editor, window, cx| {
19904 editor.move_up(&MoveUp, window, cx);
19905 editor.delete_line(&DeleteLine, window, cx);
19906 editor.move_up(&MoveUp, window, cx);
19907 editor.delete_line(&DeleteLine, window, cx);
19908 editor.move_up(&MoveUp, window, cx);
19909 editor.delete_line(&DeleteLine, window, cx);
19910 });
19911 executor.run_until_parked();
19912 cx.assert_state_with_diff(
19913 r#"
19914 use some::mod1;
19915 use some::mod2;
19916
19917 const A: u32 = 42;
19918 + const B: u32 = 42;
19919 ˇ
19920 fn main() {
19921 println!("hello");
19922
19923 println!("world");
19924 }
19925 "#
19926 .unindent(),
19927 );
19928
19929 cx.update_editor(|editor, window, cx| {
19930 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
19931 editor.delete_line(&DeleteLine, window, cx);
19932 });
19933 executor.run_until_parked();
19934 cx.assert_state_with_diff(
19935 r#"
19936 ˇ
19937 fn main() {
19938 println!("hello");
19939
19940 println!("world");
19941 }
19942 "#
19943 .unindent(),
19944 );
19945}
19946
19947#[gpui::test]
19948async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
19949 init_test(cx, |_| {});
19950
19951 let mut cx = EditorTestContext::new(cx).await;
19952 cx.set_head_text(indoc! { "
19953 one
19954 two
19955 three
19956 four
19957 five
19958 "
19959 });
19960 cx.set_state(indoc! { "
19961 one
19962 ˇthree
19963 five
19964 "});
19965 cx.run_until_parked();
19966 cx.update_editor(|editor, window, cx| {
19967 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19968 });
19969 cx.assert_state_with_diff(
19970 indoc! { "
19971 one
19972 - two
19973 ˇthree
19974 - four
19975 five
19976 "}
19977 .to_string(),
19978 );
19979 cx.update_editor(|editor, window, cx| {
19980 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19981 });
19982
19983 cx.assert_state_with_diff(
19984 indoc! { "
19985 one
19986 ˇthree
19987 five
19988 "}
19989 .to_string(),
19990 );
19991
19992 cx.set_state(indoc! { "
19993 one
19994 ˇTWO
19995 three
19996 four
19997 five
19998 "});
19999 cx.run_until_parked();
20000 cx.update_editor(|editor, window, cx| {
20001 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20002 });
20003
20004 cx.assert_state_with_diff(
20005 indoc! { "
20006 one
20007 - two
20008 + ˇTWO
20009 three
20010 four
20011 five
20012 "}
20013 .to_string(),
20014 );
20015 cx.update_editor(|editor, window, cx| {
20016 editor.move_up(&Default::default(), window, cx);
20017 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20018 });
20019 cx.assert_state_with_diff(
20020 indoc! { "
20021 one
20022 ˇTWO
20023 three
20024 four
20025 five
20026 "}
20027 .to_string(),
20028 );
20029}
20030
20031#[gpui::test]
20032async fn test_edits_around_expanded_deletion_hunks(
20033 executor: BackgroundExecutor,
20034 cx: &mut TestAppContext,
20035) {
20036 init_test(cx, |_| {});
20037
20038 let mut cx = EditorTestContext::new(cx).await;
20039
20040 let diff_base = r#"
20041 use some::mod1;
20042 use some::mod2;
20043
20044 const A: u32 = 42;
20045 const B: u32 = 42;
20046 const C: u32 = 42;
20047
20048
20049 fn main() {
20050 println!("hello");
20051
20052 println!("world");
20053 }
20054 "#
20055 .unindent();
20056 executor.run_until_parked();
20057 cx.set_state(
20058 &r#"
20059 use some::mod1;
20060 use some::mod2;
20061
20062 ˇconst B: u32 = 42;
20063 const C: u32 = 42;
20064
20065
20066 fn main() {
20067 println!("hello");
20068
20069 println!("world");
20070 }
20071 "#
20072 .unindent(),
20073 );
20074
20075 cx.set_head_text(&diff_base);
20076 executor.run_until_parked();
20077
20078 cx.update_editor(|editor, window, cx| {
20079 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20080 });
20081 executor.run_until_parked();
20082
20083 cx.assert_state_with_diff(
20084 r#"
20085 use some::mod1;
20086 use some::mod2;
20087
20088 - const A: u32 = 42;
20089 ˇconst B: u32 = 42;
20090 const C: u32 = 42;
20091
20092
20093 fn main() {
20094 println!("hello");
20095
20096 println!("world");
20097 }
20098 "#
20099 .unindent(),
20100 );
20101
20102 cx.update_editor(|editor, window, cx| {
20103 editor.delete_line(&DeleteLine, window, cx);
20104 });
20105 executor.run_until_parked();
20106 cx.assert_state_with_diff(
20107 r#"
20108 use some::mod1;
20109 use some::mod2;
20110
20111 - const A: u32 = 42;
20112 - const B: u32 = 42;
20113 ˇconst C: u32 = 42;
20114
20115
20116 fn main() {
20117 println!("hello");
20118
20119 println!("world");
20120 }
20121 "#
20122 .unindent(),
20123 );
20124
20125 cx.update_editor(|editor, window, cx| {
20126 editor.delete_line(&DeleteLine, window, cx);
20127 });
20128 executor.run_until_parked();
20129 cx.assert_state_with_diff(
20130 r#"
20131 use some::mod1;
20132 use some::mod2;
20133
20134 - const A: u32 = 42;
20135 - const B: u32 = 42;
20136 - const C: u32 = 42;
20137 ˇ
20138
20139 fn main() {
20140 println!("hello");
20141
20142 println!("world");
20143 }
20144 "#
20145 .unindent(),
20146 );
20147
20148 cx.update_editor(|editor, window, cx| {
20149 editor.handle_input("replacement", window, cx);
20150 });
20151 executor.run_until_parked();
20152 cx.assert_state_with_diff(
20153 r#"
20154 use some::mod1;
20155 use some::mod2;
20156
20157 - const A: u32 = 42;
20158 - const B: u32 = 42;
20159 - const C: u32 = 42;
20160 -
20161 + replacementˇ
20162
20163 fn main() {
20164 println!("hello");
20165
20166 println!("world");
20167 }
20168 "#
20169 .unindent(),
20170 );
20171}
20172
20173#[gpui::test]
20174async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20175 init_test(cx, |_| {});
20176
20177 let mut cx = EditorTestContext::new(cx).await;
20178
20179 let base_text = r#"
20180 one
20181 two
20182 three
20183 four
20184 five
20185 "#
20186 .unindent();
20187 executor.run_until_parked();
20188 cx.set_state(
20189 &r#"
20190 one
20191 two
20192 fˇour
20193 five
20194 "#
20195 .unindent(),
20196 );
20197
20198 cx.set_head_text(&base_text);
20199 executor.run_until_parked();
20200
20201 cx.update_editor(|editor, window, cx| {
20202 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20203 });
20204 executor.run_until_parked();
20205
20206 cx.assert_state_with_diff(
20207 r#"
20208 one
20209 two
20210 - three
20211 fˇour
20212 five
20213 "#
20214 .unindent(),
20215 );
20216
20217 cx.update_editor(|editor, window, cx| {
20218 editor.backspace(&Backspace, window, cx);
20219 editor.backspace(&Backspace, window, cx);
20220 });
20221 executor.run_until_parked();
20222 cx.assert_state_with_diff(
20223 r#"
20224 one
20225 two
20226 - threeˇ
20227 - four
20228 + our
20229 five
20230 "#
20231 .unindent(),
20232 );
20233}
20234
20235#[gpui::test]
20236async fn test_edit_after_expanded_modification_hunk(
20237 executor: BackgroundExecutor,
20238 cx: &mut TestAppContext,
20239) {
20240 init_test(cx, |_| {});
20241
20242 let mut cx = EditorTestContext::new(cx).await;
20243
20244 let diff_base = r#"
20245 use some::mod1;
20246 use some::mod2;
20247
20248 const A: u32 = 42;
20249 const B: u32 = 42;
20250 const C: u32 = 42;
20251 const D: u32 = 42;
20252
20253
20254 fn main() {
20255 println!("hello");
20256
20257 println!("world");
20258 }"#
20259 .unindent();
20260
20261 cx.set_state(
20262 &r#"
20263 use some::mod1;
20264 use some::mod2;
20265
20266 const A: u32 = 42;
20267 const B: u32 = 42;
20268 const C: u32 = 43ˇ
20269 const D: u32 = 42;
20270
20271
20272 fn main() {
20273 println!("hello");
20274
20275 println!("world");
20276 }"#
20277 .unindent(),
20278 );
20279
20280 cx.set_head_text(&diff_base);
20281 executor.run_until_parked();
20282 cx.update_editor(|editor, window, cx| {
20283 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20284 });
20285 executor.run_until_parked();
20286
20287 cx.assert_state_with_diff(
20288 r#"
20289 use some::mod1;
20290 use some::mod2;
20291
20292 const A: u32 = 42;
20293 const B: u32 = 42;
20294 - const C: u32 = 42;
20295 + const C: u32 = 43ˇ
20296 const D: u32 = 42;
20297
20298
20299 fn main() {
20300 println!("hello");
20301
20302 println!("world");
20303 }"#
20304 .unindent(),
20305 );
20306
20307 cx.update_editor(|editor, window, cx| {
20308 editor.handle_input("\nnew_line\n", window, cx);
20309 });
20310 executor.run_until_parked();
20311
20312 cx.assert_state_with_diff(
20313 r#"
20314 use some::mod1;
20315 use some::mod2;
20316
20317 const A: u32 = 42;
20318 const B: u32 = 42;
20319 - const C: u32 = 42;
20320 + const C: u32 = 43
20321 + new_line
20322 + ˇ
20323 const D: u32 = 42;
20324
20325
20326 fn main() {
20327 println!("hello");
20328
20329 println!("world");
20330 }"#
20331 .unindent(),
20332 );
20333}
20334
20335#[gpui::test]
20336async fn test_stage_and_unstage_added_file_hunk(
20337 executor: BackgroundExecutor,
20338 cx: &mut TestAppContext,
20339) {
20340 init_test(cx, |_| {});
20341
20342 let mut cx = EditorTestContext::new(cx).await;
20343 cx.update_editor(|editor, _, cx| {
20344 editor.set_expand_all_diff_hunks(cx);
20345 });
20346
20347 let working_copy = r#"
20348 ˇfn main() {
20349 println!("hello, world!");
20350 }
20351 "#
20352 .unindent();
20353
20354 cx.set_state(&working_copy);
20355 executor.run_until_parked();
20356
20357 cx.assert_state_with_diff(
20358 r#"
20359 + ˇfn main() {
20360 + println!("hello, world!");
20361 + }
20362 "#
20363 .unindent(),
20364 );
20365 cx.assert_index_text(None);
20366
20367 cx.update_editor(|editor, window, cx| {
20368 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20369 });
20370 executor.run_until_parked();
20371 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
20372 cx.assert_state_with_diff(
20373 r#"
20374 + ˇfn main() {
20375 + println!("hello, world!");
20376 + }
20377 "#
20378 .unindent(),
20379 );
20380
20381 cx.update_editor(|editor, window, cx| {
20382 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20383 });
20384 executor.run_until_parked();
20385 cx.assert_index_text(None);
20386}
20387
20388async fn setup_indent_guides_editor(
20389 text: &str,
20390 cx: &mut TestAppContext,
20391) -> (BufferId, EditorTestContext) {
20392 init_test(cx, |_| {});
20393
20394 let mut cx = EditorTestContext::new(cx).await;
20395
20396 let buffer_id = cx.update_editor(|editor, window, cx| {
20397 editor.set_text(text, window, cx);
20398 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
20399
20400 buffer_ids[0]
20401 });
20402
20403 (buffer_id, cx)
20404}
20405
20406fn assert_indent_guides(
20407 range: Range<u32>,
20408 expected: Vec<IndentGuide>,
20409 active_indices: Option<Vec<usize>>,
20410 cx: &mut EditorTestContext,
20411) {
20412 let indent_guides = cx.update_editor(|editor, window, cx| {
20413 let snapshot = editor.snapshot(window, cx).display_snapshot;
20414 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
20415 editor,
20416 MultiBufferRow(range.start)..MultiBufferRow(range.end),
20417 true,
20418 &snapshot,
20419 cx,
20420 );
20421
20422 indent_guides.sort_by(|a, b| {
20423 a.depth.cmp(&b.depth).then(
20424 a.start_row
20425 .cmp(&b.start_row)
20426 .then(a.end_row.cmp(&b.end_row)),
20427 )
20428 });
20429 indent_guides
20430 });
20431
20432 if let Some(expected) = active_indices {
20433 let active_indices = cx.update_editor(|editor, window, cx| {
20434 let snapshot = editor.snapshot(window, cx).display_snapshot;
20435 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
20436 });
20437
20438 assert_eq!(
20439 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
20440 expected,
20441 "Active indent guide indices do not match"
20442 );
20443 }
20444
20445 assert_eq!(indent_guides, expected, "Indent guides do not match");
20446}
20447
20448fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
20449 IndentGuide {
20450 buffer_id,
20451 start_row: MultiBufferRow(start_row),
20452 end_row: MultiBufferRow(end_row),
20453 depth,
20454 tab_size: 4,
20455 settings: IndentGuideSettings {
20456 enabled: true,
20457 line_width: 1,
20458 active_line_width: 1,
20459 coloring: IndentGuideColoring::default(),
20460 background_coloring: IndentGuideBackgroundColoring::default(),
20461 },
20462 }
20463}
20464
20465#[gpui::test]
20466async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
20467 let (buffer_id, mut cx) = setup_indent_guides_editor(
20468 &"
20469 fn main() {
20470 let a = 1;
20471 }"
20472 .unindent(),
20473 cx,
20474 )
20475 .await;
20476
20477 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20478}
20479
20480#[gpui::test]
20481async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
20482 let (buffer_id, mut cx) = setup_indent_guides_editor(
20483 &"
20484 fn main() {
20485 let a = 1;
20486 let b = 2;
20487 }"
20488 .unindent(),
20489 cx,
20490 )
20491 .await;
20492
20493 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
20494}
20495
20496#[gpui::test]
20497async fn test_indent_guide_nested(cx: &mut TestAppContext) {
20498 let (buffer_id, mut cx) = setup_indent_guides_editor(
20499 &"
20500 fn main() {
20501 let a = 1;
20502 if a == 3 {
20503 let b = 2;
20504 } else {
20505 let c = 3;
20506 }
20507 }"
20508 .unindent(),
20509 cx,
20510 )
20511 .await;
20512
20513 assert_indent_guides(
20514 0..8,
20515 vec![
20516 indent_guide(buffer_id, 1, 6, 0),
20517 indent_guide(buffer_id, 3, 3, 1),
20518 indent_guide(buffer_id, 5, 5, 1),
20519 ],
20520 None,
20521 &mut cx,
20522 );
20523}
20524
20525#[gpui::test]
20526async fn test_indent_guide_tab(cx: &mut TestAppContext) {
20527 let (buffer_id, mut cx) = setup_indent_guides_editor(
20528 &"
20529 fn main() {
20530 let a = 1;
20531 let b = 2;
20532 let c = 3;
20533 }"
20534 .unindent(),
20535 cx,
20536 )
20537 .await;
20538
20539 assert_indent_guides(
20540 0..5,
20541 vec![
20542 indent_guide(buffer_id, 1, 3, 0),
20543 indent_guide(buffer_id, 2, 2, 1),
20544 ],
20545 None,
20546 &mut cx,
20547 );
20548}
20549
20550#[gpui::test]
20551async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
20552 let (buffer_id, mut cx) = setup_indent_guides_editor(
20553 &"
20554 fn main() {
20555 let a = 1;
20556
20557 let c = 3;
20558 }"
20559 .unindent(),
20560 cx,
20561 )
20562 .await;
20563
20564 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
20565}
20566
20567#[gpui::test]
20568async fn test_indent_guide_complex(cx: &mut TestAppContext) {
20569 let (buffer_id, mut cx) = setup_indent_guides_editor(
20570 &"
20571 fn main() {
20572 let a = 1;
20573
20574 let c = 3;
20575
20576 if a == 3 {
20577 let b = 2;
20578 } else {
20579 let c = 3;
20580 }
20581 }"
20582 .unindent(),
20583 cx,
20584 )
20585 .await;
20586
20587 assert_indent_guides(
20588 0..11,
20589 vec![
20590 indent_guide(buffer_id, 1, 9, 0),
20591 indent_guide(buffer_id, 6, 6, 1),
20592 indent_guide(buffer_id, 8, 8, 1),
20593 ],
20594 None,
20595 &mut cx,
20596 );
20597}
20598
20599#[gpui::test]
20600async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
20601 let (buffer_id, mut cx) = setup_indent_guides_editor(
20602 &"
20603 fn main() {
20604 let a = 1;
20605
20606 let c = 3;
20607
20608 if a == 3 {
20609 let b = 2;
20610 } else {
20611 let c = 3;
20612 }
20613 }"
20614 .unindent(),
20615 cx,
20616 )
20617 .await;
20618
20619 assert_indent_guides(
20620 1..11,
20621 vec![
20622 indent_guide(buffer_id, 1, 9, 0),
20623 indent_guide(buffer_id, 6, 6, 1),
20624 indent_guide(buffer_id, 8, 8, 1),
20625 ],
20626 None,
20627 &mut cx,
20628 );
20629}
20630
20631#[gpui::test]
20632async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
20633 let (buffer_id, mut cx) = setup_indent_guides_editor(
20634 &"
20635 fn main() {
20636 let a = 1;
20637
20638 let c = 3;
20639
20640 if a == 3 {
20641 let b = 2;
20642 } else {
20643 let c = 3;
20644 }
20645 }"
20646 .unindent(),
20647 cx,
20648 )
20649 .await;
20650
20651 assert_indent_guides(
20652 1..10,
20653 vec![
20654 indent_guide(buffer_id, 1, 9, 0),
20655 indent_guide(buffer_id, 6, 6, 1),
20656 indent_guide(buffer_id, 8, 8, 1),
20657 ],
20658 None,
20659 &mut cx,
20660 );
20661}
20662
20663#[gpui::test]
20664async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
20665 let (buffer_id, mut cx) = setup_indent_guides_editor(
20666 &"
20667 fn main() {
20668 if a {
20669 b(
20670 c,
20671 d,
20672 )
20673 } else {
20674 e(
20675 f
20676 )
20677 }
20678 }"
20679 .unindent(),
20680 cx,
20681 )
20682 .await;
20683
20684 assert_indent_guides(
20685 0..11,
20686 vec![
20687 indent_guide(buffer_id, 1, 10, 0),
20688 indent_guide(buffer_id, 2, 5, 1),
20689 indent_guide(buffer_id, 7, 9, 1),
20690 indent_guide(buffer_id, 3, 4, 2),
20691 indent_guide(buffer_id, 8, 8, 2),
20692 ],
20693 None,
20694 &mut cx,
20695 );
20696
20697 cx.update_editor(|editor, window, cx| {
20698 editor.fold_at(MultiBufferRow(2), window, cx);
20699 assert_eq!(
20700 editor.display_text(cx),
20701 "
20702 fn main() {
20703 if a {
20704 b(⋯
20705 )
20706 } else {
20707 e(
20708 f
20709 )
20710 }
20711 }"
20712 .unindent()
20713 );
20714 });
20715
20716 assert_indent_guides(
20717 0..11,
20718 vec![
20719 indent_guide(buffer_id, 1, 10, 0),
20720 indent_guide(buffer_id, 2, 5, 1),
20721 indent_guide(buffer_id, 7, 9, 1),
20722 indent_guide(buffer_id, 8, 8, 2),
20723 ],
20724 None,
20725 &mut cx,
20726 );
20727}
20728
20729#[gpui::test]
20730async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
20731 let (buffer_id, mut cx) = setup_indent_guides_editor(
20732 &"
20733 block1
20734 block2
20735 block3
20736 block4
20737 block2
20738 block1
20739 block1"
20740 .unindent(),
20741 cx,
20742 )
20743 .await;
20744
20745 assert_indent_guides(
20746 1..10,
20747 vec![
20748 indent_guide(buffer_id, 1, 4, 0),
20749 indent_guide(buffer_id, 2, 3, 1),
20750 indent_guide(buffer_id, 3, 3, 2),
20751 ],
20752 None,
20753 &mut cx,
20754 );
20755}
20756
20757#[gpui::test]
20758async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
20759 let (buffer_id, mut cx) = setup_indent_guides_editor(
20760 &"
20761 block1
20762 block2
20763 block3
20764
20765 block1
20766 block1"
20767 .unindent(),
20768 cx,
20769 )
20770 .await;
20771
20772 assert_indent_guides(
20773 0..6,
20774 vec![
20775 indent_guide(buffer_id, 1, 2, 0),
20776 indent_guide(buffer_id, 2, 2, 1),
20777 ],
20778 None,
20779 &mut cx,
20780 );
20781}
20782
20783#[gpui::test]
20784async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
20785 let (buffer_id, mut cx) = setup_indent_guides_editor(
20786 &"
20787 function component() {
20788 \treturn (
20789 \t\t\t
20790 \t\t<div>
20791 \t\t\t<abc></abc>
20792 \t\t</div>
20793 \t)
20794 }"
20795 .unindent(),
20796 cx,
20797 )
20798 .await;
20799
20800 assert_indent_guides(
20801 0..8,
20802 vec![
20803 indent_guide(buffer_id, 1, 6, 0),
20804 indent_guide(buffer_id, 2, 5, 1),
20805 indent_guide(buffer_id, 4, 4, 2),
20806 ],
20807 None,
20808 &mut cx,
20809 );
20810}
20811
20812#[gpui::test]
20813async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
20814 let (buffer_id, mut cx) = setup_indent_guides_editor(
20815 &"
20816 function component() {
20817 \treturn (
20818 \t
20819 \t\t<div>
20820 \t\t\t<abc></abc>
20821 \t\t</div>
20822 \t)
20823 }"
20824 .unindent(),
20825 cx,
20826 )
20827 .await;
20828
20829 assert_indent_guides(
20830 0..8,
20831 vec![
20832 indent_guide(buffer_id, 1, 6, 0),
20833 indent_guide(buffer_id, 2, 5, 1),
20834 indent_guide(buffer_id, 4, 4, 2),
20835 ],
20836 None,
20837 &mut cx,
20838 );
20839}
20840
20841#[gpui::test]
20842async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
20843 let (buffer_id, mut cx) = setup_indent_guides_editor(
20844 &"
20845 block1
20846
20847
20848
20849 block2
20850 "
20851 .unindent(),
20852 cx,
20853 )
20854 .await;
20855
20856 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20857}
20858
20859#[gpui::test]
20860async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
20861 let (buffer_id, mut cx) = setup_indent_guides_editor(
20862 &"
20863 def a:
20864 \tb = 3
20865 \tif True:
20866 \t\tc = 4
20867 \t\td = 5
20868 \tprint(b)
20869 "
20870 .unindent(),
20871 cx,
20872 )
20873 .await;
20874
20875 assert_indent_guides(
20876 0..6,
20877 vec![
20878 indent_guide(buffer_id, 1, 5, 0),
20879 indent_guide(buffer_id, 3, 4, 1),
20880 ],
20881 None,
20882 &mut cx,
20883 );
20884}
20885
20886#[gpui::test]
20887async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
20888 let (buffer_id, mut cx) = setup_indent_guides_editor(
20889 &"
20890 fn main() {
20891 let a = 1;
20892 }"
20893 .unindent(),
20894 cx,
20895 )
20896 .await;
20897
20898 cx.update_editor(|editor, window, cx| {
20899 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20900 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20901 });
20902 });
20903
20904 assert_indent_guides(
20905 0..3,
20906 vec![indent_guide(buffer_id, 1, 1, 0)],
20907 Some(vec![0]),
20908 &mut cx,
20909 );
20910}
20911
20912#[gpui::test]
20913async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
20914 let (buffer_id, mut cx) = setup_indent_guides_editor(
20915 &"
20916 fn main() {
20917 if 1 == 2 {
20918 let a = 1;
20919 }
20920 }"
20921 .unindent(),
20922 cx,
20923 )
20924 .await;
20925
20926 cx.update_editor(|editor, window, cx| {
20927 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20928 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20929 });
20930 });
20931
20932 assert_indent_guides(
20933 0..4,
20934 vec![
20935 indent_guide(buffer_id, 1, 3, 0),
20936 indent_guide(buffer_id, 2, 2, 1),
20937 ],
20938 Some(vec![1]),
20939 &mut cx,
20940 );
20941
20942 cx.update_editor(|editor, window, cx| {
20943 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20944 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20945 });
20946 });
20947
20948 assert_indent_guides(
20949 0..4,
20950 vec![
20951 indent_guide(buffer_id, 1, 3, 0),
20952 indent_guide(buffer_id, 2, 2, 1),
20953 ],
20954 Some(vec![1]),
20955 &mut cx,
20956 );
20957
20958 cx.update_editor(|editor, window, cx| {
20959 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20960 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
20961 });
20962 });
20963
20964 assert_indent_guides(
20965 0..4,
20966 vec![
20967 indent_guide(buffer_id, 1, 3, 0),
20968 indent_guide(buffer_id, 2, 2, 1),
20969 ],
20970 Some(vec![0]),
20971 &mut cx,
20972 );
20973}
20974
20975#[gpui::test]
20976async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
20977 let (buffer_id, mut cx) = setup_indent_guides_editor(
20978 &"
20979 fn main() {
20980 let a = 1;
20981
20982 let b = 2;
20983 }"
20984 .unindent(),
20985 cx,
20986 )
20987 .await;
20988
20989 cx.update_editor(|editor, window, cx| {
20990 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20991 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20992 });
20993 });
20994
20995 assert_indent_guides(
20996 0..5,
20997 vec![indent_guide(buffer_id, 1, 3, 0)],
20998 Some(vec![0]),
20999 &mut cx,
21000 );
21001}
21002
21003#[gpui::test]
21004async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
21005 let (buffer_id, mut cx) = setup_indent_guides_editor(
21006 &"
21007 def m:
21008 a = 1
21009 pass"
21010 .unindent(),
21011 cx,
21012 )
21013 .await;
21014
21015 cx.update_editor(|editor, window, cx| {
21016 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21017 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
21018 });
21019 });
21020
21021 assert_indent_guides(
21022 0..3,
21023 vec![indent_guide(buffer_id, 1, 2, 0)],
21024 Some(vec![0]),
21025 &mut cx,
21026 );
21027}
21028
21029#[gpui::test]
21030async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
21031 init_test(cx, |_| {});
21032 let mut cx = EditorTestContext::new(cx).await;
21033 let text = indoc! {
21034 "
21035 impl A {
21036 fn b() {
21037 0;
21038 3;
21039 5;
21040 6;
21041 7;
21042 }
21043 }
21044 "
21045 };
21046 let base_text = indoc! {
21047 "
21048 impl A {
21049 fn b() {
21050 0;
21051 1;
21052 2;
21053 3;
21054 4;
21055 }
21056 fn c() {
21057 5;
21058 6;
21059 7;
21060 }
21061 }
21062 "
21063 };
21064
21065 cx.update_editor(|editor, window, cx| {
21066 editor.set_text(text, window, cx);
21067
21068 editor.buffer().update(cx, |multibuffer, cx| {
21069 let buffer = multibuffer.as_singleton().unwrap();
21070 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
21071
21072 multibuffer.set_all_diff_hunks_expanded(cx);
21073 multibuffer.add_diff(diff, cx);
21074
21075 buffer.read(cx).remote_id()
21076 })
21077 });
21078 cx.run_until_parked();
21079
21080 cx.assert_state_with_diff(
21081 indoc! { "
21082 impl A {
21083 fn b() {
21084 0;
21085 - 1;
21086 - 2;
21087 3;
21088 - 4;
21089 - }
21090 - fn c() {
21091 5;
21092 6;
21093 7;
21094 }
21095 }
21096 ˇ"
21097 }
21098 .to_string(),
21099 );
21100
21101 let mut actual_guides = cx.update_editor(|editor, window, cx| {
21102 editor
21103 .snapshot(window, cx)
21104 .buffer_snapshot()
21105 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
21106 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
21107 .collect::<Vec<_>>()
21108 });
21109 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
21110 assert_eq!(
21111 actual_guides,
21112 vec![
21113 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
21114 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
21115 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
21116 ]
21117 );
21118}
21119
21120#[gpui::test]
21121async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
21122 init_test(cx, |_| {});
21123 let mut cx = EditorTestContext::new(cx).await;
21124
21125 let diff_base = r#"
21126 a
21127 b
21128 c
21129 "#
21130 .unindent();
21131
21132 cx.set_state(
21133 &r#"
21134 ˇA
21135 b
21136 C
21137 "#
21138 .unindent(),
21139 );
21140 cx.set_head_text(&diff_base);
21141 cx.update_editor(|editor, window, cx| {
21142 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21143 });
21144 executor.run_until_parked();
21145
21146 let both_hunks_expanded = r#"
21147 - a
21148 + ˇA
21149 b
21150 - c
21151 + C
21152 "#
21153 .unindent();
21154
21155 cx.assert_state_with_diff(both_hunks_expanded.clone());
21156
21157 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21158 let snapshot = editor.snapshot(window, cx);
21159 let hunks = editor
21160 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21161 .collect::<Vec<_>>();
21162 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21163 let buffer_id = hunks[0].buffer_id;
21164 hunks
21165 .into_iter()
21166 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
21167 .collect::<Vec<_>>()
21168 });
21169 assert_eq!(hunk_ranges.len(), 2);
21170
21171 cx.update_editor(|editor, _, cx| {
21172 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21173 });
21174 executor.run_until_parked();
21175
21176 let second_hunk_expanded = r#"
21177 ˇA
21178 b
21179 - c
21180 + C
21181 "#
21182 .unindent();
21183
21184 cx.assert_state_with_diff(second_hunk_expanded);
21185
21186 cx.update_editor(|editor, _, cx| {
21187 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21188 });
21189 executor.run_until_parked();
21190
21191 cx.assert_state_with_diff(both_hunks_expanded.clone());
21192
21193 cx.update_editor(|editor, _, cx| {
21194 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21195 });
21196 executor.run_until_parked();
21197
21198 let first_hunk_expanded = r#"
21199 - a
21200 + ˇA
21201 b
21202 C
21203 "#
21204 .unindent();
21205
21206 cx.assert_state_with_diff(first_hunk_expanded);
21207
21208 cx.update_editor(|editor, _, cx| {
21209 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21210 });
21211 executor.run_until_parked();
21212
21213 cx.assert_state_with_diff(both_hunks_expanded);
21214
21215 cx.set_state(
21216 &r#"
21217 ˇA
21218 b
21219 "#
21220 .unindent(),
21221 );
21222 cx.run_until_parked();
21223
21224 // TODO this cursor position seems bad
21225 cx.assert_state_with_diff(
21226 r#"
21227 - ˇa
21228 + A
21229 b
21230 "#
21231 .unindent(),
21232 );
21233
21234 cx.update_editor(|editor, window, cx| {
21235 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21236 });
21237
21238 cx.assert_state_with_diff(
21239 r#"
21240 - ˇa
21241 + A
21242 b
21243 - c
21244 "#
21245 .unindent(),
21246 );
21247
21248 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21249 let snapshot = editor.snapshot(window, cx);
21250 let hunks = editor
21251 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21252 .collect::<Vec<_>>();
21253 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21254 let buffer_id = hunks[0].buffer_id;
21255 hunks
21256 .into_iter()
21257 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
21258 .collect::<Vec<_>>()
21259 });
21260 assert_eq!(hunk_ranges.len(), 2);
21261
21262 cx.update_editor(|editor, _, cx| {
21263 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21264 });
21265 executor.run_until_parked();
21266
21267 cx.assert_state_with_diff(
21268 r#"
21269 - ˇa
21270 + A
21271 b
21272 "#
21273 .unindent(),
21274 );
21275}
21276
21277#[gpui::test]
21278async fn test_toggle_deletion_hunk_at_start_of_file(
21279 executor: BackgroundExecutor,
21280 cx: &mut TestAppContext,
21281) {
21282 init_test(cx, |_| {});
21283 let mut cx = EditorTestContext::new(cx).await;
21284
21285 let diff_base = r#"
21286 a
21287 b
21288 c
21289 "#
21290 .unindent();
21291
21292 cx.set_state(
21293 &r#"
21294 ˇb
21295 c
21296 "#
21297 .unindent(),
21298 );
21299 cx.set_head_text(&diff_base);
21300 cx.update_editor(|editor, window, cx| {
21301 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21302 });
21303 executor.run_until_parked();
21304
21305 let hunk_expanded = r#"
21306 - a
21307 ˇb
21308 c
21309 "#
21310 .unindent();
21311
21312 cx.assert_state_with_diff(hunk_expanded.clone());
21313
21314 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21315 let snapshot = editor.snapshot(window, cx);
21316 let hunks = editor
21317 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21318 .collect::<Vec<_>>();
21319 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21320 let buffer_id = hunks[0].buffer_id;
21321 hunks
21322 .into_iter()
21323 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
21324 .collect::<Vec<_>>()
21325 });
21326 assert_eq!(hunk_ranges.len(), 1);
21327
21328 cx.update_editor(|editor, _, cx| {
21329 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21330 });
21331 executor.run_until_parked();
21332
21333 let hunk_collapsed = r#"
21334 ˇb
21335 c
21336 "#
21337 .unindent();
21338
21339 cx.assert_state_with_diff(hunk_collapsed);
21340
21341 cx.update_editor(|editor, _, cx| {
21342 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21343 });
21344 executor.run_until_parked();
21345
21346 cx.assert_state_with_diff(hunk_expanded);
21347}
21348
21349#[gpui::test]
21350async fn test_display_diff_hunks(cx: &mut TestAppContext) {
21351 init_test(cx, |_| {});
21352
21353 let fs = FakeFs::new(cx.executor());
21354 fs.insert_tree(
21355 path!("/test"),
21356 json!({
21357 ".git": {},
21358 "file-1": "ONE\n",
21359 "file-2": "TWO\n",
21360 "file-3": "THREE\n",
21361 }),
21362 )
21363 .await;
21364
21365 fs.set_head_for_repo(
21366 path!("/test/.git").as_ref(),
21367 &[
21368 ("file-1", "one\n".into()),
21369 ("file-2", "two\n".into()),
21370 ("file-3", "three\n".into()),
21371 ],
21372 "deadbeef",
21373 );
21374
21375 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
21376 let mut buffers = vec![];
21377 for i in 1..=3 {
21378 let buffer = project
21379 .update(cx, |project, cx| {
21380 let path = format!(path!("/test/file-{}"), i);
21381 project.open_local_buffer(path, cx)
21382 })
21383 .await
21384 .unwrap();
21385 buffers.push(buffer);
21386 }
21387
21388 let multibuffer = cx.new(|cx| {
21389 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
21390 multibuffer.set_all_diff_hunks_expanded(cx);
21391 for buffer in &buffers {
21392 let snapshot = buffer.read(cx).snapshot();
21393 multibuffer.set_excerpts_for_path(
21394 PathKey::with_sort_prefix(0, buffer.read(cx).file().unwrap().path().clone()),
21395 buffer.clone(),
21396 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
21397 2,
21398 cx,
21399 );
21400 }
21401 multibuffer
21402 });
21403
21404 let editor = cx.add_window(|window, cx| {
21405 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
21406 });
21407 cx.run_until_parked();
21408
21409 let snapshot = editor
21410 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21411 .unwrap();
21412 let hunks = snapshot
21413 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
21414 .map(|hunk| match hunk {
21415 DisplayDiffHunk::Unfolded {
21416 display_row_range, ..
21417 } => display_row_range,
21418 DisplayDiffHunk::Folded { .. } => unreachable!(),
21419 })
21420 .collect::<Vec<_>>();
21421 assert_eq!(
21422 hunks,
21423 [
21424 DisplayRow(2)..DisplayRow(4),
21425 DisplayRow(7)..DisplayRow(9),
21426 DisplayRow(12)..DisplayRow(14),
21427 ]
21428 );
21429}
21430
21431#[gpui::test]
21432async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
21433 init_test(cx, |_| {});
21434
21435 let mut cx = EditorTestContext::new(cx).await;
21436 cx.set_head_text(indoc! { "
21437 one
21438 two
21439 three
21440 four
21441 five
21442 "
21443 });
21444 cx.set_index_text(indoc! { "
21445 one
21446 two
21447 three
21448 four
21449 five
21450 "
21451 });
21452 cx.set_state(indoc! {"
21453 one
21454 TWO
21455 ˇTHREE
21456 FOUR
21457 five
21458 "});
21459 cx.run_until_parked();
21460 cx.update_editor(|editor, window, cx| {
21461 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21462 });
21463 cx.run_until_parked();
21464 cx.assert_index_text(Some(indoc! {"
21465 one
21466 TWO
21467 THREE
21468 FOUR
21469 five
21470 "}));
21471 cx.set_state(indoc! { "
21472 one
21473 TWO
21474 ˇTHREE-HUNDRED
21475 FOUR
21476 five
21477 "});
21478 cx.run_until_parked();
21479 cx.update_editor(|editor, window, cx| {
21480 let snapshot = editor.snapshot(window, cx);
21481 let hunks = editor
21482 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21483 .collect::<Vec<_>>();
21484 assert_eq!(hunks.len(), 1);
21485 assert_eq!(
21486 hunks[0].status(),
21487 DiffHunkStatus {
21488 kind: DiffHunkStatusKind::Modified,
21489 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
21490 }
21491 );
21492
21493 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21494 });
21495 cx.run_until_parked();
21496 cx.assert_index_text(Some(indoc! {"
21497 one
21498 TWO
21499 THREE-HUNDRED
21500 FOUR
21501 five
21502 "}));
21503}
21504
21505#[gpui::test]
21506fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
21507 init_test(cx, |_| {});
21508
21509 let editor = cx.add_window(|window, cx| {
21510 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
21511 build_editor(buffer, window, cx)
21512 });
21513
21514 let render_args = Arc::new(Mutex::new(None));
21515 let snapshot = editor
21516 .update(cx, |editor, window, cx| {
21517 let snapshot = editor.buffer().read(cx).snapshot(cx);
21518 let range =
21519 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
21520
21521 struct RenderArgs {
21522 row: MultiBufferRow,
21523 folded: bool,
21524 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
21525 }
21526
21527 let crease = Crease::inline(
21528 range,
21529 FoldPlaceholder::test(),
21530 {
21531 let toggle_callback = render_args.clone();
21532 move |row, folded, callback, _window, _cx| {
21533 *toggle_callback.lock() = Some(RenderArgs {
21534 row,
21535 folded,
21536 callback,
21537 });
21538 div()
21539 }
21540 },
21541 |_row, _folded, _window, _cx| div(),
21542 );
21543
21544 editor.insert_creases(Some(crease), cx);
21545 let snapshot = editor.snapshot(window, cx);
21546 let _div =
21547 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
21548 snapshot
21549 })
21550 .unwrap();
21551
21552 let render_args = render_args.lock().take().unwrap();
21553 assert_eq!(render_args.row, MultiBufferRow(1));
21554 assert!(!render_args.folded);
21555 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21556
21557 cx.update_window(*editor, |_, window, cx| {
21558 (render_args.callback)(true, window, cx)
21559 })
21560 .unwrap();
21561 let snapshot = editor
21562 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21563 .unwrap();
21564 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
21565
21566 cx.update_window(*editor, |_, window, cx| {
21567 (render_args.callback)(false, window, cx)
21568 })
21569 .unwrap();
21570 let snapshot = editor
21571 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21572 .unwrap();
21573 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21574}
21575
21576#[gpui::test]
21577async fn test_input_text(cx: &mut TestAppContext) {
21578 init_test(cx, |_| {});
21579 let mut cx = EditorTestContext::new(cx).await;
21580
21581 cx.set_state(
21582 &r#"ˇone
21583 two
21584
21585 three
21586 fourˇ
21587 five
21588
21589 siˇx"#
21590 .unindent(),
21591 );
21592
21593 cx.dispatch_action(HandleInput(String::new()));
21594 cx.assert_editor_state(
21595 &r#"ˇone
21596 two
21597
21598 three
21599 fourˇ
21600 five
21601
21602 siˇx"#
21603 .unindent(),
21604 );
21605
21606 cx.dispatch_action(HandleInput("AAAA".to_string()));
21607 cx.assert_editor_state(
21608 &r#"AAAAˇone
21609 two
21610
21611 three
21612 fourAAAAˇ
21613 five
21614
21615 siAAAAˇx"#
21616 .unindent(),
21617 );
21618}
21619
21620#[gpui::test]
21621async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
21622 init_test(cx, |_| {});
21623
21624 let mut cx = EditorTestContext::new(cx).await;
21625 cx.set_state(
21626 r#"let foo = 1;
21627let foo = 2;
21628let foo = 3;
21629let fooˇ = 4;
21630let foo = 5;
21631let foo = 6;
21632let foo = 7;
21633let foo = 8;
21634let foo = 9;
21635let foo = 10;
21636let foo = 11;
21637let foo = 12;
21638let foo = 13;
21639let foo = 14;
21640let foo = 15;"#,
21641 );
21642
21643 cx.update_editor(|e, window, cx| {
21644 assert_eq!(
21645 e.next_scroll_position,
21646 NextScrollCursorCenterTopBottom::Center,
21647 "Default next scroll direction is center",
21648 );
21649
21650 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21651 assert_eq!(
21652 e.next_scroll_position,
21653 NextScrollCursorCenterTopBottom::Top,
21654 "After center, next scroll direction should be top",
21655 );
21656
21657 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21658 assert_eq!(
21659 e.next_scroll_position,
21660 NextScrollCursorCenterTopBottom::Bottom,
21661 "After top, next scroll direction should be bottom",
21662 );
21663
21664 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21665 assert_eq!(
21666 e.next_scroll_position,
21667 NextScrollCursorCenterTopBottom::Center,
21668 "After bottom, scrolling should start over",
21669 );
21670
21671 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21672 assert_eq!(
21673 e.next_scroll_position,
21674 NextScrollCursorCenterTopBottom::Top,
21675 "Scrolling continues if retriggered fast enough"
21676 );
21677 });
21678
21679 cx.executor()
21680 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
21681 cx.executor().run_until_parked();
21682 cx.update_editor(|e, _, _| {
21683 assert_eq!(
21684 e.next_scroll_position,
21685 NextScrollCursorCenterTopBottom::Center,
21686 "If scrolling is not triggered fast enough, it should reset"
21687 );
21688 });
21689}
21690
21691#[gpui::test]
21692async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
21693 init_test(cx, |_| {});
21694 let mut cx = EditorLspTestContext::new_rust(
21695 lsp::ServerCapabilities {
21696 definition_provider: Some(lsp::OneOf::Left(true)),
21697 references_provider: Some(lsp::OneOf::Left(true)),
21698 ..lsp::ServerCapabilities::default()
21699 },
21700 cx,
21701 )
21702 .await;
21703
21704 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
21705 let go_to_definition = cx
21706 .lsp
21707 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21708 move |params, _| async move {
21709 if empty_go_to_definition {
21710 Ok(None)
21711 } else {
21712 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
21713 uri: params.text_document_position_params.text_document.uri,
21714 range: lsp::Range::new(
21715 lsp::Position::new(4, 3),
21716 lsp::Position::new(4, 6),
21717 ),
21718 })))
21719 }
21720 },
21721 );
21722 let references = cx
21723 .lsp
21724 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21725 Ok(Some(vec![lsp::Location {
21726 uri: params.text_document_position.text_document.uri,
21727 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
21728 }]))
21729 });
21730 (go_to_definition, references)
21731 };
21732
21733 cx.set_state(
21734 &r#"fn one() {
21735 let mut a = ˇtwo();
21736 }
21737
21738 fn two() {}"#
21739 .unindent(),
21740 );
21741 set_up_lsp_handlers(false, &mut cx);
21742 let navigated = cx
21743 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21744 .await
21745 .expect("Failed to navigate to definition");
21746 assert_eq!(
21747 navigated,
21748 Navigated::Yes,
21749 "Should have navigated to definition from the GetDefinition response"
21750 );
21751 cx.assert_editor_state(
21752 &r#"fn one() {
21753 let mut a = two();
21754 }
21755
21756 fn «twoˇ»() {}"#
21757 .unindent(),
21758 );
21759
21760 let editors = cx.update_workspace(|workspace, _, cx| {
21761 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21762 });
21763 cx.update_editor(|_, _, test_editor_cx| {
21764 assert_eq!(
21765 editors.len(),
21766 1,
21767 "Initially, only one, test, editor should be open in the workspace"
21768 );
21769 assert_eq!(
21770 test_editor_cx.entity(),
21771 editors.last().expect("Asserted len is 1").clone()
21772 );
21773 });
21774
21775 set_up_lsp_handlers(true, &mut cx);
21776 let navigated = cx
21777 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21778 .await
21779 .expect("Failed to navigate to lookup references");
21780 assert_eq!(
21781 navigated,
21782 Navigated::Yes,
21783 "Should have navigated to references as a fallback after empty GoToDefinition response"
21784 );
21785 // We should not change the selections in the existing file,
21786 // if opening another milti buffer with the references
21787 cx.assert_editor_state(
21788 &r#"fn one() {
21789 let mut a = two();
21790 }
21791
21792 fn «twoˇ»() {}"#
21793 .unindent(),
21794 );
21795 let editors = cx.update_workspace(|workspace, _, cx| {
21796 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21797 });
21798 cx.update_editor(|_, _, test_editor_cx| {
21799 assert_eq!(
21800 editors.len(),
21801 2,
21802 "After falling back to references search, we open a new editor with the results"
21803 );
21804 let references_fallback_text = editors
21805 .into_iter()
21806 .find(|new_editor| *new_editor != test_editor_cx.entity())
21807 .expect("Should have one non-test editor now")
21808 .read(test_editor_cx)
21809 .text(test_editor_cx);
21810 assert_eq!(
21811 references_fallback_text, "fn one() {\n let mut a = two();\n}",
21812 "Should use the range from the references response and not the GoToDefinition one"
21813 );
21814 });
21815}
21816
21817#[gpui::test]
21818async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
21819 init_test(cx, |_| {});
21820 cx.update(|cx| {
21821 let mut editor_settings = EditorSettings::get_global(cx).clone();
21822 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
21823 EditorSettings::override_global(editor_settings, cx);
21824 });
21825 let mut cx = EditorLspTestContext::new_rust(
21826 lsp::ServerCapabilities {
21827 definition_provider: Some(lsp::OneOf::Left(true)),
21828 references_provider: Some(lsp::OneOf::Left(true)),
21829 ..lsp::ServerCapabilities::default()
21830 },
21831 cx,
21832 )
21833 .await;
21834 let original_state = r#"fn one() {
21835 let mut a = ˇtwo();
21836 }
21837
21838 fn two() {}"#
21839 .unindent();
21840 cx.set_state(&original_state);
21841
21842 let mut go_to_definition = cx
21843 .lsp
21844 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21845 move |_, _| async move { Ok(None) },
21846 );
21847 let _references = cx
21848 .lsp
21849 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
21850 panic!("Should not call for references with no go to definition fallback")
21851 });
21852
21853 let navigated = cx
21854 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21855 .await
21856 .expect("Failed to navigate to lookup references");
21857 go_to_definition
21858 .next()
21859 .await
21860 .expect("Should have called the go_to_definition handler");
21861
21862 assert_eq!(
21863 navigated,
21864 Navigated::No,
21865 "Should have navigated to references as a fallback after empty GoToDefinition response"
21866 );
21867 cx.assert_editor_state(&original_state);
21868 let editors = cx.update_workspace(|workspace, _, cx| {
21869 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21870 });
21871 cx.update_editor(|_, _, _| {
21872 assert_eq!(
21873 editors.len(),
21874 1,
21875 "After unsuccessful fallback, no other editor should have been opened"
21876 );
21877 });
21878}
21879
21880#[gpui::test]
21881async fn test_find_all_references_editor_reuse(cx: &mut TestAppContext) {
21882 init_test(cx, |_| {});
21883 let mut cx = EditorLspTestContext::new_rust(
21884 lsp::ServerCapabilities {
21885 references_provider: Some(lsp::OneOf::Left(true)),
21886 ..lsp::ServerCapabilities::default()
21887 },
21888 cx,
21889 )
21890 .await;
21891
21892 cx.set_state(
21893 &r#"
21894 fn one() {
21895 let mut a = two();
21896 }
21897
21898 fn ˇtwo() {}"#
21899 .unindent(),
21900 );
21901 cx.lsp
21902 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21903 Ok(Some(vec![
21904 lsp::Location {
21905 uri: params.text_document_position.text_document.uri.clone(),
21906 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21907 },
21908 lsp::Location {
21909 uri: params.text_document_position.text_document.uri,
21910 range: lsp::Range::new(lsp::Position::new(4, 4), lsp::Position::new(4, 7)),
21911 },
21912 ]))
21913 });
21914 let navigated = cx
21915 .update_editor(|editor, window, cx| {
21916 editor.find_all_references(&FindAllReferences, window, cx)
21917 })
21918 .unwrap()
21919 .await
21920 .expect("Failed to navigate to references");
21921 assert_eq!(
21922 navigated,
21923 Navigated::Yes,
21924 "Should have navigated to references from the FindAllReferences response"
21925 );
21926 cx.assert_editor_state(
21927 &r#"fn one() {
21928 let mut a = two();
21929 }
21930
21931 fn ˇtwo() {}"#
21932 .unindent(),
21933 );
21934
21935 let editors = cx.update_workspace(|workspace, _, cx| {
21936 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21937 });
21938 cx.update_editor(|_, _, _| {
21939 assert_eq!(editors.len(), 2, "We should have opened a new multibuffer");
21940 });
21941
21942 cx.set_state(
21943 &r#"fn one() {
21944 let mut a = ˇtwo();
21945 }
21946
21947 fn two() {}"#
21948 .unindent(),
21949 );
21950 let navigated = cx
21951 .update_editor(|editor, window, cx| {
21952 editor.find_all_references(&FindAllReferences, window, cx)
21953 })
21954 .unwrap()
21955 .await
21956 .expect("Failed to navigate to references");
21957 assert_eq!(
21958 navigated,
21959 Navigated::Yes,
21960 "Should have navigated to references from the FindAllReferences response"
21961 );
21962 cx.assert_editor_state(
21963 &r#"fn one() {
21964 let mut a = ˇtwo();
21965 }
21966
21967 fn two() {}"#
21968 .unindent(),
21969 );
21970 let editors = cx.update_workspace(|workspace, _, cx| {
21971 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21972 });
21973 cx.update_editor(|_, _, _| {
21974 assert_eq!(
21975 editors.len(),
21976 2,
21977 "should have re-used the previous multibuffer"
21978 );
21979 });
21980
21981 cx.set_state(
21982 &r#"fn one() {
21983 let mut a = ˇtwo();
21984 }
21985 fn three() {}
21986 fn two() {}"#
21987 .unindent(),
21988 );
21989 cx.lsp
21990 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21991 Ok(Some(vec![
21992 lsp::Location {
21993 uri: params.text_document_position.text_document.uri.clone(),
21994 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21995 },
21996 lsp::Location {
21997 uri: params.text_document_position.text_document.uri,
21998 range: lsp::Range::new(lsp::Position::new(5, 4), lsp::Position::new(5, 7)),
21999 },
22000 ]))
22001 });
22002 let navigated = cx
22003 .update_editor(|editor, window, cx| {
22004 editor.find_all_references(&FindAllReferences, window, cx)
22005 })
22006 .unwrap()
22007 .await
22008 .expect("Failed to navigate to references");
22009 assert_eq!(
22010 navigated,
22011 Navigated::Yes,
22012 "Should have navigated to references from the FindAllReferences response"
22013 );
22014 cx.assert_editor_state(
22015 &r#"fn one() {
22016 let mut a = ˇtwo();
22017 }
22018 fn three() {}
22019 fn two() {}"#
22020 .unindent(),
22021 );
22022 let editors = cx.update_workspace(|workspace, _, cx| {
22023 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22024 });
22025 cx.update_editor(|_, _, _| {
22026 assert_eq!(
22027 editors.len(),
22028 3,
22029 "should have used a new multibuffer as offsets changed"
22030 );
22031 });
22032}
22033#[gpui::test]
22034async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
22035 init_test(cx, |_| {});
22036
22037 let language = Arc::new(Language::new(
22038 LanguageConfig::default(),
22039 Some(tree_sitter_rust::LANGUAGE.into()),
22040 ));
22041
22042 let text = r#"
22043 #[cfg(test)]
22044 mod tests() {
22045 #[test]
22046 fn runnable_1() {
22047 let a = 1;
22048 }
22049
22050 #[test]
22051 fn runnable_2() {
22052 let a = 1;
22053 let b = 2;
22054 }
22055 }
22056 "#
22057 .unindent();
22058
22059 let fs = FakeFs::new(cx.executor());
22060 fs.insert_file("/file.rs", Default::default()).await;
22061
22062 let project = Project::test(fs, ["/a".as_ref()], cx).await;
22063 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22064 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22065 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
22066 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
22067
22068 let editor = cx.new_window_entity(|window, cx| {
22069 Editor::new(
22070 EditorMode::full(),
22071 multi_buffer,
22072 Some(project.clone()),
22073 window,
22074 cx,
22075 )
22076 });
22077
22078 editor.update_in(cx, |editor, window, cx| {
22079 let snapshot = editor.buffer().read(cx).snapshot(cx);
22080 editor.tasks.insert(
22081 (buffer.read(cx).remote_id(), 3),
22082 RunnableTasks {
22083 templates: vec![],
22084 offset: snapshot.anchor_before(43),
22085 column: 0,
22086 extra_variables: HashMap::default(),
22087 context_range: BufferOffset(43)..BufferOffset(85),
22088 },
22089 );
22090 editor.tasks.insert(
22091 (buffer.read(cx).remote_id(), 8),
22092 RunnableTasks {
22093 templates: vec![],
22094 offset: snapshot.anchor_before(86),
22095 column: 0,
22096 extra_variables: HashMap::default(),
22097 context_range: BufferOffset(86)..BufferOffset(191),
22098 },
22099 );
22100
22101 // Test finding task when cursor is inside function body
22102 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22103 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
22104 });
22105 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
22106 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
22107
22108 // Test finding task when cursor is on function name
22109 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22110 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
22111 });
22112 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
22113 assert_eq!(row, 8, "Should find task when cursor is on function name");
22114 });
22115}
22116
22117#[gpui::test]
22118async fn test_folding_buffers(cx: &mut TestAppContext) {
22119 init_test(cx, |_| {});
22120
22121 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
22122 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
22123 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
22124
22125 let fs = FakeFs::new(cx.executor());
22126 fs.insert_tree(
22127 path!("/a"),
22128 json!({
22129 "first.rs": sample_text_1,
22130 "second.rs": sample_text_2,
22131 "third.rs": sample_text_3,
22132 }),
22133 )
22134 .await;
22135 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22136 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22137 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22138 let worktree = project.update(cx, |project, cx| {
22139 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22140 assert_eq!(worktrees.len(), 1);
22141 worktrees.pop().unwrap()
22142 });
22143 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22144
22145 let buffer_1 = project
22146 .update(cx, |project, cx| {
22147 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
22148 })
22149 .await
22150 .unwrap();
22151 let buffer_2 = project
22152 .update(cx, |project, cx| {
22153 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
22154 })
22155 .await
22156 .unwrap();
22157 let buffer_3 = project
22158 .update(cx, |project, cx| {
22159 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
22160 })
22161 .await
22162 .unwrap();
22163
22164 let multi_buffer = cx.new(|cx| {
22165 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22166 multi_buffer.push_excerpts(
22167 buffer_1.clone(),
22168 [
22169 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22170 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22171 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22172 ],
22173 cx,
22174 );
22175 multi_buffer.push_excerpts(
22176 buffer_2.clone(),
22177 [
22178 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22179 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22180 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22181 ],
22182 cx,
22183 );
22184 multi_buffer.push_excerpts(
22185 buffer_3.clone(),
22186 [
22187 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22188 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22189 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22190 ],
22191 cx,
22192 );
22193 multi_buffer
22194 });
22195 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22196 Editor::new(
22197 EditorMode::full(),
22198 multi_buffer.clone(),
22199 Some(project.clone()),
22200 window,
22201 cx,
22202 )
22203 });
22204
22205 assert_eq!(
22206 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22207 "\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",
22208 );
22209
22210 multi_buffer_editor.update(cx, |editor, cx| {
22211 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
22212 });
22213 assert_eq!(
22214 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22215 "\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",
22216 "After folding the first buffer, its text should not be displayed"
22217 );
22218
22219 multi_buffer_editor.update(cx, |editor, cx| {
22220 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
22221 });
22222 assert_eq!(
22223 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22224 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
22225 "After folding the second buffer, its text should not be displayed"
22226 );
22227
22228 multi_buffer_editor.update(cx, |editor, cx| {
22229 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
22230 });
22231 assert_eq!(
22232 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22233 "\n\n\n\n\n",
22234 "After folding the third buffer, its text should not be displayed"
22235 );
22236
22237 // Emulate selection inside the fold logic, that should work
22238 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22239 editor
22240 .snapshot(window, cx)
22241 .next_line_boundary(Point::new(0, 4));
22242 });
22243
22244 multi_buffer_editor.update(cx, |editor, cx| {
22245 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
22246 });
22247 assert_eq!(
22248 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22249 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
22250 "After unfolding the second buffer, its text should be displayed"
22251 );
22252
22253 // Typing inside of buffer 1 causes that buffer to be unfolded.
22254 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22255 assert_eq!(
22256 multi_buffer
22257 .read(cx)
22258 .snapshot(cx)
22259 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
22260 .collect::<String>(),
22261 "bbbb"
22262 );
22263 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
22264 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
22265 });
22266 editor.handle_input("B", window, cx);
22267 });
22268
22269 assert_eq!(
22270 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22271 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
22272 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
22273 );
22274
22275 multi_buffer_editor.update(cx, |editor, cx| {
22276 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
22277 });
22278 assert_eq!(
22279 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22280 "\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",
22281 "After unfolding the all buffers, all original text should be displayed"
22282 );
22283}
22284
22285#[gpui::test]
22286async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
22287 init_test(cx, |_| {});
22288
22289 let sample_text_1 = "1111\n2222\n3333".to_string();
22290 let sample_text_2 = "4444\n5555\n6666".to_string();
22291 let sample_text_3 = "7777\n8888\n9999".to_string();
22292
22293 let fs = FakeFs::new(cx.executor());
22294 fs.insert_tree(
22295 path!("/a"),
22296 json!({
22297 "first.rs": sample_text_1,
22298 "second.rs": sample_text_2,
22299 "third.rs": sample_text_3,
22300 }),
22301 )
22302 .await;
22303 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22304 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22305 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22306 let worktree = project.update(cx, |project, cx| {
22307 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22308 assert_eq!(worktrees.len(), 1);
22309 worktrees.pop().unwrap()
22310 });
22311 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22312
22313 let buffer_1 = project
22314 .update(cx, |project, cx| {
22315 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
22316 })
22317 .await
22318 .unwrap();
22319 let buffer_2 = project
22320 .update(cx, |project, cx| {
22321 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
22322 })
22323 .await
22324 .unwrap();
22325 let buffer_3 = project
22326 .update(cx, |project, cx| {
22327 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
22328 })
22329 .await
22330 .unwrap();
22331
22332 let multi_buffer = cx.new(|cx| {
22333 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22334 multi_buffer.push_excerpts(
22335 buffer_1.clone(),
22336 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22337 cx,
22338 );
22339 multi_buffer.push_excerpts(
22340 buffer_2.clone(),
22341 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22342 cx,
22343 );
22344 multi_buffer.push_excerpts(
22345 buffer_3.clone(),
22346 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22347 cx,
22348 );
22349 multi_buffer
22350 });
22351
22352 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22353 Editor::new(
22354 EditorMode::full(),
22355 multi_buffer,
22356 Some(project.clone()),
22357 window,
22358 cx,
22359 )
22360 });
22361
22362 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
22363 assert_eq!(
22364 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22365 full_text,
22366 );
22367
22368 multi_buffer_editor.update(cx, |editor, cx| {
22369 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
22370 });
22371 assert_eq!(
22372 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22373 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
22374 "After folding the first buffer, its text should not be displayed"
22375 );
22376
22377 multi_buffer_editor.update(cx, |editor, cx| {
22378 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
22379 });
22380
22381 assert_eq!(
22382 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22383 "\n\n\n\n\n\n7777\n8888\n9999",
22384 "After folding the second buffer, its text should not be displayed"
22385 );
22386
22387 multi_buffer_editor.update(cx, |editor, cx| {
22388 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
22389 });
22390 assert_eq!(
22391 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22392 "\n\n\n\n\n",
22393 "After folding the third buffer, its text should not be displayed"
22394 );
22395
22396 multi_buffer_editor.update(cx, |editor, cx| {
22397 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
22398 });
22399 assert_eq!(
22400 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22401 "\n\n\n\n4444\n5555\n6666\n\n",
22402 "After unfolding the second buffer, its text should be displayed"
22403 );
22404
22405 multi_buffer_editor.update(cx, |editor, cx| {
22406 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
22407 });
22408 assert_eq!(
22409 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22410 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
22411 "After unfolding the first buffer, its text should be displayed"
22412 );
22413
22414 multi_buffer_editor.update(cx, |editor, cx| {
22415 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
22416 });
22417 assert_eq!(
22418 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22419 full_text,
22420 "After unfolding all buffers, all original text should be displayed"
22421 );
22422}
22423
22424#[gpui::test]
22425async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
22426 init_test(cx, |_| {});
22427
22428 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
22429
22430 let fs = FakeFs::new(cx.executor());
22431 fs.insert_tree(
22432 path!("/a"),
22433 json!({
22434 "main.rs": sample_text,
22435 }),
22436 )
22437 .await;
22438 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22439 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22440 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22441 let worktree = project.update(cx, |project, cx| {
22442 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22443 assert_eq!(worktrees.len(), 1);
22444 worktrees.pop().unwrap()
22445 });
22446 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22447
22448 let buffer_1 = project
22449 .update(cx, |project, cx| {
22450 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22451 })
22452 .await
22453 .unwrap();
22454
22455 let multi_buffer = cx.new(|cx| {
22456 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22457 multi_buffer.push_excerpts(
22458 buffer_1.clone(),
22459 [ExcerptRange::new(
22460 Point::new(0, 0)
22461 ..Point::new(
22462 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
22463 0,
22464 ),
22465 )],
22466 cx,
22467 );
22468 multi_buffer
22469 });
22470 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22471 Editor::new(
22472 EditorMode::full(),
22473 multi_buffer,
22474 Some(project.clone()),
22475 window,
22476 cx,
22477 )
22478 });
22479
22480 let selection_range = Point::new(1, 0)..Point::new(2, 0);
22481 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22482 enum TestHighlight {}
22483 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
22484 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
22485 editor.highlight_text::<TestHighlight>(
22486 vec![highlight_range.clone()],
22487 HighlightStyle::color(Hsla::green()),
22488 cx,
22489 );
22490 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22491 s.select_ranges(Some(highlight_range))
22492 });
22493 });
22494
22495 let full_text = format!("\n\n{sample_text}");
22496 assert_eq!(
22497 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22498 full_text,
22499 );
22500}
22501
22502#[gpui::test]
22503async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
22504 init_test(cx, |_| {});
22505 cx.update(|cx| {
22506 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
22507 "keymaps/default-linux.json",
22508 cx,
22509 )
22510 .unwrap();
22511 cx.bind_keys(default_key_bindings);
22512 });
22513
22514 let (editor, cx) = cx.add_window_view(|window, cx| {
22515 let multi_buffer = MultiBuffer::build_multi(
22516 [
22517 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
22518 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
22519 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
22520 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
22521 ],
22522 cx,
22523 );
22524 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
22525
22526 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
22527 // fold all but the second buffer, so that we test navigating between two
22528 // adjacent folded buffers, as well as folded buffers at the start and
22529 // end the multibuffer
22530 editor.fold_buffer(buffer_ids[0], cx);
22531 editor.fold_buffer(buffer_ids[2], cx);
22532 editor.fold_buffer(buffer_ids[3], cx);
22533
22534 editor
22535 });
22536 cx.simulate_resize(size(px(1000.), px(1000.)));
22537
22538 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
22539 cx.assert_excerpts_with_selections(indoc! {"
22540 [EXCERPT]
22541 ˇ[FOLDED]
22542 [EXCERPT]
22543 a1
22544 b1
22545 [EXCERPT]
22546 [FOLDED]
22547 [EXCERPT]
22548 [FOLDED]
22549 "
22550 });
22551 cx.simulate_keystroke("down");
22552 cx.assert_excerpts_with_selections(indoc! {"
22553 [EXCERPT]
22554 [FOLDED]
22555 [EXCERPT]
22556 ˇa1
22557 b1
22558 [EXCERPT]
22559 [FOLDED]
22560 [EXCERPT]
22561 [FOLDED]
22562 "
22563 });
22564 cx.simulate_keystroke("down");
22565 cx.assert_excerpts_with_selections(indoc! {"
22566 [EXCERPT]
22567 [FOLDED]
22568 [EXCERPT]
22569 a1
22570 ˇb1
22571 [EXCERPT]
22572 [FOLDED]
22573 [EXCERPT]
22574 [FOLDED]
22575 "
22576 });
22577 cx.simulate_keystroke("down");
22578 cx.assert_excerpts_with_selections(indoc! {"
22579 [EXCERPT]
22580 [FOLDED]
22581 [EXCERPT]
22582 a1
22583 b1
22584 ˇ[EXCERPT]
22585 [FOLDED]
22586 [EXCERPT]
22587 [FOLDED]
22588 "
22589 });
22590 cx.simulate_keystroke("down");
22591 cx.assert_excerpts_with_selections(indoc! {"
22592 [EXCERPT]
22593 [FOLDED]
22594 [EXCERPT]
22595 a1
22596 b1
22597 [EXCERPT]
22598 ˇ[FOLDED]
22599 [EXCERPT]
22600 [FOLDED]
22601 "
22602 });
22603 for _ in 0..5 {
22604 cx.simulate_keystroke("down");
22605 cx.assert_excerpts_with_selections(indoc! {"
22606 [EXCERPT]
22607 [FOLDED]
22608 [EXCERPT]
22609 a1
22610 b1
22611 [EXCERPT]
22612 [FOLDED]
22613 [EXCERPT]
22614 ˇ[FOLDED]
22615 "
22616 });
22617 }
22618
22619 cx.simulate_keystroke("up");
22620 cx.assert_excerpts_with_selections(indoc! {"
22621 [EXCERPT]
22622 [FOLDED]
22623 [EXCERPT]
22624 a1
22625 b1
22626 [EXCERPT]
22627 ˇ[FOLDED]
22628 [EXCERPT]
22629 [FOLDED]
22630 "
22631 });
22632 cx.simulate_keystroke("up");
22633 cx.assert_excerpts_with_selections(indoc! {"
22634 [EXCERPT]
22635 [FOLDED]
22636 [EXCERPT]
22637 a1
22638 b1
22639 ˇ[EXCERPT]
22640 [FOLDED]
22641 [EXCERPT]
22642 [FOLDED]
22643 "
22644 });
22645 cx.simulate_keystroke("up");
22646 cx.assert_excerpts_with_selections(indoc! {"
22647 [EXCERPT]
22648 [FOLDED]
22649 [EXCERPT]
22650 a1
22651 ˇb1
22652 [EXCERPT]
22653 [FOLDED]
22654 [EXCERPT]
22655 [FOLDED]
22656 "
22657 });
22658 cx.simulate_keystroke("up");
22659 cx.assert_excerpts_with_selections(indoc! {"
22660 [EXCERPT]
22661 [FOLDED]
22662 [EXCERPT]
22663 ˇa1
22664 b1
22665 [EXCERPT]
22666 [FOLDED]
22667 [EXCERPT]
22668 [FOLDED]
22669 "
22670 });
22671 for _ in 0..5 {
22672 cx.simulate_keystroke("up");
22673 cx.assert_excerpts_with_selections(indoc! {"
22674 [EXCERPT]
22675 ˇ[FOLDED]
22676 [EXCERPT]
22677 a1
22678 b1
22679 [EXCERPT]
22680 [FOLDED]
22681 [EXCERPT]
22682 [FOLDED]
22683 "
22684 });
22685 }
22686}
22687
22688#[gpui::test]
22689async fn test_edit_prediction_text(cx: &mut TestAppContext) {
22690 init_test(cx, |_| {});
22691
22692 // Simple insertion
22693 assert_highlighted_edits(
22694 "Hello, world!",
22695 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
22696 true,
22697 cx,
22698 |highlighted_edits, cx| {
22699 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
22700 assert_eq!(highlighted_edits.highlights.len(), 1);
22701 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
22702 assert_eq!(
22703 highlighted_edits.highlights[0].1.background_color,
22704 Some(cx.theme().status().created_background)
22705 );
22706 },
22707 )
22708 .await;
22709
22710 // Replacement
22711 assert_highlighted_edits(
22712 "This is a test.",
22713 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
22714 false,
22715 cx,
22716 |highlighted_edits, cx| {
22717 assert_eq!(highlighted_edits.text, "That is a test.");
22718 assert_eq!(highlighted_edits.highlights.len(), 1);
22719 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
22720 assert_eq!(
22721 highlighted_edits.highlights[0].1.background_color,
22722 Some(cx.theme().status().created_background)
22723 );
22724 },
22725 )
22726 .await;
22727
22728 // Multiple edits
22729 assert_highlighted_edits(
22730 "Hello, world!",
22731 vec![
22732 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
22733 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
22734 ],
22735 false,
22736 cx,
22737 |highlighted_edits, cx| {
22738 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
22739 assert_eq!(highlighted_edits.highlights.len(), 2);
22740 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
22741 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
22742 assert_eq!(
22743 highlighted_edits.highlights[0].1.background_color,
22744 Some(cx.theme().status().created_background)
22745 );
22746 assert_eq!(
22747 highlighted_edits.highlights[1].1.background_color,
22748 Some(cx.theme().status().created_background)
22749 );
22750 },
22751 )
22752 .await;
22753
22754 // Multiple lines with edits
22755 assert_highlighted_edits(
22756 "First line\nSecond line\nThird line\nFourth line",
22757 vec![
22758 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
22759 (
22760 Point::new(2, 0)..Point::new(2, 10),
22761 "New third line".to_string(),
22762 ),
22763 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
22764 ],
22765 false,
22766 cx,
22767 |highlighted_edits, cx| {
22768 assert_eq!(
22769 highlighted_edits.text,
22770 "Second modified\nNew third line\nFourth updated line"
22771 );
22772 assert_eq!(highlighted_edits.highlights.len(), 3);
22773 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
22774 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
22775 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
22776 for highlight in &highlighted_edits.highlights {
22777 assert_eq!(
22778 highlight.1.background_color,
22779 Some(cx.theme().status().created_background)
22780 );
22781 }
22782 },
22783 )
22784 .await;
22785}
22786
22787#[gpui::test]
22788async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
22789 init_test(cx, |_| {});
22790
22791 // Deletion
22792 assert_highlighted_edits(
22793 "Hello, world!",
22794 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
22795 true,
22796 cx,
22797 |highlighted_edits, cx| {
22798 assert_eq!(highlighted_edits.text, "Hello, world!");
22799 assert_eq!(highlighted_edits.highlights.len(), 1);
22800 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
22801 assert_eq!(
22802 highlighted_edits.highlights[0].1.background_color,
22803 Some(cx.theme().status().deleted_background)
22804 );
22805 },
22806 )
22807 .await;
22808
22809 // Insertion
22810 assert_highlighted_edits(
22811 "Hello, world!",
22812 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
22813 true,
22814 cx,
22815 |highlighted_edits, cx| {
22816 assert_eq!(highlighted_edits.highlights.len(), 1);
22817 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
22818 assert_eq!(
22819 highlighted_edits.highlights[0].1.background_color,
22820 Some(cx.theme().status().created_background)
22821 );
22822 },
22823 )
22824 .await;
22825}
22826
22827async fn assert_highlighted_edits(
22828 text: &str,
22829 edits: Vec<(Range<Point>, String)>,
22830 include_deletions: bool,
22831 cx: &mut TestAppContext,
22832 assertion_fn: impl Fn(HighlightedText, &App),
22833) {
22834 let window = cx.add_window(|window, cx| {
22835 let buffer = MultiBuffer::build_simple(text, cx);
22836 Editor::new(EditorMode::full(), buffer, None, window, cx)
22837 });
22838 let cx = &mut VisualTestContext::from_window(*window, cx);
22839
22840 let (buffer, snapshot) = window
22841 .update(cx, |editor, _window, cx| {
22842 (
22843 editor.buffer().clone(),
22844 editor.buffer().read(cx).snapshot(cx),
22845 )
22846 })
22847 .unwrap();
22848
22849 let edits = edits
22850 .into_iter()
22851 .map(|(range, edit)| {
22852 (
22853 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
22854 edit,
22855 )
22856 })
22857 .collect::<Vec<_>>();
22858
22859 let text_anchor_edits = edits
22860 .clone()
22861 .into_iter()
22862 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit.into()))
22863 .collect::<Vec<_>>();
22864
22865 let edit_preview = window
22866 .update(cx, |_, _window, cx| {
22867 buffer
22868 .read(cx)
22869 .as_singleton()
22870 .unwrap()
22871 .read(cx)
22872 .preview_edits(text_anchor_edits.into(), cx)
22873 })
22874 .unwrap()
22875 .await;
22876
22877 cx.update(|_window, cx| {
22878 let highlighted_edits = edit_prediction_edit_text(
22879 snapshot.as_singleton().unwrap().2,
22880 &edits,
22881 &edit_preview,
22882 include_deletions,
22883 cx,
22884 );
22885 assertion_fn(highlighted_edits, cx)
22886 });
22887}
22888
22889#[track_caller]
22890fn assert_breakpoint(
22891 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
22892 path: &Arc<Path>,
22893 expected: Vec<(u32, Breakpoint)>,
22894) {
22895 if expected.is_empty() {
22896 assert!(!breakpoints.contains_key(path), "{}", path.display());
22897 } else {
22898 let mut breakpoint = breakpoints
22899 .get(path)
22900 .unwrap()
22901 .iter()
22902 .map(|breakpoint| {
22903 (
22904 breakpoint.row,
22905 Breakpoint {
22906 message: breakpoint.message.clone(),
22907 state: breakpoint.state,
22908 condition: breakpoint.condition.clone(),
22909 hit_condition: breakpoint.hit_condition.clone(),
22910 },
22911 )
22912 })
22913 .collect::<Vec<_>>();
22914
22915 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
22916
22917 assert_eq!(expected, breakpoint);
22918 }
22919}
22920
22921fn add_log_breakpoint_at_cursor(
22922 editor: &mut Editor,
22923 log_message: &str,
22924 window: &mut Window,
22925 cx: &mut Context<Editor>,
22926) {
22927 let (anchor, bp) = editor
22928 .breakpoints_at_cursors(window, cx)
22929 .first()
22930 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
22931 .unwrap_or_else(|| {
22932 let snapshot = editor.snapshot(window, cx);
22933 let cursor_position: Point =
22934 editor.selections.newest(&snapshot.display_snapshot).head();
22935
22936 let breakpoint_position = snapshot
22937 .buffer_snapshot()
22938 .anchor_before(Point::new(cursor_position.row, 0));
22939
22940 (breakpoint_position, Breakpoint::new_log(log_message))
22941 });
22942
22943 editor.edit_breakpoint_at_anchor(
22944 anchor,
22945 bp,
22946 BreakpointEditAction::EditLogMessage(log_message.into()),
22947 cx,
22948 );
22949}
22950
22951#[gpui::test]
22952async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
22953 init_test(cx, |_| {});
22954
22955 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22956 let fs = FakeFs::new(cx.executor());
22957 fs.insert_tree(
22958 path!("/a"),
22959 json!({
22960 "main.rs": sample_text,
22961 }),
22962 )
22963 .await;
22964 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22965 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22966 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22967
22968 let fs = FakeFs::new(cx.executor());
22969 fs.insert_tree(
22970 path!("/a"),
22971 json!({
22972 "main.rs": sample_text,
22973 }),
22974 )
22975 .await;
22976 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22977 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22978 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22979 let worktree_id = workspace
22980 .update(cx, |workspace, _window, cx| {
22981 workspace.project().update(cx, |project, cx| {
22982 project.worktrees(cx).next().unwrap().read(cx).id()
22983 })
22984 })
22985 .unwrap();
22986
22987 let buffer = project
22988 .update(cx, |project, cx| {
22989 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22990 })
22991 .await
22992 .unwrap();
22993
22994 let (editor, cx) = cx.add_window_view(|window, cx| {
22995 Editor::new(
22996 EditorMode::full(),
22997 MultiBuffer::build_from_buffer(buffer, cx),
22998 Some(project.clone()),
22999 window,
23000 cx,
23001 )
23002 });
23003
23004 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
23005 let abs_path = project.read_with(cx, |project, cx| {
23006 project
23007 .absolute_path(&project_path, cx)
23008 .map(Arc::from)
23009 .unwrap()
23010 });
23011
23012 // assert we can add breakpoint on the first line
23013 editor.update_in(cx, |editor, window, cx| {
23014 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23015 editor.move_to_end(&MoveToEnd, window, cx);
23016 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23017 });
23018
23019 let breakpoints = editor.update(cx, |editor, cx| {
23020 editor
23021 .breakpoint_store()
23022 .as_ref()
23023 .unwrap()
23024 .read(cx)
23025 .all_source_breakpoints(cx)
23026 });
23027
23028 assert_eq!(1, breakpoints.len());
23029 assert_breakpoint(
23030 &breakpoints,
23031 &abs_path,
23032 vec![
23033 (0, Breakpoint::new_standard()),
23034 (3, Breakpoint::new_standard()),
23035 ],
23036 );
23037
23038 editor.update_in(cx, |editor, window, cx| {
23039 editor.move_to_beginning(&MoveToBeginning, window, cx);
23040 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23041 });
23042
23043 let breakpoints = editor.update(cx, |editor, cx| {
23044 editor
23045 .breakpoint_store()
23046 .as_ref()
23047 .unwrap()
23048 .read(cx)
23049 .all_source_breakpoints(cx)
23050 });
23051
23052 assert_eq!(1, breakpoints.len());
23053 assert_breakpoint(
23054 &breakpoints,
23055 &abs_path,
23056 vec![(3, Breakpoint::new_standard())],
23057 );
23058
23059 editor.update_in(cx, |editor, window, cx| {
23060 editor.move_to_end(&MoveToEnd, window, cx);
23061 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23062 });
23063
23064 let breakpoints = editor.update(cx, |editor, cx| {
23065 editor
23066 .breakpoint_store()
23067 .as_ref()
23068 .unwrap()
23069 .read(cx)
23070 .all_source_breakpoints(cx)
23071 });
23072
23073 assert_eq!(0, breakpoints.len());
23074 assert_breakpoint(&breakpoints, &abs_path, vec![]);
23075}
23076
23077#[gpui::test]
23078async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
23079 init_test(cx, |_| {});
23080
23081 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
23082
23083 let fs = FakeFs::new(cx.executor());
23084 fs.insert_tree(
23085 path!("/a"),
23086 json!({
23087 "main.rs": sample_text,
23088 }),
23089 )
23090 .await;
23091 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23092 let (workspace, cx) =
23093 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23094
23095 let worktree_id = workspace.update(cx, |workspace, cx| {
23096 workspace.project().update(cx, |project, cx| {
23097 project.worktrees(cx).next().unwrap().read(cx).id()
23098 })
23099 });
23100
23101 let buffer = project
23102 .update(cx, |project, cx| {
23103 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23104 })
23105 .await
23106 .unwrap();
23107
23108 let (editor, cx) = cx.add_window_view(|window, cx| {
23109 Editor::new(
23110 EditorMode::full(),
23111 MultiBuffer::build_from_buffer(buffer, cx),
23112 Some(project.clone()),
23113 window,
23114 cx,
23115 )
23116 });
23117
23118 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
23119 let abs_path = project.read_with(cx, |project, cx| {
23120 project
23121 .absolute_path(&project_path, cx)
23122 .map(Arc::from)
23123 .unwrap()
23124 });
23125
23126 editor.update_in(cx, |editor, window, cx| {
23127 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
23128 });
23129
23130 let breakpoints = editor.update(cx, |editor, cx| {
23131 editor
23132 .breakpoint_store()
23133 .as_ref()
23134 .unwrap()
23135 .read(cx)
23136 .all_source_breakpoints(cx)
23137 });
23138
23139 assert_breakpoint(
23140 &breakpoints,
23141 &abs_path,
23142 vec![(0, Breakpoint::new_log("hello world"))],
23143 );
23144
23145 // Removing a log message from a log breakpoint should remove it
23146 editor.update_in(cx, |editor, window, cx| {
23147 add_log_breakpoint_at_cursor(editor, "", window, cx);
23148 });
23149
23150 let breakpoints = editor.update(cx, |editor, cx| {
23151 editor
23152 .breakpoint_store()
23153 .as_ref()
23154 .unwrap()
23155 .read(cx)
23156 .all_source_breakpoints(cx)
23157 });
23158
23159 assert_breakpoint(&breakpoints, &abs_path, vec![]);
23160
23161 editor.update_in(cx, |editor, window, cx| {
23162 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23163 editor.move_to_end(&MoveToEnd, window, cx);
23164 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23165 // Not adding a log message to a standard breakpoint shouldn't remove it
23166 add_log_breakpoint_at_cursor(editor, "", window, cx);
23167 });
23168
23169 let breakpoints = editor.update(cx, |editor, cx| {
23170 editor
23171 .breakpoint_store()
23172 .as_ref()
23173 .unwrap()
23174 .read(cx)
23175 .all_source_breakpoints(cx)
23176 });
23177
23178 assert_breakpoint(
23179 &breakpoints,
23180 &abs_path,
23181 vec![
23182 (0, Breakpoint::new_standard()),
23183 (3, Breakpoint::new_standard()),
23184 ],
23185 );
23186
23187 editor.update_in(cx, |editor, window, cx| {
23188 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
23189 });
23190
23191 let breakpoints = editor.update(cx, |editor, cx| {
23192 editor
23193 .breakpoint_store()
23194 .as_ref()
23195 .unwrap()
23196 .read(cx)
23197 .all_source_breakpoints(cx)
23198 });
23199
23200 assert_breakpoint(
23201 &breakpoints,
23202 &abs_path,
23203 vec![
23204 (0, Breakpoint::new_standard()),
23205 (3, Breakpoint::new_log("hello world")),
23206 ],
23207 );
23208
23209 editor.update_in(cx, |editor, window, cx| {
23210 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
23211 });
23212
23213 let breakpoints = editor.update(cx, |editor, cx| {
23214 editor
23215 .breakpoint_store()
23216 .as_ref()
23217 .unwrap()
23218 .read(cx)
23219 .all_source_breakpoints(cx)
23220 });
23221
23222 assert_breakpoint(
23223 &breakpoints,
23224 &abs_path,
23225 vec![
23226 (0, Breakpoint::new_standard()),
23227 (3, Breakpoint::new_log("hello Earth!!")),
23228 ],
23229 );
23230}
23231
23232/// This also tests that Editor::breakpoint_at_cursor_head is working properly
23233/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
23234/// or when breakpoints were placed out of order. This tests for a regression too
23235#[gpui::test]
23236async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
23237 init_test(cx, |_| {});
23238
23239 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
23240 let fs = FakeFs::new(cx.executor());
23241 fs.insert_tree(
23242 path!("/a"),
23243 json!({
23244 "main.rs": sample_text,
23245 }),
23246 )
23247 .await;
23248 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23249 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23250 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23251
23252 let fs = FakeFs::new(cx.executor());
23253 fs.insert_tree(
23254 path!("/a"),
23255 json!({
23256 "main.rs": sample_text,
23257 }),
23258 )
23259 .await;
23260 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23261 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23262 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23263 let worktree_id = workspace
23264 .update(cx, |workspace, _window, cx| {
23265 workspace.project().update(cx, |project, cx| {
23266 project.worktrees(cx).next().unwrap().read(cx).id()
23267 })
23268 })
23269 .unwrap();
23270
23271 let buffer = project
23272 .update(cx, |project, cx| {
23273 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23274 })
23275 .await
23276 .unwrap();
23277
23278 let (editor, cx) = cx.add_window_view(|window, cx| {
23279 Editor::new(
23280 EditorMode::full(),
23281 MultiBuffer::build_from_buffer(buffer, cx),
23282 Some(project.clone()),
23283 window,
23284 cx,
23285 )
23286 });
23287
23288 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
23289 let abs_path = project.read_with(cx, |project, cx| {
23290 project
23291 .absolute_path(&project_path, cx)
23292 .map(Arc::from)
23293 .unwrap()
23294 });
23295
23296 // assert we can add breakpoint on the first line
23297 editor.update_in(cx, |editor, window, cx| {
23298 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23299 editor.move_to_end(&MoveToEnd, window, cx);
23300 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23301 editor.move_up(&MoveUp, window, cx);
23302 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23303 });
23304
23305 let breakpoints = editor.update(cx, |editor, cx| {
23306 editor
23307 .breakpoint_store()
23308 .as_ref()
23309 .unwrap()
23310 .read(cx)
23311 .all_source_breakpoints(cx)
23312 });
23313
23314 assert_eq!(1, breakpoints.len());
23315 assert_breakpoint(
23316 &breakpoints,
23317 &abs_path,
23318 vec![
23319 (0, Breakpoint::new_standard()),
23320 (2, Breakpoint::new_standard()),
23321 (3, Breakpoint::new_standard()),
23322 ],
23323 );
23324
23325 editor.update_in(cx, |editor, window, cx| {
23326 editor.move_to_beginning(&MoveToBeginning, window, cx);
23327 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23328 editor.move_to_end(&MoveToEnd, window, cx);
23329 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23330 // Disabling a breakpoint that doesn't exist should do nothing
23331 editor.move_up(&MoveUp, window, cx);
23332 editor.move_up(&MoveUp, window, cx);
23333 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23334 });
23335
23336 let breakpoints = editor.update(cx, |editor, cx| {
23337 editor
23338 .breakpoint_store()
23339 .as_ref()
23340 .unwrap()
23341 .read(cx)
23342 .all_source_breakpoints(cx)
23343 });
23344
23345 let disable_breakpoint = {
23346 let mut bp = Breakpoint::new_standard();
23347 bp.state = BreakpointState::Disabled;
23348 bp
23349 };
23350
23351 assert_eq!(1, breakpoints.len());
23352 assert_breakpoint(
23353 &breakpoints,
23354 &abs_path,
23355 vec![
23356 (0, disable_breakpoint.clone()),
23357 (2, Breakpoint::new_standard()),
23358 (3, disable_breakpoint.clone()),
23359 ],
23360 );
23361
23362 editor.update_in(cx, |editor, window, cx| {
23363 editor.move_to_beginning(&MoveToBeginning, window, cx);
23364 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
23365 editor.move_to_end(&MoveToEnd, window, cx);
23366 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
23367 editor.move_up(&MoveUp, window, cx);
23368 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23369 });
23370
23371 let breakpoints = editor.update(cx, |editor, cx| {
23372 editor
23373 .breakpoint_store()
23374 .as_ref()
23375 .unwrap()
23376 .read(cx)
23377 .all_source_breakpoints(cx)
23378 });
23379
23380 assert_eq!(1, breakpoints.len());
23381 assert_breakpoint(
23382 &breakpoints,
23383 &abs_path,
23384 vec![
23385 (0, Breakpoint::new_standard()),
23386 (2, disable_breakpoint),
23387 (3, Breakpoint::new_standard()),
23388 ],
23389 );
23390}
23391
23392#[gpui::test]
23393async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
23394 init_test(cx, |_| {});
23395 let capabilities = lsp::ServerCapabilities {
23396 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
23397 prepare_provider: Some(true),
23398 work_done_progress_options: Default::default(),
23399 })),
23400 ..Default::default()
23401 };
23402 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23403
23404 cx.set_state(indoc! {"
23405 struct Fˇoo {}
23406 "});
23407
23408 cx.update_editor(|editor, _, cx| {
23409 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23410 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23411 editor.highlight_background::<DocumentHighlightRead>(
23412 &[highlight_range],
23413 |theme| theme.colors().editor_document_highlight_read_background,
23414 cx,
23415 );
23416 });
23417
23418 let mut prepare_rename_handler = cx
23419 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
23420 move |_, _, _| async move {
23421 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
23422 start: lsp::Position {
23423 line: 0,
23424 character: 7,
23425 },
23426 end: lsp::Position {
23427 line: 0,
23428 character: 10,
23429 },
23430 })))
23431 },
23432 );
23433 let prepare_rename_task = cx
23434 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23435 .expect("Prepare rename was not started");
23436 prepare_rename_handler.next().await.unwrap();
23437 prepare_rename_task.await.expect("Prepare rename failed");
23438
23439 let mut rename_handler =
23440 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23441 let edit = lsp::TextEdit {
23442 range: lsp::Range {
23443 start: lsp::Position {
23444 line: 0,
23445 character: 7,
23446 },
23447 end: lsp::Position {
23448 line: 0,
23449 character: 10,
23450 },
23451 },
23452 new_text: "FooRenamed".to_string(),
23453 };
23454 Ok(Some(lsp::WorkspaceEdit::new(
23455 // Specify the same edit twice
23456 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
23457 )))
23458 });
23459 let rename_task = cx
23460 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23461 .expect("Confirm rename was not started");
23462 rename_handler.next().await.unwrap();
23463 rename_task.await.expect("Confirm rename failed");
23464 cx.run_until_parked();
23465
23466 // Despite two edits, only one is actually applied as those are identical
23467 cx.assert_editor_state(indoc! {"
23468 struct FooRenamedˇ {}
23469 "});
23470}
23471
23472#[gpui::test]
23473async fn test_rename_without_prepare(cx: &mut TestAppContext) {
23474 init_test(cx, |_| {});
23475 // These capabilities indicate that the server does not support prepare rename.
23476 let capabilities = lsp::ServerCapabilities {
23477 rename_provider: Some(lsp::OneOf::Left(true)),
23478 ..Default::default()
23479 };
23480 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23481
23482 cx.set_state(indoc! {"
23483 struct Fˇoo {}
23484 "});
23485
23486 cx.update_editor(|editor, _window, cx| {
23487 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23488 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23489 editor.highlight_background::<DocumentHighlightRead>(
23490 &[highlight_range],
23491 |theme| theme.colors().editor_document_highlight_read_background,
23492 cx,
23493 );
23494 });
23495
23496 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23497 .expect("Prepare rename was not started")
23498 .await
23499 .expect("Prepare rename failed");
23500
23501 let mut rename_handler =
23502 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23503 let edit = lsp::TextEdit {
23504 range: lsp::Range {
23505 start: lsp::Position {
23506 line: 0,
23507 character: 7,
23508 },
23509 end: lsp::Position {
23510 line: 0,
23511 character: 10,
23512 },
23513 },
23514 new_text: "FooRenamed".to_string(),
23515 };
23516 Ok(Some(lsp::WorkspaceEdit::new(
23517 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
23518 )))
23519 });
23520 let rename_task = cx
23521 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23522 .expect("Confirm rename was not started");
23523 rename_handler.next().await.unwrap();
23524 rename_task.await.expect("Confirm rename failed");
23525 cx.run_until_parked();
23526
23527 // Correct range is renamed, as `surrounding_word` is used to find it.
23528 cx.assert_editor_state(indoc! {"
23529 struct FooRenamedˇ {}
23530 "});
23531}
23532
23533#[gpui::test]
23534async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
23535 init_test(cx, |_| {});
23536 let mut cx = EditorTestContext::new(cx).await;
23537
23538 let language = Arc::new(
23539 Language::new(
23540 LanguageConfig::default(),
23541 Some(tree_sitter_html::LANGUAGE.into()),
23542 )
23543 .with_brackets_query(
23544 r#"
23545 ("<" @open "/>" @close)
23546 ("</" @open ">" @close)
23547 ("<" @open ">" @close)
23548 ("\"" @open "\"" @close)
23549 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
23550 "#,
23551 )
23552 .unwrap(),
23553 );
23554 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23555
23556 cx.set_state(indoc! {"
23557 <span>ˇ</span>
23558 "});
23559 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23560 cx.assert_editor_state(indoc! {"
23561 <span>
23562 ˇ
23563 </span>
23564 "});
23565
23566 cx.set_state(indoc! {"
23567 <span><span></span>ˇ</span>
23568 "});
23569 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23570 cx.assert_editor_state(indoc! {"
23571 <span><span></span>
23572 ˇ</span>
23573 "});
23574
23575 cx.set_state(indoc! {"
23576 <span>ˇ
23577 </span>
23578 "});
23579 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23580 cx.assert_editor_state(indoc! {"
23581 <span>
23582 ˇ
23583 </span>
23584 "});
23585}
23586
23587#[gpui::test(iterations = 10)]
23588async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
23589 init_test(cx, |_| {});
23590
23591 let fs = FakeFs::new(cx.executor());
23592 fs.insert_tree(
23593 path!("/dir"),
23594 json!({
23595 "a.ts": "a",
23596 }),
23597 )
23598 .await;
23599
23600 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
23601 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23602 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23603
23604 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23605 language_registry.add(Arc::new(Language::new(
23606 LanguageConfig {
23607 name: "TypeScript".into(),
23608 matcher: LanguageMatcher {
23609 path_suffixes: vec!["ts".to_string()],
23610 ..Default::default()
23611 },
23612 ..Default::default()
23613 },
23614 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
23615 )));
23616 let mut fake_language_servers = language_registry.register_fake_lsp(
23617 "TypeScript",
23618 FakeLspAdapter {
23619 capabilities: lsp::ServerCapabilities {
23620 code_lens_provider: Some(lsp::CodeLensOptions {
23621 resolve_provider: Some(true),
23622 }),
23623 execute_command_provider: Some(lsp::ExecuteCommandOptions {
23624 commands: vec!["_the/command".to_string()],
23625 ..lsp::ExecuteCommandOptions::default()
23626 }),
23627 ..lsp::ServerCapabilities::default()
23628 },
23629 ..FakeLspAdapter::default()
23630 },
23631 );
23632
23633 let editor = workspace
23634 .update(cx, |workspace, window, cx| {
23635 workspace.open_abs_path(
23636 PathBuf::from(path!("/dir/a.ts")),
23637 OpenOptions::default(),
23638 window,
23639 cx,
23640 )
23641 })
23642 .unwrap()
23643 .await
23644 .unwrap()
23645 .downcast::<Editor>()
23646 .unwrap();
23647 cx.executor().run_until_parked();
23648
23649 let fake_server = fake_language_servers.next().await.unwrap();
23650
23651 let buffer = editor.update(cx, |editor, cx| {
23652 editor
23653 .buffer()
23654 .read(cx)
23655 .as_singleton()
23656 .expect("have opened a single file by path")
23657 });
23658
23659 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
23660 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
23661 drop(buffer_snapshot);
23662 let actions = cx
23663 .update_window(*workspace, |_, window, cx| {
23664 project.code_actions(&buffer, anchor..anchor, window, cx)
23665 })
23666 .unwrap();
23667
23668 fake_server
23669 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23670 Ok(Some(vec![
23671 lsp::CodeLens {
23672 range: lsp::Range::default(),
23673 command: Some(lsp::Command {
23674 title: "Code lens command".to_owned(),
23675 command: "_the/command".to_owned(),
23676 arguments: None,
23677 }),
23678 data: None,
23679 },
23680 lsp::CodeLens {
23681 range: lsp::Range::default(),
23682 command: Some(lsp::Command {
23683 title: "Command not in capabilities".to_owned(),
23684 command: "not in capabilities".to_owned(),
23685 arguments: None,
23686 }),
23687 data: None,
23688 },
23689 lsp::CodeLens {
23690 range: lsp::Range {
23691 start: lsp::Position {
23692 line: 1,
23693 character: 1,
23694 },
23695 end: lsp::Position {
23696 line: 1,
23697 character: 1,
23698 },
23699 },
23700 command: Some(lsp::Command {
23701 title: "Command not in range".to_owned(),
23702 command: "_the/command".to_owned(),
23703 arguments: None,
23704 }),
23705 data: None,
23706 },
23707 ]))
23708 })
23709 .next()
23710 .await;
23711
23712 let actions = actions.await.unwrap();
23713 assert_eq!(
23714 actions.len(),
23715 1,
23716 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
23717 );
23718 let action = actions[0].clone();
23719 let apply = project.update(cx, |project, cx| {
23720 project.apply_code_action(buffer.clone(), action, true, cx)
23721 });
23722
23723 // Resolving the code action does not populate its edits. In absence of
23724 // edits, we must execute the given command.
23725 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
23726 |mut lens, _| async move {
23727 let lens_command = lens.command.as_mut().expect("should have a command");
23728 assert_eq!(lens_command.title, "Code lens command");
23729 lens_command.arguments = Some(vec![json!("the-argument")]);
23730 Ok(lens)
23731 },
23732 );
23733
23734 // While executing the command, the language server sends the editor
23735 // a `workspaceEdit` request.
23736 fake_server
23737 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
23738 let fake = fake_server.clone();
23739 move |params, _| {
23740 assert_eq!(params.command, "_the/command");
23741 let fake = fake.clone();
23742 async move {
23743 fake.server
23744 .request::<lsp::request::ApplyWorkspaceEdit>(
23745 lsp::ApplyWorkspaceEditParams {
23746 label: None,
23747 edit: lsp::WorkspaceEdit {
23748 changes: Some(
23749 [(
23750 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
23751 vec![lsp::TextEdit {
23752 range: lsp::Range::new(
23753 lsp::Position::new(0, 0),
23754 lsp::Position::new(0, 0),
23755 ),
23756 new_text: "X".into(),
23757 }],
23758 )]
23759 .into_iter()
23760 .collect(),
23761 ),
23762 ..lsp::WorkspaceEdit::default()
23763 },
23764 },
23765 )
23766 .await
23767 .into_response()
23768 .unwrap();
23769 Ok(Some(json!(null)))
23770 }
23771 }
23772 })
23773 .next()
23774 .await;
23775
23776 // Applying the code lens command returns a project transaction containing the edits
23777 // sent by the language server in its `workspaceEdit` request.
23778 let transaction = apply.await.unwrap();
23779 assert!(transaction.0.contains_key(&buffer));
23780 buffer.update(cx, |buffer, cx| {
23781 assert_eq!(buffer.text(), "Xa");
23782 buffer.undo(cx);
23783 assert_eq!(buffer.text(), "a");
23784 });
23785
23786 let actions_after_edits = cx
23787 .update_window(*workspace, |_, window, cx| {
23788 project.code_actions(&buffer, anchor..anchor, window, cx)
23789 })
23790 .unwrap()
23791 .await
23792 .unwrap();
23793 assert_eq!(
23794 actions, actions_after_edits,
23795 "For the same selection, same code lens actions should be returned"
23796 );
23797
23798 let _responses =
23799 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23800 panic!("No more code lens requests are expected");
23801 });
23802 editor.update_in(cx, |editor, window, cx| {
23803 editor.select_all(&SelectAll, window, cx);
23804 });
23805 cx.executor().run_until_parked();
23806 let new_actions = cx
23807 .update_window(*workspace, |_, window, cx| {
23808 project.code_actions(&buffer, anchor..anchor, window, cx)
23809 })
23810 .unwrap()
23811 .await
23812 .unwrap();
23813 assert_eq!(
23814 actions, new_actions,
23815 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
23816 );
23817}
23818
23819#[gpui::test]
23820async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
23821 init_test(cx, |_| {});
23822
23823 let fs = FakeFs::new(cx.executor());
23824 let main_text = r#"fn main() {
23825println!("1");
23826println!("2");
23827println!("3");
23828println!("4");
23829println!("5");
23830}"#;
23831 let lib_text = "mod foo {}";
23832 fs.insert_tree(
23833 path!("/a"),
23834 json!({
23835 "lib.rs": lib_text,
23836 "main.rs": main_text,
23837 }),
23838 )
23839 .await;
23840
23841 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23842 let (workspace, cx) =
23843 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23844 let worktree_id = workspace.update(cx, |workspace, cx| {
23845 workspace.project().update(cx, |project, cx| {
23846 project.worktrees(cx).next().unwrap().read(cx).id()
23847 })
23848 });
23849
23850 let expected_ranges = vec![
23851 Point::new(0, 0)..Point::new(0, 0),
23852 Point::new(1, 0)..Point::new(1, 1),
23853 Point::new(2, 0)..Point::new(2, 2),
23854 Point::new(3, 0)..Point::new(3, 3),
23855 ];
23856
23857 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23858 let editor_1 = workspace
23859 .update_in(cx, |workspace, window, cx| {
23860 workspace.open_path(
23861 (worktree_id, rel_path("main.rs")),
23862 Some(pane_1.downgrade()),
23863 true,
23864 window,
23865 cx,
23866 )
23867 })
23868 .unwrap()
23869 .await
23870 .downcast::<Editor>()
23871 .unwrap();
23872 pane_1.update(cx, |pane, cx| {
23873 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23874 open_editor.update(cx, |editor, cx| {
23875 assert_eq!(
23876 editor.display_text(cx),
23877 main_text,
23878 "Original main.rs text on initial open",
23879 );
23880 assert_eq!(
23881 editor
23882 .selections
23883 .all::<Point>(&editor.display_snapshot(cx))
23884 .into_iter()
23885 .map(|s| s.range())
23886 .collect::<Vec<_>>(),
23887 vec![Point::zero()..Point::zero()],
23888 "Default selections on initial open",
23889 );
23890 })
23891 });
23892 editor_1.update_in(cx, |editor, window, cx| {
23893 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23894 s.select_ranges(expected_ranges.clone());
23895 });
23896 });
23897
23898 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
23899 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
23900 });
23901 let editor_2 = workspace
23902 .update_in(cx, |workspace, window, cx| {
23903 workspace.open_path(
23904 (worktree_id, rel_path("main.rs")),
23905 Some(pane_2.downgrade()),
23906 true,
23907 window,
23908 cx,
23909 )
23910 })
23911 .unwrap()
23912 .await
23913 .downcast::<Editor>()
23914 .unwrap();
23915 pane_2.update(cx, |pane, cx| {
23916 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23917 open_editor.update(cx, |editor, cx| {
23918 assert_eq!(
23919 editor.display_text(cx),
23920 main_text,
23921 "Original main.rs text on initial open in another panel",
23922 );
23923 assert_eq!(
23924 editor
23925 .selections
23926 .all::<Point>(&editor.display_snapshot(cx))
23927 .into_iter()
23928 .map(|s| s.range())
23929 .collect::<Vec<_>>(),
23930 vec![Point::zero()..Point::zero()],
23931 "Default selections on initial open in another panel",
23932 );
23933 })
23934 });
23935
23936 editor_2.update_in(cx, |editor, window, cx| {
23937 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
23938 });
23939
23940 let _other_editor_1 = workspace
23941 .update_in(cx, |workspace, window, cx| {
23942 workspace.open_path(
23943 (worktree_id, rel_path("lib.rs")),
23944 Some(pane_1.downgrade()),
23945 true,
23946 window,
23947 cx,
23948 )
23949 })
23950 .unwrap()
23951 .await
23952 .downcast::<Editor>()
23953 .unwrap();
23954 pane_1
23955 .update_in(cx, |pane, window, cx| {
23956 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23957 })
23958 .await
23959 .unwrap();
23960 drop(editor_1);
23961 pane_1.update(cx, |pane, cx| {
23962 pane.active_item()
23963 .unwrap()
23964 .downcast::<Editor>()
23965 .unwrap()
23966 .update(cx, |editor, cx| {
23967 assert_eq!(
23968 editor.display_text(cx),
23969 lib_text,
23970 "Other file should be open and active",
23971 );
23972 });
23973 assert_eq!(pane.items().count(), 1, "No other editors should be open");
23974 });
23975
23976 let _other_editor_2 = workspace
23977 .update_in(cx, |workspace, window, cx| {
23978 workspace.open_path(
23979 (worktree_id, rel_path("lib.rs")),
23980 Some(pane_2.downgrade()),
23981 true,
23982 window,
23983 cx,
23984 )
23985 })
23986 .unwrap()
23987 .await
23988 .downcast::<Editor>()
23989 .unwrap();
23990 pane_2
23991 .update_in(cx, |pane, window, cx| {
23992 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23993 })
23994 .await
23995 .unwrap();
23996 drop(editor_2);
23997 pane_2.update(cx, |pane, cx| {
23998 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23999 open_editor.update(cx, |editor, cx| {
24000 assert_eq!(
24001 editor.display_text(cx),
24002 lib_text,
24003 "Other file should be open and active in another panel too",
24004 );
24005 });
24006 assert_eq!(
24007 pane.items().count(),
24008 1,
24009 "No other editors should be open in another pane",
24010 );
24011 });
24012
24013 let _editor_1_reopened = workspace
24014 .update_in(cx, |workspace, window, cx| {
24015 workspace.open_path(
24016 (worktree_id, rel_path("main.rs")),
24017 Some(pane_1.downgrade()),
24018 true,
24019 window,
24020 cx,
24021 )
24022 })
24023 .unwrap()
24024 .await
24025 .downcast::<Editor>()
24026 .unwrap();
24027 let _editor_2_reopened = workspace
24028 .update_in(cx, |workspace, window, cx| {
24029 workspace.open_path(
24030 (worktree_id, rel_path("main.rs")),
24031 Some(pane_2.downgrade()),
24032 true,
24033 window,
24034 cx,
24035 )
24036 })
24037 .unwrap()
24038 .await
24039 .downcast::<Editor>()
24040 .unwrap();
24041 pane_1.update(cx, |pane, cx| {
24042 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24043 open_editor.update(cx, |editor, cx| {
24044 assert_eq!(
24045 editor.display_text(cx),
24046 main_text,
24047 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
24048 );
24049 assert_eq!(
24050 editor
24051 .selections
24052 .all::<Point>(&editor.display_snapshot(cx))
24053 .into_iter()
24054 .map(|s| s.range())
24055 .collect::<Vec<_>>(),
24056 expected_ranges,
24057 "Previous editor in the 1st panel had selections and should get them restored on reopen",
24058 );
24059 })
24060 });
24061 pane_2.update(cx, |pane, cx| {
24062 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24063 open_editor.update(cx, |editor, cx| {
24064 assert_eq!(
24065 editor.display_text(cx),
24066 r#"fn main() {
24067⋯rintln!("1");
24068⋯intln!("2");
24069⋯ntln!("3");
24070println!("4");
24071println!("5");
24072}"#,
24073 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
24074 );
24075 assert_eq!(
24076 editor
24077 .selections
24078 .all::<Point>(&editor.display_snapshot(cx))
24079 .into_iter()
24080 .map(|s| s.range())
24081 .collect::<Vec<_>>(),
24082 vec![Point::zero()..Point::zero()],
24083 "Previous editor in the 2nd pane had no selections changed hence should restore none",
24084 );
24085 })
24086 });
24087}
24088
24089#[gpui::test]
24090async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
24091 init_test(cx, |_| {});
24092
24093 let fs = FakeFs::new(cx.executor());
24094 let main_text = r#"fn main() {
24095println!("1");
24096println!("2");
24097println!("3");
24098println!("4");
24099println!("5");
24100}"#;
24101 let lib_text = "mod foo {}";
24102 fs.insert_tree(
24103 path!("/a"),
24104 json!({
24105 "lib.rs": lib_text,
24106 "main.rs": main_text,
24107 }),
24108 )
24109 .await;
24110
24111 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24112 let (workspace, cx) =
24113 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24114 let worktree_id = workspace.update(cx, |workspace, cx| {
24115 workspace.project().update(cx, |project, cx| {
24116 project.worktrees(cx).next().unwrap().read(cx).id()
24117 })
24118 });
24119
24120 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24121 let editor = workspace
24122 .update_in(cx, |workspace, window, cx| {
24123 workspace.open_path(
24124 (worktree_id, rel_path("main.rs")),
24125 Some(pane.downgrade()),
24126 true,
24127 window,
24128 cx,
24129 )
24130 })
24131 .unwrap()
24132 .await
24133 .downcast::<Editor>()
24134 .unwrap();
24135 pane.update(cx, |pane, cx| {
24136 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24137 open_editor.update(cx, |editor, cx| {
24138 assert_eq!(
24139 editor.display_text(cx),
24140 main_text,
24141 "Original main.rs text on initial open",
24142 );
24143 })
24144 });
24145 editor.update_in(cx, |editor, window, cx| {
24146 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
24147 });
24148
24149 cx.update_global(|store: &mut SettingsStore, cx| {
24150 store.update_user_settings(cx, |s| {
24151 s.workspace.restore_on_file_reopen = Some(false);
24152 });
24153 });
24154 editor.update_in(cx, |editor, window, cx| {
24155 editor.fold_ranges(
24156 vec![
24157 Point::new(1, 0)..Point::new(1, 1),
24158 Point::new(2, 0)..Point::new(2, 2),
24159 Point::new(3, 0)..Point::new(3, 3),
24160 ],
24161 false,
24162 window,
24163 cx,
24164 );
24165 });
24166 pane.update_in(cx, |pane, window, cx| {
24167 pane.close_all_items(&CloseAllItems::default(), window, cx)
24168 })
24169 .await
24170 .unwrap();
24171 pane.update(cx, |pane, _| {
24172 assert!(pane.active_item().is_none());
24173 });
24174 cx.update_global(|store: &mut SettingsStore, cx| {
24175 store.update_user_settings(cx, |s| {
24176 s.workspace.restore_on_file_reopen = Some(true);
24177 });
24178 });
24179
24180 let _editor_reopened = workspace
24181 .update_in(cx, |workspace, window, cx| {
24182 workspace.open_path(
24183 (worktree_id, rel_path("main.rs")),
24184 Some(pane.downgrade()),
24185 true,
24186 window,
24187 cx,
24188 )
24189 })
24190 .unwrap()
24191 .await
24192 .downcast::<Editor>()
24193 .unwrap();
24194 pane.update(cx, |pane, cx| {
24195 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24196 open_editor.update(cx, |editor, cx| {
24197 assert_eq!(
24198 editor.display_text(cx),
24199 main_text,
24200 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
24201 );
24202 })
24203 });
24204}
24205
24206#[gpui::test]
24207async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
24208 struct EmptyModalView {
24209 focus_handle: gpui::FocusHandle,
24210 }
24211 impl EventEmitter<DismissEvent> for EmptyModalView {}
24212 impl Render for EmptyModalView {
24213 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
24214 div()
24215 }
24216 }
24217 impl Focusable for EmptyModalView {
24218 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
24219 self.focus_handle.clone()
24220 }
24221 }
24222 impl workspace::ModalView for EmptyModalView {}
24223 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
24224 EmptyModalView {
24225 focus_handle: cx.focus_handle(),
24226 }
24227 }
24228
24229 init_test(cx, |_| {});
24230
24231 let fs = FakeFs::new(cx.executor());
24232 let project = Project::test(fs, [], cx).await;
24233 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24234 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
24235 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24236 let editor = cx.new_window_entity(|window, cx| {
24237 Editor::new(
24238 EditorMode::full(),
24239 buffer,
24240 Some(project.clone()),
24241 window,
24242 cx,
24243 )
24244 });
24245 workspace
24246 .update(cx, |workspace, window, cx| {
24247 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
24248 })
24249 .unwrap();
24250 editor.update_in(cx, |editor, window, cx| {
24251 editor.open_context_menu(&OpenContextMenu, window, cx);
24252 assert!(editor.mouse_context_menu.is_some());
24253 });
24254 workspace
24255 .update(cx, |workspace, window, cx| {
24256 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
24257 })
24258 .unwrap();
24259 cx.read(|cx| {
24260 assert!(editor.read(cx).mouse_context_menu.is_none());
24261 });
24262}
24263
24264fn set_linked_edit_ranges(
24265 opening: (Point, Point),
24266 closing: (Point, Point),
24267 editor: &mut Editor,
24268 cx: &mut Context<Editor>,
24269) {
24270 let Some((buffer, _)) = editor
24271 .buffer
24272 .read(cx)
24273 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
24274 else {
24275 panic!("Failed to get buffer for selection position");
24276 };
24277 let buffer = buffer.read(cx);
24278 let buffer_id = buffer.remote_id();
24279 let opening_range = buffer.anchor_before(opening.0)..buffer.anchor_after(opening.1);
24280 let closing_range = buffer.anchor_before(closing.0)..buffer.anchor_after(closing.1);
24281 let mut linked_ranges = HashMap::default();
24282 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
24283 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
24284}
24285
24286#[gpui::test]
24287async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
24288 init_test(cx, |_| {});
24289
24290 let fs = FakeFs::new(cx.executor());
24291 fs.insert_file(path!("/file.html"), Default::default())
24292 .await;
24293
24294 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
24295
24296 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24297 let html_language = Arc::new(Language::new(
24298 LanguageConfig {
24299 name: "HTML".into(),
24300 matcher: LanguageMatcher {
24301 path_suffixes: vec!["html".to_string()],
24302 ..LanguageMatcher::default()
24303 },
24304 brackets: BracketPairConfig {
24305 pairs: vec![BracketPair {
24306 start: "<".into(),
24307 end: ">".into(),
24308 close: true,
24309 ..Default::default()
24310 }],
24311 ..Default::default()
24312 },
24313 ..Default::default()
24314 },
24315 Some(tree_sitter_html::LANGUAGE.into()),
24316 ));
24317 language_registry.add(html_language);
24318 let mut fake_servers = language_registry.register_fake_lsp(
24319 "HTML",
24320 FakeLspAdapter {
24321 capabilities: lsp::ServerCapabilities {
24322 completion_provider: Some(lsp::CompletionOptions {
24323 resolve_provider: Some(true),
24324 ..Default::default()
24325 }),
24326 ..Default::default()
24327 },
24328 ..Default::default()
24329 },
24330 );
24331
24332 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24333 let cx = &mut VisualTestContext::from_window(*workspace, cx);
24334
24335 let worktree_id = workspace
24336 .update(cx, |workspace, _window, cx| {
24337 workspace.project().update(cx, |project, cx| {
24338 project.worktrees(cx).next().unwrap().read(cx).id()
24339 })
24340 })
24341 .unwrap();
24342 project
24343 .update(cx, |project, cx| {
24344 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
24345 })
24346 .await
24347 .unwrap();
24348 let editor = workspace
24349 .update(cx, |workspace, window, cx| {
24350 workspace.open_path((worktree_id, rel_path("file.html")), None, true, window, cx)
24351 })
24352 .unwrap()
24353 .await
24354 .unwrap()
24355 .downcast::<Editor>()
24356 .unwrap();
24357
24358 let fake_server = fake_servers.next().await.unwrap();
24359 editor.update_in(cx, |editor, window, cx| {
24360 editor.set_text("<ad></ad>", window, cx);
24361 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
24362 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
24363 });
24364 set_linked_edit_ranges(
24365 (Point::new(0, 1), Point::new(0, 3)),
24366 (Point::new(0, 6), Point::new(0, 8)),
24367 editor,
24368 cx,
24369 );
24370 });
24371 let mut completion_handle =
24372 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
24373 Ok(Some(lsp::CompletionResponse::Array(vec![
24374 lsp::CompletionItem {
24375 label: "head".to_string(),
24376 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
24377 lsp::InsertReplaceEdit {
24378 new_text: "head".to_string(),
24379 insert: lsp::Range::new(
24380 lsp::Position::new(0, 1),
24381 lsp::Position::new(0, 3),
24382 ),
24383 replace: lsp::Range::new(
24384 lsp::Position::new(0, 1),
24385 lsp::Position::new(0, 3),
24386 ),
24387 },
24388 )),
24389 ..Default::default()
24390 },
24391 ])))
24392 });
24393 editor.update_in(cx, |editor, window, cx| {
24394 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
24395 });
24396 cx.run_until_parked();
24397 completion_handle.next().await.unwrap();
24398 editor.update(cx, |editor, _| {
24399 assert!(
24400 editor.context_menu_visible(),
24401 "Completion menu should be visible"
24402 );
24403 });
24404 editor.update_in(cx, |editor, window, cx| {
24405 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
24406 });
24407 cx.executor().run_until_parked();
24408 editor.update(cx, |editor, cx| {
24409 assert_eq!(editor.text(cx), "<head></head>");
24410 });
24411}
24412
24413#[gpui::test]
24414async fn test_linked_edits_on_typing_punctuation(cx: &mut TestAppContext) {
24415 init_test(cx, |_| {});
24416
24417 let mut cx = EditorTestContext::new(cx).await;
24418 let language = Arc::new(Language::new(
24419 LanguageConfig {
24420 name: "TSX".into(),
24421 matcher: LanguageMatcher {
24422 path_suffixes: vec!["tsx".to_string()],
24423 ..LanguageMatcher::default()
24424 },
24425 brackets: BracketPairConfig {
24426 pairs: vec![BracketPair {
24427 start: "<".into(),
24428 end: ">".into(),
24429 close: true,
24430 ..Default::default()
24431 }],
24432 ..Default::default()
24433 },
24434 linked_edit_characters: HashSet::from_iter(['.']),
24435 ..Default::default()
24436 },
24437 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
24438 ));
24439 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24440
24441 // Test typing > does not extend linked pair
24442 cx.set_state("<divˇ<div></div>");
24443 cx.update_editor(|editor, _, cx| {
24444 set_linked_edit_ranges(
24445 (Point::new(0, 1), Point::new(0, 4)),
24446 (Point::new(0, 11), Point::new(0, 14)),
24447 editor,
24448 cx,
24449 );
24450 });
24451 cx.update_editor(|editor, window, cx| {
24452 editor.handle_input(">", window, cx);
24453 });
24454 cx.assert_editor_state("<div>ˇ<div></div>");
24455
24456 // Test typing . do extend linked pair
24457 cx.set_state("<Animatedˇ></Animated>");
24458 cx.update_editor(|editor, _, cx| {
24459 set_linked_edit_ranges(
24460 (Point::new(0, 1), Point::new(0, 9)),
24461 (Point::new(0, 12), Point::new(0, 20)),
24462 editor,
24463 cx,
24464 );
24465 });
24466 cx.update_editor(|editor, window, cx| {
24467 editor.handle_input(".", window, cx);
24468 });
24469 cx.assert_editor_state("<Animated.ˇ></Animated.>");
24470 cx.update_editor(|editor, _, cx| {
24471 set_linked_edit_ranges(
24472 (Point::new(0, 1), Point::new(0, 10)),
24473 (Point::new(0, 13), Point::new(0, 21)),
24474 editor,
24475 cx,
24476 );
24477 });
24478 cx.update_editor(|editor, window, cx| {
24479 editor.handle_input("V", window, cx);
24480 });
24481 cx.assert_editor_state("<Animated.Vˇ></Animated.V>");
24482}
24483
24484#[gpui::test]
24485async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
24486 init_test(cx, |_| {});
24487
24488 let fs = FakeFs::new(cx.executor());
24489 fs.insert_tree(
24490 path!("/root"),
24491 json!({
24492 "a": {
24493 "main.rs": "fn main() {}",
24494 },
24495 "foo": {
24496 "bar": {
24497 "external_file.rs": "pub mod external {}",
24498 }
24499 }
24500 }),
24501 )
24502 .await;
24503
24504 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
24505 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24506 language_registry.add(rust_lang());
24507 let _fake_servers = language_registry.register_fake_lsp(
24508 "Rust",
24509 FakeLspAdapter {
24510 ..FakeLspAdapter::default()
24511 },
24512 );
24513 let (workspace, cx) =
24514 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24515 let worktree_id = workspace.update(cx, |workspace, cx| {
24516 workspace.project().update(cx, |project, cx| {
24517 project.worktrees(cx).next().unwrap().read(cx).id()
24518 })
24519 });
24520
24521 let assert_language_servers_count =
24522 |expected: usize, context: &str, cx: &mut VisualTestContext| {
24523 project.update(cx, |project, cx| {
24524 let current = project
24525 .lsp_store()
24526 .read(cx)
24527 .as_local()
24528 .unwrap()
24529 .language_servers
24530 .len();
24531 assert_eq!(expected, current, "{context}");
24532 });
24533 };
24534
24535 assert_language_servers_count(
24536 0,
24537 "No servers should be running before any file is open",
24538 cx,
24539 );
24540 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24541 let main_editor = workspace
24542 .update_in(cx, |workspace, window, cx| {
24543 workspace.open_path(
24544 (worktree_id, rel_path("main.rs")),
24545 Some(pane.downgrade()),
24546 true,
24547 window,
24548 cx,
24549 )
24550 })
24551 .unwrap()
24552 .await
24553 .downcast::<Editor>()
24554 .unwrap();
24555 pane.update(cx, |pane, cx| {
24556 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24557 open_editor.update(cx, |editor, cx| {
24558 assert_eq!(
24559 editor.display_text(cx),
24560 "fn main() {}",
24561 "Original main.rs text on initial open",
24562 );
24563 });
24564 assert_eq!(open_editor, main_editor);
24565 });
24566 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
24567
24568 let external_editor = workspace
24569 .update_in(cx, |workspace, window, cx| {
24570 workspace.open_abs_path(
24571 PathBuf::from("/root/foo/bar/external_file.rs"),
24572 OpenOptions::default(),
24573 window,
24574 cx,
24575 )
24576 })
24577 .await
24578 .expect("opening external file")
24579 .downcast::<Editor>()
24580 .expect("downcasted external file's open element to editor");
24581 pane.update(cx, |pane, cx| {
24582 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24583 open_editor.update(cx, |editor, cx| {
24584 assert_eq!(
24585 editor.display_text(cx),
24586 "pub mod external {}",
24587 "External file is open now",
24588 );
24589 });
24590 assert_eq!(open_editor, external_editor);
24591 });
24592 assert_language_servers_count(
24593 1,
24594 "Second, external, *.rs file should join the existing server",
24595 cx,
24596 );
24597
24598 pane.update_in(cx, |pane, window, cx| {
24599 pane.close_active_item(&CloseActiveItem::default(), window, cx)
24600 })
24601 .await
24602 .unwrap();
24603 pane.update_in(cx, |pane, window, cx| {
24604 pane.navigate_backward(&Default::default(), window, cx);
24605 });
24606 cx.run_until_parked();
24607 pane.update(cx, |pane, cx| {
24608 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24609 open_editor.update(cx, |editor, cx| {
24610 assert_eq!(
24611 editor.display_text(cx),
24612 "pub mod external {}",
24613 "External file is open now",
24614 );
24615 });
24616 });
24617 assert_language_servers_count(
24618 1,
24619 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
24620 cx,
24621 );
24622
24623 cx.update(|_, cx| {
24624 workspace::reload(cx);
24625 });
24626 assert_language_servers_count(
24627 1,
24628 "After reloading the worktree with local and external files opened, only one project should be started",
24629 cx,
24630 );
24631}
24632
24633#[gpui::test]
24634async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
24635 init_test(cx, |_| {});
24636
24637 let mut cx = EditorTestContext::new(cx).await;
24638 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24639 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24640
24641 // test cursor move to start of each line on tab
24642 // for `if`, `elif`, `else`, `while`, `with` and `for`
24643 cx.set_state(indoc! {"
24644 def main():
24645 ˇ for item in items:
24646 ˇ while item.active:
24647 ˇ if item.value > 10:
24648 ˇ continue
24649 ˇ elif item.value < 0:
24650 ˇ break
24651 ˇ else:
24652 ˇ with item.context() as ctx:
24653 ˇ yield count
24654 ˇ else:
24655 ˇ log('while else')
24656 ˇ else:
24657 ˇ log('for else')
24658 "});
24659 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24660 cx.assert_editor_state(indoc! {"
24661 def main():
24662 ˇfor item in items:
24663 ˇwhile item.active:
24664 ˇif item.value > 10:
24665 ˇcontinue
24666 ˇelif item.value < 0:
24667 ˇbreak
24668 ˇelse:
24669 ˇwith item.context() as ctx:
24670 ˇyield count
24671 ˇelse:
24672 ˇlog('while else')
24673 ˇelse:
24674 ˇlog('for else')
24675 "});
24676 // test relative indent is preserved when tab
24677 // for `if`, `elif`, `else`, `while`, `with` and `for`
24678 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24679 cx.assert_editor_state(indoc! {"
24680 def main():
24681 ˇfor item in items:
24682 ˇwhile item.active:
24683 ˇif item.value > 10:
24684 ˇcontinue
24685 ˇelif item.value < 0:
24686 ˇbreak
24687 ˇelse:
24688 ˇwith item.context() as ctx:
24689 ˇyield count
24690 ˇelse:
24691 ˇlog('while else')
24692 ˇelse:
24693 ˇlog('for else')
24694 "});
24695
24696 // test cursor move to start of each line on tab
24697 // for `try`, `except`, `else`, `finally`, `match` and `def`
24698 cx.set_state(indoc! {"
24699 def main():
24700 ˇ try:
24701 ˇ fetch()
24702 ˇ except ValueError:
24703 ˇ handle_error()
24704 ˇ else:
24705 ˇ match value:
24706 ˇ case _:
24707 ˇ finally:
24708 ˇ def status():
24709 ˇ return 0
24710 "});
24711 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24712 cx.assert_editor_state(indoc! {"
24713 def main():
24714 ˇtry:
24715 ˇfetch()
24716 ˇexcept ValueError:
24717 ˇhandle_error()
24718 ˇelse:
24719 ˇmatch value:
24720 ˇcase _:
24721 ˇfinally:
24722 ˇdef status():
24723 ˇreturn 0
24724 "});
24725 // test relative indent is preserved when tab
24726 // for `try`, `except`, `else`, `finally`, `match` and `def`
24727 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24728 cx.assert_editor_state(indoc! {"
24729 def main():
24730 ˇtry:
24731 ˇfetch()
24732 ˇexcept ValueError:
24733 ˇhandle_error()
24734 ˇelse:
24735 ˇmatch value:
24736 ˇcase _:
24737 ˇfinally:
24738 ˇdef status():
24739 ˇreturn 0
24740 "});
24741}
24742
24743#[gpui::test]
24744async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
24745 init_test(cx, |_| {});
24746
24747 let mut cx = EditorTestContext::new(cx).await;
24748 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24749 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24750
24751 // test `else` auto outdents when typed inside `if` block
24752 cx.set_state(indoc! {"
24753 def main():
24754 if i == 2:
24755 return
24756 ˇ
24757 "});
24758 cx.update_editor(|editor, window, cx| {
24759 editor.handle_input("else:", window, cx);
24760 });
24761 cx.assert_editor_state(indoc! {"
24762 def main():
24763 if i == 2:
24764 return
24765 else:ˇ
24766 "});
24767
24768 // test `except` auto outdents when typed inside `try` block
24769 cx.set_state(indoc! {"
24770 def main():
24771 try:
24772 i = 2
24773 ˇ
24774 "});
24775 cx.update_editor(|editor, window, cx| {
24776 editor.handle_input("except:", window, cx);
24777 });
24778 cx.assert_editor_state(indoc! {"
24779 def main():
24780 try:
24781 i = 2
24782 except:ˇ
24783 "});
24784
24785 // test `else` auto outdents when typed inside `except` block
24786 cx.set_state(indoc! {"
24787 def main():
24788 try:
24789 i = 2
24790 except:
24791 j = 2
24792 ˇ
24793 "});
24794 cx.update_editor(|editor, window, cx| {
24795 editor.handle_input("else:", window, cx);
24796 });
24797 cx.assert_editor_state(indoc! {"
24798 def main():
24799 try:
24800 i = 2
24801 except:
24802 j = 2
24803 else:ˇ
24804 "});
24805
24806 // test `finally` auto outdents when typed inside `else` block
24807 cx.set_state(indoc! {"
24808 def main():
24809 try:
24810 i = 2
24811 except:
24812 j = 2
24813 else:
24814 k = 2
24815 ˇ
24816 "});
24817 cx.update_editor(|editor, window, cx| {
24818 editor.handle_input("finally:", window, cx);
24819 });
24820 cx.assert_editor_state(indoc! {"
24821 def main():
24822 try:
24823 i = 2
24824 except:
24825 j = 2
24826 else:
24827 k = 2
24828 finally:ˇ
24829 "});
24830
24831 // test `else` does not outdents when typed inside `except` block right after for block
24832 cx.set_state(indoc! {"
24833 def main():
24834 try:
24835 i = 2
24836 except:
24837 for i in range(n):
24838 pass
24839 ˇ
24840 "});
24841 cx.update_editor(|editor, window, cx| {
24842 editor.handle_input("else:", window, cx);
24843 });
24844 cx.assert_editor_state(indoc! {"
24845 def main():
24846 try:
24847 i = 2
24848 except:
24849 for i in range(n):
24850 pass
24851 else:ˇ
24852 "});
24853
24854 // test `finally` auto outdents when typed inside `else` block right after for block
24855 cx.set_state(indoc! {"
24856 def main():
24857 try:
24858 i = 2
24859 except:
24860 j = 2
24861 else:
24862 for i in range(n):
24863 pass
24864 ˇ
24865 "});
24866 cx.update_editor(|editor, window, cx| {
24867 editor.handle_input("finally:", window, cx);
24868 });
24869 cx.assert_editor_state(indoc! {"
24870 def main():
24871 try:
24872 i = 2
24873 except:
24874 j = 2
24875 else:
24876 for i in range(n):
24877 pass
24878 finally:ˇ
24879 "});
24880
24881 // test `except` outdents to inner "try" block
24882 cx.set_state(indoc! {"
24883 def main():
24884 try:
24885 i = 2
24886 if i == 2:
24887 try:
24888 i = 3
24889 ˇ
24890 "});
24891 cx.update_editor(|editor, window, cx| {
24892 editor.handle_input("except:", window, cx);
24893 });
24894 cx.assert_editor_state(indoc! {"
24895 def main():
24896 try:
24897 i = 2
24898 if i == 2:
24899 try:
24900 i = 3
24901 except:ˇ
24902 "});
24903
24904 // test `except` outdents to outer "try" block
24905 cx.set_state(indoc! {"
24906 def main():
24907 try:
24908 i = 2
24909 if i == 2:
24910 try:
24911 i = 3
24912 ˇ
24913 "});
24914 cx.update_editor(|editor, window, cx| {
24915 editor.handle_input("except:", window, cx);
24916 });
24917 cx.assert_editor_state(indoc! {"
24918 def main():
24919 try:
24920 i = 2
24921 if i == 2:
24922 try:
24923 i = 3
24924 except:ˇ
24925 "});
24926
24927 // test `else` stays at correct indent when typed after `for` block
24928 cx.set_state(indoc! {"
24929 def main():
24930 for i in range(10):
24931 if i == 3:
24932 break
24933 ˇ
24934 "});
24935 cx.update_editor(|editor, window, cx| {
24936 editor.handle_input("else:", window, cx);
24937 });
24938 cx.assert_editor_state(indoc! {"
24939 def main():
24940 for i in range(10):
24941 if i == 3:
24942 break
24943 else:ˇ
24944 "});
24945
24946 // test does not outdent on typing after line with square brackets
24947 cx.set_state(indoc! {"
24948 def f() -> list[str]:
24949 ˇ
24950 "});
24951 cx.update_editor(|editor, window, cx| {
24952 editor.handle_input("a", window, cx);
24953 });
24954 cx.assert_editor_state(indoc! {"
24955 def f() -> list[str]:
24956 aˇ
24957 "});
24958
24959 // test does not outdent on typing : after case keyword
24960 cx.set_state(indoc! {"
24961 match 1:
24962 caseˇ
24963 "});
24964 cx.update_editor(|editor, window, cx| {
24965 editor.handle_input(":", window, cx);
24966 });
24967 cx.assert_editor_state(indoc! {"
24968 match 1:
24969 case:ˇ
24970 "});
24971}
24972
24973#[gpui::test]
24974async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
24975 init_test(cx, |_| {});
24976 update_test_language_settings(cx, |settings| {
24977 settings.defaults.extend_comment_on_newline = Some(false);
24978 });
24979 let mut cx = EditorTestContext::new(cx).await;
24980 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24981 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24982
24983 // test correct indent after newline on comment
24984 cx.set_state(indoc! {"
24985 # COMMENT:ˇ
24986 "});
24987 cx.update_editor(|editor, window, cx| {
24988 editor.newline(&Newline, window, cx);
24989 });
24990 cx.assert_editor_state(indoc! {"
24991 # COMMENT:
24992 ˇ
24993 "});
24994
24995 // test correct indent after newline in brackets
24996 cx.set_state(indoc! {"
24997 {ˇ}
24998 "});
24999 cx.update_editor(|editor, window, cx| {
25000 editor.newline(&Newline, window, cx);
25001 });
25002 cx.run_until_parked();
25003 cx.assert_editor_state(indoc! {"
25004 {
25005 ˇ
25006 }
25007 "});
25008
25009 cx.set_state(indoc! {"
25010 (ˇ)
25011 "});
25012 cx.update_editor(|editor, window, cx| {
25013 editor.newline(&Newline, window, cx);
25014 });
25015 cx.run_until_parked();
25016 cx.assert_editor_state(indoc! {"
25017 (
25018 ˇ
25019 )
25020 "});
25021
25022 // do not indent after empty lists or dictionaries
25023 cx.set_state(indoc! {"
25024 a = []ˇ
25025 "});
25026 cx.update_editor(|editor, window, cx| {
25027 editor.newline(&Newline, window, cx);
25028 });
25029 cx.run_until_parked();
25030 cx.assert_editor_state(indoc! {"
25031 a = []
25032 ˇ
25033 "});
25034}
25035
25036#[gpui::test]
25037async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
25038 init_test(cx, |_| {});
25039
25040 let mut cx = EditorTestContext::new(cx).await;
25041 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25042 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25043
25044 // test cursor move to start of each line on tab
25045 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
25046 cx.set_state(indoc! {"
25047 function main() {
25048 ˇ for item in $items; do
25049 ˇ while [ -n \"$item\" ]; do
25050 ˇ if [ \"$value\" -gt 10 ]; then
25051 ˇ continue
25052 ˇ elif [ \"$value\" -lt 0 ]; then
25053 ˇ break
25054 ˇ else
25055 ˇ echo \"$item\"
25056 ˇ fi
25057 ˇ done
25058 ˇ done
25059 ˇ}
25060 "});
25061 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25062 cx.assert_editor_state(indoc! {"
25063 function main() {
25064 ˇfor item in $items; do
25065 ˇwhile [ -n \"$item\" ]; do
25066 ˇif [ \"$value\" -gt 10 ]; then
25067 ˇcontinue
25068 ˇelif [ \"$value\" -lt 0 ]; then
25069 ˇbreak
25070 ˇelse
25071 ˇecho \"$item\"
25072 ˇfi
25073 ˇdone
25074 ˇdone
25075 ˇ}
25076 "});
25077 // test relative indent is preserved when tab
25078 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25079 cx.assert_editor_state(indoc! {"
25080 function main() {
25081 ˇfor item in $items; do
25082 ˇwhile [ -n \"$item\" ]; do
25083 ˇif [ \"$value\" -gt 10 ]; then
25084 ˇcontinue
25085 ˇelif [ \"$value\" -lt 0 ]; then
25086 ˇbreak
25087 ˇelse
25088 ˇecho \"$item\"
25089 ˇfi
25090 ˇdone
25091 ˇdone
25092 ˇ}
25093 "});
25094
25095 // test cursor move to start of each line on tab
25096 // for `case` statement with patterns
25097 cx.set_state(indoc! {"
25098 function handle() {
25099 ˇ case \"$1\" in
25100 ˇ start)
25101 ˇ echo \"a\"
25102 ˇ ;;
25103 ˇ stop)
25104 ˇ echo \"b\"
25105 ˇ ;;
25106 ˇ *)
25107 ˇ echo \"c\"
25108 ˇ ;;
25109 ˇ esac
25110 ˇ}
25111 "});
25112 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25113 cx.assert_editor_state(indoc! {"
25114 function handle() {
25115 ˇcase \"$1\" in
25116 ˇstart)
25117 ˇecho \"a\"
25118 ˇ;;
25119 ˇstop)
25120 ˇecho \"b\"
25121 ˇ;;
25122 ˇ*)
25123 ˇecho \"c\"
25124 ˇ;;
25125 ˇesac
25126 ˇ}
25127 "});
25128}
25129
25130#[gpui::test]
25131async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
25132 init_test(cx, |_| {});
25133
25134 let mut cx = EditorTestContext::new(cx).await;
25135 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25136 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25137
25138 // test indents on comment insert
25139 cx.set_state(indoc! {"
25140 function main() {
25141 ˇ for item in $items; do
25142 ˇ while [ -n \"$item\" ]; do
25143 ˇ if [ \"$value\" -gt 10 ]; then
25144 ˇ continue
25145 ˇ elif [ \"$value\" -lt 0 ]; then
25146 ˇ break
25147 ˇ else
25148 ˇ echo \"$item\"
25149 ˇ fi
25150 ˇ done
25151 ˇ done
25152 ˇ}
25153 "});
25154 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
25155 cx.assert_editor_state(indoc! {"
25156 function main() {
25157 #ˇ for item in $items; do
25158 #ˇ while [ -n \"$item\" ]; do
25159 #ˇ if [ \"$value\" -gt 10 ]; then
25160 #ˇ continue
25161 #ˇ elif [ \"$value\" -lt 0 ]; then
25162 #ˇ break
25163 #ˇ else
25164 #ˇ echo \"$item\"
25165 #ˇ fi
25166 #ˇ done
25167 #ˇ done
25168 #ˇ}
25169 "});
25170}
25171
25172#[gpui::test]
25173async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
25174 init_test(cx, |_| {});
25175
25176 let mut cx = EditorTestContext::new(cx).await;
25177 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25178 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25179
25180 // test `else` auto outdents when typed inside `if` block
25181 cx.set_state(indoc! {"
25182 if [ \"$1\" = \"test\" ]; then
25183 echo \"foo bar\"
25184 ˇ
25185 "});
25186 cx.update_editor(|editor, window, cx| {
25187 editor.handle_input("else", window, cx);
25188 });
25189 cx.assert_editor_state(indoc! {"
25190 if [ \"$1\" = \"test\" ]; then
25191 echo \"foo bar\"
25192 elseˇ
25193 "});
25194
25195 // test `elif` auto outdents when typed inside `if` block
25196 cx.set_state(indoc! {"
25197 if [ \"$1\" = \"test\" ]; then
25198 echo \"foo bar\"
25199 ˇ
25200 "});
25201 cx.update_editor(|editor, window, cx| {
25202 editor.handle_input("elif", window, cx);
25203 });
25204 cx.assert_editor_state(indoc! {"
25205 if [ \"$1\" = \"test\" ]; then
25206 echo \"foo bar\"
25207 elifˇ
25208 "});
25209
25210 // test `fi` auto outdents when typed inside `else` block
25211 cx.set_state(indoc! {"
25212 if [ \"$1\" = \"test\" ]; then
25213 echo \"foo bar\"
25214 else
25215 echo \"bar baz\"
25216 ˇ
25217 "});
25218 cx.update_editor(|editor, window, cx| {
25219 editor.handle_input("fi", window, cx);
25220 });
25221 cx.assert_editor_state(indoc! {"
25222 if [ \"$1\" = \"test\" ]; then
25223 echo \"foo bar\"
25224 else
25225 echo \"bar baz\"
25226 fiˇ
25227 "});
25228
25229 // test `done` auto outdents when typed inside `while` block
25230 cx.set_state(indoc! {"
25231 while read line; do
25232 echo \"$line\"
25233 ˇ
25234 "});
25235 cx.update_editor(|editor, window, cx| {
25236 editor.handle_input("done", window, cx);
25237 });
25238 cx.assert_editor_state(indoc! {"
25239 while read line; do
25240 echo \"$line\"
25241 doneˇ
25242 "});
25243
25244 // test `done` auto outdents when typed inside `for` block
25245 cx.set_state(indoc! {"
25246 for file in *.txt; do
25247 cat \"$file\"
25248 ˇ
25249 "});
25250 cx.update_editor(|editor, window, cx| {
25251 editor.handle_input("done", window, cx);
25252 });
25253 cx.assert_editor_state(indoc! {"
25254 for file in *.txt; do
25255 cat \"$file\"
25256 doneˇ
25257 "});
25258
25259 // test `esac` auto outdents when typed inside `case` block
25260 cx.set_state(indoc! {"
25261 case \"$1\" in
25262 start)
25263 echo \"foo bar\"
25264 ;;
25265 stop)
25266 echo \"bar baz\"
25267 ;;
25268 ˇ
25269 "});
25270 cx.update_editor(|editor, window, cx| {
25271 editor.handle_input("esac", window, cx);
25272 });
25273 cx.assert_editor_state(indoc! {"
25274 case \"$1\" in
25275 start)
25276 echo \"foo bar\"
25277 ;;
25278 stop)
25279 echo \"bar baz\"
25280 ;;
25281 esacˇ
25282 "});
25283
25284 // test `*)` auto outdents when typed inside `case` block
25285 cx.set_state(indoc! {"
25286 case \"$1\" in
25287 start)
25288 echo \"foo bar\"
25289 ;;
25290 ˇ
25291 "});
25292 cx.update_editor(|editor, window, cx| {
25293 editor.handle_input("*)", window, cx);
25294 });
25295 cx.assert_editor_state(indoc! {"
25296 case \"$1\" in
25297 start)
25298 echo \"foo bar\"
25299 ;;
25300 *)ˇ
25301 "});
25302
25303 // test `fi` outdents to correct level with nested if blocks
25304 cx.set_state(indoc! {"
25305 if [ \"$1\" = \"test\" ]; then
25306 echo \"outer if\"
25307 if [ \"$2\" = \"debug\" ]; then
25308 echo \"inner if\"
25309 ˇ
25310 "});
25311 cx.update_editor(|editor, window, cx| {
25312 editor.handle_input("fi", window, cx);
25313 });
25314 cx.assert_editor_state(indoc! {"
25315 if [ \"$1\" = \"test\" ]; then
25316 echo \"outer if\"
25317 if [ \"$2\" = \"debug\" ]; then
25318 echo \"inner if\"
25319 fiˇ
25320 "});
25321}
25322
25323#[gpui::test]
25324async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
25325 init_test(cx, |_| {});
25326 update_test_language_settings(cx, |settings| {
25327 settings.defaults.extend_comment_on_newline = Some(false);
25328 });
25329 let mut cx = EditorTestContext::new(cx).await;
25330 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25331 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25332
25333 // test correct indent after newline on comment
25334 cx.set_state(indoc! {"
25335 # COMMENT:ˇ
25336 "});
25337 cx.update_editor(|editor, window, cx| {
25338 editor.newline(&Newline, window, cx);
25339 });
25340 cx.assert_editor_state(indoc! {"
25341 # COMMENT:
25342 ˇ
25343 "});
25344
25345 // test correct indent after newline after `then`
25346 cx.set_state(indoc! {"
25347
25348 if [ \"$1\" = \"test\" ]; thenˇ
25349 "});
25350 cx.update_editor(|editor, window, cx| {
25351 editor.newline(&Newline, window, cx);
25352 });
25353 cx.run_until_parked();
25354 cx.assert_editor_state(indoc! {"
25355
25356 if [ \"$1\" = \"test\" ]; then
25357 ˇ
25358 "});
25359
25360 // test correct indent after newline after `else`
25361 cx.set_state(indoc! {"
25362 if [ \"$1\" = \"test\" ]; then
25363 elseˇ
25364 "});
25365 cx.update_editor(|editor, window, cx| {
25366 editor.newline(&Newline, window, cx);
25367 });
25368 cx.run_until_parked();
25369 cx.assert_editor_state(indoc! {"
25370 if [ \"$1\" = \"test\" ]; then
25371 else
25372 ˇ
25373 "});
25374
25375 // test correct indent after newline after `elif`
25376 cx.set_state(indoc! {"
25377 if [ \"$1\" = \"test\" ]; then
25378 elifˇ
25379 "});
25380 cx.update_editor(|editor, window, cx| {
25381 editor.newline(&Newline, window, cx);
25382 });
25383 cx.run_until_parked();
25384 cx.assert_editor_state(indoc! {"
25385 if [ \"$1\" = \"test\" ]; then
25386 elif
25387 ˇ
25388 "});
25389
25390 // test correct indent after newline after `do`
25391 cx.set_state(indoc! {"
25392 for file in *.txt; doˇ
25393 "});
25394 cx.update_editor(|editor, window, cx| {
25395 editor.newline(&Newline, window, cx);
25396 });
25397 cx.run_until_parked();
25398 cx.assert_editor_state(indoc! {"
25399 for file in *.txt; do
25400 ˇ
25401 "});
25402
25403 // test correct indent after newline after case pattern
25404 cx.set_state(indoc! {"
25405 case \"$1\" in
25406 start)ˇ
25407 "});
25408 cx.update_editor(|editor, window, cx| {
25409 editor.newline(&Newline, window, cx);
25410 });
25411 cx.run_until_parked();
25412 cx.assert_editor_state(indoc! {"
25413 case \"$1\" in
25414 start)
25415 ˇ
25416 "});
25417
25418 // test correct indent after newline after case pattern
25419 cx.set_state(indoc! {"
25420 case \"$1\" in
25421 start)
25422 ;;
25423 *)ˇ
25424 "});
25425 cx.update_editor(|editor, window, cx| {
25426 editor.newline(&Newline, window, cx);
25427 });
25428 cx.run_until_parked();
25429 cx.assert_editor_state(indoc! {"
25430 case \"$1\" in
25431 start)
25432 ;;
25433 *)
25434 ˇ
25435 "});
25436
25437 // test correct indent after newline after function opening brace
25438 cx.set_state(indoc! {"
25439 function test() {ˇ}
25440 "});
25441 cx.update_editor(|editor, window, cx| {
25442 editor.newline(&Newline, window, cx);
25443 });
25444 cx.run_until_parked();
25445 cx.assert_editor_state(indoc! {"
25446 function test() {
25447 ˇ
25448 }
25449 "});
25450
25451 // test no extra indent after semicolon on same line
25452 cx.set_state(indoc! {"
25453 echo \"test\";ˇ
25454 "});
25455 cx.update_editor(|editor, window, cx| {
25456 editor.newline(&Newline, window, cx);
25457 });
25458 cx.run_until_parked();
25459 cx.assert_editor_state(indoc! {"
25460 echo \"test\";
25461 ˇ
25462 "});
25463}
25464
25465fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
25466 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
25467 point..point
25468}
25469
25470#[track_caller]
25471fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
25472 let (text, ranges) = marked_text_ranges(marked_text, true);
25473 assert_eq!(editor.text(cx), text);
25474 assert_eq!(
25475 editor.selections.ranges(&editor.display_snapshot(cx)),
25476 ranges,
25477 "Assert selections are {}",
25478 marked_text
25479 );
25480}
25481
25482pub fn handle_signature_help_request(
25483 cx: &mut EditorLspTestContext,
25484 mocked_response: lsp::SignatureHelp,
25485) -> impl Future<Output = ()> + use<> {
25486 let mut request =
25487 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
25488 let mocked_response = mocked_response.clone();
25489 async move { Ok(Some(mocked_response)) }
25490 });
25491
25492 async move {
25493 request.next().await;
25494 }
25495}
25496
25497#[track_caller]
25498pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
25499 cx.update_editor(|editor, _, _| {
25500 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
25501 let entries = menu.entries.borrow();
25502 let entries = entries
25503 .iter()
25504 .map(|entry| entry.string.as_str())
25505 .collect::<Vec<_>>();
25506 assert_eq!(entries, expected);
25507 } else {
25508 panic!("Expected completions menu");
25509 }
25510 });
25511}
25512
25513/// Handle completion request passing a marked string specifying where the completion
25514/// should be triggered from using '|' character, what range should be replaced, and what completions
25515/// should be returned using '<' and '>' to delimit the range.
25516///
25517/// Also see `handle_completion_request_with_insert_and_replace`.
25518#[track_caller]
25519pub fn handle_completion_request(
25520 marked_string: &str,
25521 completions: Vec<&'static str>,
25522 is_incomplete: bool,
25523 counter: Arc<AtomicUsize>,
25524 cx: &mut EditorLspTestContext,
25525) -> impl Future<Output = ()> {
25526 let complete_from_marker: TextRangeMarker = '|'.into();
25527 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25528 let (_, mut marked_ranges) = marked_text_ranges_by(
25529 marked_string,
25530 vec![complete_from_marker.clone(), replace_range_marker.clone()],
25531 );
25532
25533 let complete_from_position =
25534 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25535 let replace_range =
25536 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25537
25538 let mut request =
25539 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25540 let completions = completions.clone();
25541 counter.fetch_add(1, atomic::Ordering::Release);
25542 async move {
25543 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25544 assert_eq!(
25545 params.text_document_position.position,
25546 complete_from_position
25547 );
25548 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
25549 is_incomplete,
25550 item_defaults: None,
25551 items: completions
25552 .iter()
25553 .map(|completion_text| lsp::CompletionItem {
25554 label: completion_text.to_string(),
25555 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
25556 range: replace_range,
25557 new_text: completion_text.to_string(),
25558 })),
25559 ..Default::default()
25560 })
25561 .collect(),
25562 })))
25563 }
25564 });
25565
25566 async move {
25567 request.next().await;
25568 }
25569}
25570
25571/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
25572/// given instead, which also contains an `insert` range.
25573///
25574/// This function uses markers to define ranges:
25575/// - `|` marks the cursor position
25576/// - `<>` marks the replace range
25577/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
25578pub fn handle_completion_request_with_insert_and_replace(
25579 cx: &mut EditorLspTestContext,
25580 marked_string: &str,
25581 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
25582 counter: Arc<AtomicUsize>,
25583) -> impl Future<Output = ()> {
25584 let complete_from_marker: TextRangeMarker = '|'.into();
25585 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25586 let insert_range_marker: TextRangeMarker = ('{', '}').into();
25587
25588 let (_, mut marked_ranges) = marked_text_ranges_by(
25589 marked_string,
25590 vec![
25591 complete_from_marker.clone(),
25592 replace_range_marker.clone(),
25593 insert_range_marker.clone(),
25594 ],
25595 );
25596
25597 let complete_from_position =
25598 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25599 let replace_range =
25600 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25601
25602 let insert_range = match marked_ranges.remove(&insert_range_marker) {
25603 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
25604 _ => lsp::Range {
25605 start: replace_range.start,
25606 end: complete_from_position,
25607 },
25608 };
25609
25610 let mut request =
25611 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25612 let completions = completions.clone();
25613 counter.fetch_add(1, atomic::Ordering::Release);
25614 async move {
25615 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25616 assert_eq!(
25617 params.text_document_position.position, complete_from_position,
25618 "marker `|` position doesn't match",
25619 );
25620 Ok(Some(lsp::CompletionResponse::Array(
25621 completions
25622 .iter()
25623 .map(|(label, new_text)| lsp::CompletionItem {
25624 label: label.to_string(),
25625 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
25626 lsp::InsertReplaceEdit {
25627 insert: insert_range,
25628 replace: replace_range,
25629 new_text: new_text.to_string(),
25630 },
25631 )),
25632 ..Default::default()
25633 })
25634 .collect(),
25635 )))
25636 }
25637 });
25638
25639 async move {
25640 request.next().await;
25641 }
25642}
25643
25644fn handle_resolve_completion_request(
25645 cx: &mut EditorLspTestContext,
25646 edits: Option<Vec<(&'static str, &'static str)>>,
25647) -> impl Future<Output = ()> {
25648 let edits = edits.map(|edits| {
25649 edits
25650 .iter()
25651 .map(|(marked_string, new_text)| {
25652 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
25653 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
25654 lsp::TextEdit::new(replace_range, new_text.to_string())
25655 })
25656 .collect::<Vec<_>>()
25657 });
25658
25659 let mut request =
25660 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
25661 let edits = edits.clone();
25662 async move {
25663 Ok(lsp::CompletionItem {
25664 additional_text_edits: edits,
25665 ..Default::default()
25666 })
25667 }
25668 });
25669
25670 async move {
25671 request.next().await;
25672 }
25673}
25674
25675pub(crate) fn update_test_language_settings(
25676 cx: &mut TestAppContext,
25677 f: impl Fn(&mut AllLanguageSettingsContent),
25678) {
25679 cx.update(|cx| {
25680 SettingsStore::update_global(cx, |store, cx| {
25681 store.update_user_settings(cx, |settings| f(&mut settings.project.all_languages));
25682 });
25683 });
25684}
25685
25686pub(crate) fn update_test_project_settings(
25687 cx: &mut TestAppContext,
25688 f: impl Fn(&mut ProjectSettingsContent),
25689) {
25690 cx.update(|cx| {
25691 SettingsStore::update_global(cx, |store, cx| {
25692 store.update_user_settings(cx, |settings| f(&mut settings.project));
25693 });
25694 });
25695}
25696
25697pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
25698 cx.update(|cx| {
25699 assets::Assets.load_test_fonts(cx);
25700 let store = SettingsStore::test(cx);
25701 cx.set_global(store);
25702 theme::init(theme::LoadThemes::JustBase, cx);
25703 release_channel::init(SemanticVersion::default(), cx);
25704 crate::init(cx);
25705 });
25706 zlog::init_test();
25707 update_test_language_settings(cx, f);
25708}
25709
25710#[track_caller]
25711fn assert_hunk_revert(
25712 not_reverted_text_with_selections: &str,
25713 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
25714 expected_reverted_text_with_selections: &str,
25715 base_text: &str,
25716 cx: &mut EditorLspTestContext,
25717) {
25718 cx.set_state(not_reverted_text_with_selections);
25719 cx.set_head_text(base_text);
25720 cx.executor().run_until_parked();
25721
25722 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
25723 let snapshot = editor.snapshot(window, cx);
25724 let reverted_hunk_statuses = snapshot
25725 .buffer_snapshot()
25726 .diff_hunks_in_range(0..snapshot.buffer_snapshot().len())
25727 .map(|hunk| hunk.status().kind)
25728 .collect::<Vec<_>>();
25729
25730 editor.git_restore(&Default::default(), window, cx);
25731 reverted_hunk_statuses
25732 });
25733 cx.executor().run_until_parked();
25734 cx.assert_editor_state(expected_reverted_text_with_selections);
25735 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
25736}
25737
25738#[gpui::test(iterations = 10)]
25739async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
25740 init_test(cx, |_| {});
25741
25742 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
25743 let counter = diagnostic_requests.clone();
25744
25745 let fs = FakeFs::new(cx.executor());
25746 fs.insert_tree(
25747 path!("/a"),
25748 json!({
25749 "first.rs": "fn main() { let a = 5; }",
25750 "second.rs": "// Test file",
25751 }),
25752 )
25753 .await;
25754
25755 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25756 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25757 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25758
25759 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25760 language_registry.add(rust_lang());
25761 let mut fake_servers = language_registry.register_fake_lsp(
25762 "Rust",
25763 FakeLspAdapter {
25764 capabilities: lsp::ServerCapabilities {
25765 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
25766 lsp::DiagnosticOptions {
25767 identifier: None,
25768 inter_file_dependencies: true,
25769 workspace_diagnostics: true,
25770 work_done_progress_options: Default::default(),
25771 },
25772 )),
25773 ..Default::default()
25774 },
25775 ..Default::default()
25776 },
25777 );
25778
25779 let editor = workspace
25780 .update(cx, |workspace, window, cx| {
25781 workspace.open_abs_path(
25782 PathBuf::from(path!("/a/first.rs")),
25783 OpenOptions::default(),
25784 window,
25785 cx,
25786 )
25787 })
25788 .unwrap()
25789 .await
25790 .unwrap()
25791 .downcast::<Editor>()
25792 .unwrap();
25793 let fake_server = fake_servers.next().await.unwrap();
25794 let server_id = fake_server.server.server_id();
25795 let mut first_request = fake_server
25796 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
25797 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
25798 let result_id = Some(new_result_id.to_string());
25799 assert_eq!(
25800 params.text_document.uri,
25801 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25802 );
25803 async move {
25804 Ok(lsp::DocumentDiagnosticReportResult::Report(
25805 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
25806 related_documents: None,
25807 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
25808 items: Vec::new(),
25809 result_id,
25810 },
25811 }),
25812 ))
25813 }
25814 });
25815
25816 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
25817 project.update(cx, |project, cx| {
25818 let buffer_id = editor
25819 .read(cx)
25820 .buffer()
25821 .read(cx)
25822 .as_singleton()
25823 .expect("created a singleton buffer")
25824 .read(cx)
25825 .remote_id();
25826 let buffer_result_id = project
25827 .lsp_store()
25828 .read(cx)
25829 .result_id(server_id, buffer_id, cx);
25830 assert_eq!(expected, buffer_result_id);
25831 });
25832 };
25833
25834 ensure_result_id(None, cx);
25835 cx.executor().advance_clock(Duration::from_millis(60));
25836 cx.executor().run_until_parked();
25837 assert_eq!(
25838 diagnostic_requests.load(atomic::Ordering::Acquire),
25839 1,
25840 "Opening file should trigger diagnostic request"
25841 );
25842 first_request
25843 .next()
25844 .await
25845 .expect("should have sent the first diagnostics pull request");
25846 ensure_result_id(Some("1".to_string()), cx);
25847
25848 // Editing should trigger diagnostics
25849 editor.update_in(cx, |editor, window, cx| {
25850 editor.handle_input("2", window, cx)
25851 });
25852 cx.executor().advance_clock(Duration::from_millis(60));
25853 cx.executor().run_until_parked();
25854 assert_eq!(
25855 diagnostic_requests.load(atomic::Ordering::Acquire),
25856 2,
25857 "Editing should trigger diagnostic request"
25858 );
25859 ensure_result_id(Some("2".to_string()), cx);
25860
25861 // Moving cursor should not trigger diagnostic request
25862 editor.update_in(cx, |editor, window, cx| {
25863 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25864 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
25865 });
25866 });
25867 cx.executor().advance_clock(Duration::from_millis(60));
25868 cx.executor().run_until_parked();
25869 assert_eq!(
25870 diagnostic_requests.load(atomic::Ordering::Acquire),
25871 2,
25872 "Cursor movement should not trigger diagnostic request"
25873 );
25874 ensure_result_id(Some("2".to_string()), cx);
25875 // Multiple rapid edits should be debounced
25876 for _ in 0..5 {
25877 editor.update_in(cx, |editor, window, cx| {
25878 editor.handle_input("x", window, cx)
25879 });
25880 }
25881 cx.executor().advance_clock(Duration::from_millis(60));
25882 cx.executor().run_until_parked();
25883
25884 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
25885 assert!(
25886 final_requests <= 4,
25887 "Multiple rapid edits should be debounced (got {final_requests} requests)",
25888 );
25889 ensure_result_id(Some(final_requests.to_string()), cx);
25890}
25891
25892#[gpui::test]
25893async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
25894 // Regression test for issue #11671
25895 // Previously, adding a cursor after moving multiple cursors would reset
25896 // the cursor count instead of adding to the existing cursors.
25897 init_test(cx, |_| {});
25898 let mut cx = EditorTestContext::new(cx).await;
25899
25900 // Create a simple buffer with cursor at start
25901 cx.set_state(indoc! {"
25902 ˇaaaa
25903 bbbb
25904 cccc
25905 dddd
25906 eeee
25907 ffff
25908 gggg
25909 hhhh"});
25910
25911 // Add 2 cursors below (so we have 3 total)
25912 cx.update_editor(|editor, window, cx| {
25913 editor.add_selection_below(&Default::default(), window, cx);
25914 editor.add_selection_below(&Default::default(), window, cx);
25915 });
25916
25917 // Verify we have 3 cursors
25918 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
25919 assert_eq!(
25920 initial_count, 3,
25921 "Should have 3 cursors after adding 2 below"
25922 );
25923
25924 // Move down one line
25925 cx.update_editor(|editor, window, cx| {
25926 editor.move_down(&MoveDown, window, cx);
25927 });
25928
25929 // Add another cursor below
25930 cx.update_editor(|editor, window, cx| {
25931 editor.add_selection_below(&Default::default(), window, cx);
25932 });
25933
25934 // Should now have 4 cursors (3 original + 1 new)
25935 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
25936 assert_eq!(
25937 final_count, 4,
25938 "Should have 4 cursors after moving and adding another"
25939 );
25940}
25941
25942#[gpui::test]
25943async fn test_add_selection_skip_soft_wrap_option(cx: &mut TestAppContext) {
25944 init_test(cx, |_| {});
25945
25946 let mut cx = EditorTestContext::new(cx).await;
25947
25948 cx.set_state(indoc!(
25949 r#"ˇThis is a very long line that will be wrapped when soft wrapping is enabled
25950 Second line here"#
25951 ));
25952
25953 cx.update_editor(|editor, window, cx| {
25954 // Enable soft wrapping with a narrow width to force soft wrapping and
25955 // confirm that more than 2 rows are being displayed.
25956 editor.set_wrap_width(Some(100.0.into()), cx);
25957 assert!(editor.display_text(cx).lines().count() > 2);
25958
25959 editor.add_selection_below(
25960 &AddSelectionBelow {
25961 skip_soft_wrap: true,
25962 },
25963 window,
25964 cx,
25965 );
25966
25967 assert_eq!(
25968 display_ranges(editor, cx),
25969 &[
25970 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
25971 DisplayPoint::new(DisplayRow(8), 0)..DisplayPoint::new(DisplayRow(8), 0),
25972 ]
25973 );
25974
25975 editor.add_selection_above(
25976 &AddSelectionAbove {
25977 skip_soft_wrap: true,
25978 },
25979 window,
25980 cx,
25981 );
25982
25983 assert_eq!(
25984 display_ranges(editor, cx),
25985 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
25986 );
25987
25988 editor.add_selection_below(
25989 &AddSelectionBelow {
25990 skip_soft_wrap: false,
25991 },
25992 window,
25993 cx,
25994 );
25995
25996 assert_eq!(
25997 display_ranges(editor, cx),
25998 &[
25999 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
26000 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
26001 ]
26002 );
26003
26004 editor.add_selection_above(
26005 &AddSelectionAbove {
26006 skip_soft_wrap: false,
26007 },
26008 window,
26009 cx,
26010 );
26011
26012 assert_eq!(
26013 display_ranges(editor, cx),
26014 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
26015 );
26016 });
26017}
26018
26019#[gpui::test(iterations = 10)]
26020async fn test_document_colors(cx: &mut TestAppContext) {
26021 let expected_color = Rgba {
26022 r: 0.33,
26023 g: 0.33,
26024 b: 0.33,
26025 a: 0.33,
26026 };
26027
26028 init_test(cx, |_| {});
26029
26030 let fs = FakeFs::new(cx.executor());
26031 fs.insert_tree(
26032 path!("/a"),
26033 json!({
26034 "first.rs": "fn main() { let a = 5; }",
26035 }),
26036 )
26037 .await;
26038
26039 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
26040 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
26041 let cx = &mut VisualTestContext::from_window(*workspace, cx);
26042
26043 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
26044 language_registry.add(rust_lang());
26045 let mut fake_servers = language_registry.register_fake_lsp(
26046 "Rust",
26047 FakeLspAdapter {
26048 capabilities: lsp::ServerCapabilities {
26049 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
26050 ..lsp::ServerCapabilities::default()
26051 },
26052 name: "rust-analyzer",
26053 ..FakeLspAdapter::default()
26054 },
26055 );
26056 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
26057 "Rust",
26058 FakeLspAdapter {
26059 capabilities: lsp::ServerCapabilities {
26060 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
26061 ..lsp::ServerCapabilities::default()
26062 },
26063 name: "not-rust-analyzer",
26064 ..FakeLspAdapter::default()
26065 },
26066 );
26067
26068 let editor = workspace
26069 .update(cx, |workspace, window, cx| {
26070 workspace.open_abs_path(
26071 PathBuf::from(path!("/a/first.rs")),
26072 OpenOptions::default(),
26073 window,
26074 cx,
26075 )
26076 })
26077 .unwrap()
26078 .await
26079 .unwrap()
26080 .downcast::<Editor>()
26081 .unwrap();
26082 let fake_language_server = fake_servers.next().await.unwrap();
26083 let fake_language_server_without_capabilities =
26084 fake_servers_without_capabilities.next().await.unwrap();
26085 let requests_made = Arc::new(AtomicUsize::new(0));
26086 let closure_requests_made = Arc::clone(&requests_made);
26087 let mut color_request_handle = fake_language_server
26088 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
26089 let requests_made = Arc::clone(&closure_requests_made);
26090 async move {
26091 assert_eq!(
26092 params.text_document.uri,
26093 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
26094 );
26095 requests_made.fetch_add(1, atomic::Ordering::Release);
26096 Ok(vec![
26097 lsp::ColorInformation {
26098 range: lsp::Range {
26099 start: lsp::Position {
26100 line: 0,
26101 character: 0,
26102 },
26103 end: lsp::Position {
26104 line: 0,
26105 character: 1,
26106 },
26107 },
26108 color: lsp::Color {
26109 red: 0.33,
26110 green: 0.33,
26111 blue: 0.33,
26112 alpha: 0.33,
26113 },
26114 },
26115 lsp::ColorInformation {
26116 range: lsp::Range {
26117 start: lsp::Position {
26118 line: 0,
26119 character: 0,
26120 },
26121 end: lsp::Position {
26122 line: 0,
26123 character: 1,
26124 },
26125 },
26126 color: lsp::Color {
26127 red: 0.33,
26128 green: 0.33,
26129 blue: 0.33,
26130 alpha: 0.33,
26131 },
26132 },
26133 ])
26134 }
26135 });
26136
26137 let _handle = fake_language_server_without_capabilities
26138 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
26139 panic!("Should not be called");
26140 });
26141 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26142 color_request_handle.next().await.unwrap();
26143 cx.run_until_parked();
26144 assert_eq!(
26145 1,
26146 requests_made.load(atomic::Ordering::Acquire),
26147 "Should query for colors once per editor open"
26148 );
26149 editor.update_in(cx, |editor, _, cx| {
26150 assert_eq!(
26151 vec![expected_color],
26152 extract_color_inlays(editor, cx),
26153 "Should have an initial inlay"
26154 );
26155 });
26156
26157 // opening another file in a split should not influence the LSP query counter
26158 workspace
26159 .update(cx, |workspace, window, cx| {
26160 assert_eq!(
26161 workspace.panes().len(),
26162 1,
26163 "Should have one pane with one editor"
26164 );
26165 workspace.move_item_to_pane_in_direction(
26166 &MoveItemToPaneInDirection {
26167 direction: SplitDirection::Right,
26168 focus: false,
26169 clone: true,
26170 },
26171 window,
26172 cx,
26173 );
26174 })
26175 .unwrap();
26176 cx.run_until_parked();
26177 workspace
26178 .update(cx, |workspace, _, cx| {
26179 let panes = workspace.panes();
26180 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
26181 for pane in panes {
26182 let editor = pane
26183 .read(cx)
26184 .active_item()
26185 .and_then(|item| item.downcast::<Editor>())
26186 .expect("Should have opened an editor in each split");
26187 let editor_file = editor
26188 .read(cx)
26189 .buffer()
26190 .read(cx)
26191 .as_singleton()
26192 .expect("test deals with singleton buffers")
26193 .read(cx)
26194 .file()
26195 .expect("test buffese should have a file")
26196 .path();
26197 assert_eq!(
26198 editor_file.as_ref(),
26199 rel_path("first.rs"),
26200 "Both editors should be opened for the same file"
26201 )
26202 }
26203 })
26204 .unwrap();
26205
26206 cx.executor().advance_clock(Duration::from_millis(500));
26207 let save = editor.update_in(cx, |editor, window, cx| {
26208 editor.move_to_end(&MoveToEnd, window, cx);
26209 editor.handle_input("dirty", window, cx);
26210 editor.save(
26211 SaveOptions {
26212 format: true,
26213 autosave: true,
26214 },
26215 project.clone(),
26216 window,
26217 cx,
26218 )
26219 });
26220 save.await.unwrap();
26221
26222 color_request_handle.next().await.unwrap();
26223 cx.run_until_parked();
26224 assert_eq!(
26225 2,
26226 requests_made.load(atomic::Ordering::Acquire),
26227 "Should query for colors once per save (deduplicated) and once per formatting after save"
26228 );
26229
26230 drop(editor);
26231 let close = workspace
26232 .update(cx, |workspace, window, cx| {
26233 workspace.active_pane().update(cx, |pane, cx| {
26234 pane.close_active_item(&CloseActiveItem::default(), window, cx)
26235 })
26236 })
26237 .unwrap();
26238 close.await.unwrap();
26239 let close = workspace
26240 .update(cx, |workspace, window, cx| {
26241 workspace.active_pane().update(cx, |pane, cx| {
26242 pane.close_active_item(&CloseActiveItem::default(), window, cx)
26243 })
26244 })
26245 .unwrap();
26246 close.await.unwrap();
26247 assert_eq!(
26248 2,
26249 requests_made.load(atomic::Ordering::Acquire),
26250 "After saving and closing all editors, no extra requests should be made"
26251 );
26252 workspace
26253 .update(cx, |workspace, _, cx| {
26254 assert!(
26255 workspace.active_item(cx).is_none(),
26256 "Should close all editors"
26257 )
26258 })
26259 .unwrap();
26260
26261 workspace
26262 .update(cx, |workspace, window, cx| {
26263 workspace.active_pane().update(cx, |pane, cx| {
26264 pane.navigate_backward(&workspace::GoBack, window, cx);
26265 })
26266 })
26267 .unwrap();
26268 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26269 cx.run_until_parked();
26270 let editor = workspace
26271 .update(cx, |workspace, _, cx| {
26272 workspace
26273 .active_item(cx)
26274 .expect("Should have reopened the editor again after navigating back")
26275 .downcast::<Editor>()
26276 .expect("Should be an editor")
26277 })
26278 .unwrap();
26279
26280 assert_eq!(
26281 2,
26282 requests_made.load(atomic::Ordering::Acquire),
26283 "Cache should be reused on buffer close and reopen"
26284 );
26285 editor.update(cx, |editor, cx| {
26286 assert_eq!(
26287 vec![expected_color],
26288 extract_color_inlays(editor, cx),
26289 "Should have an initial inlay"
26290 );
26291 });
26292
26293 drop(color_request_handle);
26294 let closure_requests_made = Arc::clone(&requests_made);
26295 let mut empty_color_request_handle = fake_language_server
26296 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
26297 let requests_made = Arc::clone(&closure_requests_made);
26298 async move {
26299 assert_eq!(
26300 params.text_document.uri,
26301 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
26302 );
26303 requests_made.fetch_add(1, atomic::Ordering::Release);
26304 Ok(Vec::new())
26305 }
26306 });
26307 let save = editor.update_in(cx, |editor, window, cx| {
26308 editor.move_to_end(&MoveToEnd, window, cx);
26309 editor.handle_input("dirty_again", window, cx);
26310 editor.save(
26311 SaveOptions {
26312 format: false,
26313 autosave: true,
26314 },
26315 project.clone(),
26316 window,
26317 cx,
26318 )
26319 });
26320 save.await.unwrap();
26321
26322 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26323 empty_color_request_handle.next().await.unwrap();
26324 cx.run_until_parked();
26325 assert_eq!(
26326 3,
26327 requests_made.load(atomic::Ordering::Acquire),
26328 "Should query for colors once per save only, as formatting was not requested"
26329 );
26330 editor.update(cx, |editor, cx| {
26331 assert_eq!(
26332 Vec::<Rgba>::new(),
26333 extract_color_inlays(editor, cx),
26334 "Should clear all colors when the server returns an empty response"
26335 );
26336 });
26337}
26338
26339#[gpui::test]
26340async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
26341 init_test(cx, |_| {});
26342 let (editor, cx) = cx.add_window_view(Editor::single_line);
26343 editor.update_in(cx, |editor, window, cx| {
26344 editor.set_text("oops\n\nwow\n", window, cx)
26345 });
26346 cx.run_until_parked();
26347 editor.update(cx, |editor, cx| {
26348 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
26349 });
26350 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
26351 cx.run_until_parked();
26352 editor.update(cx, |editor, cx| {
26353 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
26354 });
26355}
26356
26357#[gpui::test]
26358async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
26359 init_test(cx, |_| {});
26360
26361 cx.update(|cx| {
26362 register_project_item::<Editor>(cx);
26363 });
26364
26365 let fs = FakeFs::new(cx.executor());
26366 fs.insert_tree("/root1", json!({})).await;
26367 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
26368 .await;
26369
26370 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
26371 let (workspace, cx) =
26372 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
26373
26374 let worktree_id = project.update(cx, |project, cx| {
26375 project.worktrees(cx).next().unwrap().read(cx).id()
26376 });
26377
26378 let handle = workspace
26379 .update_in(cx, |workspace, window, cx| {
26380 let project_path = (worktree_id, rel_path("one.pdf"));
26381 workspace.open_path(project_path, None, true, window, cx)
26382 })
26383 .await
26384 .unwrap();
26385
26386 assert_eq!(
26387 handle.to_any().entity_type(),
26388 TypeId::of::<InvalidItemView>()
26389 );
26390}
26391
26392#[gpui::test]
26393async fn test_select_next_prev_syntax_node(cx: &mut TestAppContext) {
26394 init_test(cx, |_| {});
26395
26396 let language = Arc::new(Language::new(
26397 LanguageConfig::default(),
26398 Some(tree_sitter_rust::LANGUAGE.into()),
26399 ));
26400
26401 // Test hierarchical sibling navigation
26402 let text = r#"
26403 fn outer() {
26404 if condition {
26405 let a = 1;
26406 }
26407 let b = 2;
26408 }
26409
26410 fn another() {
26411 let c = 3;
26412 }
26413 "#;
26414
26415 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
26416 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
26417 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
26418
26419 // Wait for parsing to complete
26420 editor
26421 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
26422 .await;
26423
26424 editor.update_in(cx, |editor, window, cx| {
26425 // Start by selecting "let a = 1;" inside the if block
26426 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26427 s.select_display_ranges([
26428 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 26)
26429 ]);
26430 });
26431
26432 let initial_selection = editor
26433 .selections
26434 .display_ranges(&editor.display_snapshot(cx));
26435 assert_eq!(initial_selection.len(), 1, "Should have one selection");
26436
26437 // Test select next sibling - should move up levels to find the next sibling
26438 // Since "let a = 1;" has no siblings in the if block, it should move up
26439 // to find "let b = 2;" which is a sibling of the if block
26440 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
26441 let next_selection = editor
26442 .selections
26443 .display_ranges(&editor.display_snapshot(cx));
26444
26445 // Should have a selection and it should be different from the initial
26446 assert_eq!(
26447 next_selection.len(),
26448 1,
26449 "Should have one selection after next"
26450 );
26451 assert_ne!(
26452 next_selection[0], initial_selection[0],
26453 "Next sibling selection should be different"
26454 );
26455
26456 // Test hierarchical navigation by going to the end of the current function
26457 // and trying to navigate to the next function
26458 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26459 s.select_display_ranges([
26460 DisplayPoint::new(DisplayRow(5), 12)..DisplayPoint::new(DisplayRow(5), 22)
26461 ]);
26462 });
26463
26464 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
26465 let function_next_selection = editor
26466 .selections
26467 .display_ranges(&editor.display_snapshot(cx));
26468
26469 // Should move to the next function
26470 assert_eq!(
26471 function_next_selection.len(),
26472 1,
26473 "Should have one selection after function next"
26474 );
26475
26476 // Test select previous sibling navigation
26477 editor.select_prev_syntax_node(&SelectPreviousSyntaxNode, window, cx);
26478 let prev_selection = editor
26479 .selections
26480 .display_ranges(&editor.display_snapshot(cx));
26481
26482 // Should have a selection and it should be different
26483 assert_eq!(
26484 prev_selection.len(),
26485 1,
26486 "Should have one selection after prev"
26487 );
26488 assert_ne!(
26489 prev_selection[0], function_next_selection[0],
26490 "Previous sibling selection should be different from next"
26491 );
26492 });
26493}
26494
26495#[gpui::test]
26496async fn test_next_prev_document_highlight(cx: &mut TestAppContext) {
26497 init_test(cx, |_| {});
26498
26499 let mut cx = EditorTestContext::new(cx).await;
26500 cx.set_state(
26501 "let ˇvariable = 42;
26502let another = variable + 1;
26503let result = variable * 2;",
26504 );
26505
26506 // Set up document highlights manually (simulating LSP response)
26507 cx.update_editor(|editor, _window, cx| {
26508 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
26509
26510 // Create highlights for "variable" occurrences
26511 let highlight_ranges = [
26512 Point::new(0, 4)..Point::new(0, 12), // First "variable"
26513 Point::new(1, 14)..Point::new(1, 22), // Second "variable"
26514 Point::new(2, 13)..Point::new(2, 21), // Third "variable"
26515 ];
26516
26517 let anchor_ranges: Vec<_> = highlight_ranges
26518 .iter()
26519 .map(|range| range.clone().to_anchors(&buffer_snapshot))
26520 .collect();
26521
26522 editor.highlight_background::<DocumentHighlightRead>(
26523 &anchor_ranges,
26524 |theme| theme.colors().editor_document_highlight_read_background,
26525 cx,
26526 );
26527 });
26528
26529 // Go to next highlight - should move to second "variable"
26530 cx.update_editor(|editor, window, cx| {
26531 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26532 });
26533 cx.assert_editor_state(
26534 "let variable = 42;
26535let another = ˇvariable + 1;
26536let result = variable * 2;",
26537 );
26538
26539 // Go to next highlight - should move to third "variable"
26540 cx.update_editor(|editor, window, cx| {
26541 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26542 });
26543 cx.assert_editor_state(
26544 "let variable = 42;
26545let another = variable + 1;
26546let result = ˇvariable * 2;",
26547 );
26548
26549 // Go to next highlight - should stay at third "variable" (no wrap-around)
26550 cx.update_editor(|editor, window, cx| {
26551 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26552 });
26553 cx.assert_editor_state(
26554 "let variable = 42;
26555let another = variable + 1;
26556let result = ˇvariable * 2;",
26557 );
26558
26559 // Now test going backwards from third position
26560 cx.update_editor(|editor, window, cx| {
26561 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26562 });
26563 cx.assert_editor_state(
26564 "let variable = 42;
26565let another = ˇvariable + 1;
26566let result = variable * 2;",
26567 );
26568
26569 // Go to previous highlight - should move to first "variable"
26570 cx.update_editor(|editor, window, cx| {
26571 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26572 });
26573 cx.assert_editor_state(
26574 "let ˇvariable = 42;
26575let another = variable + 1;
26576let result = variable * 2;",
26577 );
26578
26579 // Go to previous highlight - should stay on first "variable"
26580 cx.update_editor(|editor, window, cx| {
26581 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26582 });
26583 cx.assert_editor_state(
26584 "let ˇvariable = 42;
26585let another = variable + 1;
26586let result = variable * 2;",
26587 );
26588}
26589
26590#[gpui::test]
26591async fn test_paste_url_from_other_app_creates_markdown_link_over_selected_text(
26592 cx: &mut gpui::TestAppContext,
26593) {
26594 init_test(cx, |_| {});
26595
26596 let url = "https://zed.dev";
26597
26598 let markdown_language = Arc::new(Language::new(
26599 LanguageConfig {
26600 name: "Markdown".into(),
26601 ..LanguageConfig::default()
26602 },
26603 None,
26604 ));
26605
26606 let mut cx = EditorTestContext::new(cx).await;
26607 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26608 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)");
26609
26610 cx.update_editor(|editor, window, cx| {
26611 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26612 editor.paste(&Paste, window, cx);
26613 });
26614
26615 cx.assert_editor_state(&format!(
26616 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)"
26617 ));
26618}
26619
26620#[gpui::test]
26621async fn test_paste_url_from_zed_copy_creates_markdown_link_over_selected_text(
26622 cx: &mut gpui::TestAppContext,
26623) {
26624 init_test(cx, |_| {});
26625
26626 let url = "https://zed.dev";
26627
26628 let markdown_language = Arc::new(Language::new(
26629 LanguageConfig {
26630 name: "Markdown".into(),
26631 ..LanguageConfig::default()
26632 },
26633 None,
26634 ));
26635
26636 let mut cx = EditorTestContext::new(cx).await;
26637 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26638 cx.set_state(&format!(
26639 "Hello, editor.\nZed is great (see this link: )\n«{url}ˇ»"
26640 ));
26641
26642 cx.update_editor(|editor, window, cx| {
26643 editor.copy(&Copy, window, cx);
26644 });
26645
26646 cx.set_state(&format!(
26647 "Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)\n{url}"
26648 ));
26649
26650 cx.update_editor(|editor, window, cx| {
26651 editor.paste(&Paste, window, cx);
26652 });
26653
26654 cx.assert_editor_state(&format!(
26655 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)\n{url}"
26656 ));
26657}
26658
26659#[gpui::test]
26660async fn test_paste_url_from_other_app_replaces_existing_url_without_creating_markdown_link(
26661 cx: &mut gpui::TestAppContext,
26662) {
26663 init_test(cx, |_| {});
26664
26665 let url = "https://zed.dev";
26666
26667 let markdown_language = Arc::new(Language::new(
26668 LanguageConfig {
26669 name: "Markdown".into(),
26670 ..LanguageConfig::default()
26671 },
26672 None,
26673 ));
26674
26675 let mut cx = EditorTestContext::new(cx).await;
26676 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26677 cx.set_state("Please visit zed's homepage: «https://www.apple.comˇ»");
26678
26679 cx.update_editor(|editor, window, cx| {
26680 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26681 editor.paste(&Paste, window, cx);
26682 });
26683
26684 cx.assert_editor_state(&format!("Please visit zed's homepage: {url}ˇ"));
26685}
26686
26687#[gpui::test]
26688async fn test_paste_plain_text_from_other_app_replaces_selection_without_creating_markdown_link(
26689 cx: &mut gpui::TestAppContext,
26690) {
26691 init_test(cx, |_| {});
26692
26693 let text = "Awesome";
26694
26695 let markdown_language = Arc::new(Language::new(
26696 LanguageConfig {
26697 name: "Markdown".into(),
26698 ..LanguageConfig::default()
26699 },
26700 None,
26701 ));
26702
26703 let mut cx = EditorTestContext::new(cx).await;
26704 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26705 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat»");
26706
26707 cx.update_editor(|editor, window, cx| {
26708 cx.write_to_clipboard(ClipboardItem::new_string(text.to_string()));
26709 editor.paste(&Paste, window, cx);
26710 });
26711
26712 cx.assert_editor_state(&format!("Hello, {text}ˇ.\nZed is {text}ˇ"));
26713}
26714
26715#[gpui::test]
26716async fn test_paste_url_from_other_app_without_creating_markdown_link_in_non_markdown_language(
26717 cx: &mut gpui::TestAppContext,
26718) {
26719 init_test(cx, |_| {});
26720
26721 let url = "https://zed.dev";
26722
26723 let markdown_language = Arc::new(Language::new(
26724 LanguageConfig {
26725 name: "Rust".into(),
26726 ..LanguageConfig::default()
26727 },
26728 None,
26729 ));
26730
26731 let mut cx = EditorTestContext::new(cx).await;
26732 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26733 cx.set_state("// Hello, «editorˇ».\n// Zed is «ˇgreat» (see this link: ˇ)");
26734
26735 cx.update_editor(|editor, window, cx| {
26736 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26737 editor.paste(&Paste, window, cx);
26738 });
26739
26740 cx.assert_editor_state(&format!(
26741 "// Hello, {url}ˇ.\n// Zed is {url}ˇ (see this link: {url}ˇ)"
26742 ));
26743}
26744
26745#[gpui::test]
26746async fn test_paste_url_from_other_app_creates_markdown_link_selectively_in_multi_buffer(
26747 cx: &mut TestAppContext,
26748) {
26749 init_test(cx, |_| {});
26750
26751 let url = "https://zed.dev";
26752
26753 let markdown_language = Arc::new(Language::new(
26754 LanguageConfig {
26755 name: "Markdown".into(),
26756 ..LanguageConfig::default()
26757 },
26758 None,
26759 ));
26760
26761 let (editor, cx) = cx.add_window_view(|window, cx| {
26762 let multi_buffer = MultiBuffer::build_multi(
26763 [
26764 ("this will embed -> link", vec![Point::row_range(0..1)]),
26765 ("this will replace -> link", vec![Point::row_range(0..1)]),
26766 ],
26767 cx,
26768 );
26769 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
26770 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26771 s.select_ranges(vec![
26772 Point::new(0, 19)..Point::new(0, 23),
26773 Point::new(1, 21)..Point::new(1, 25),
26774 ])
26775 });
26776 let first_buffer_id = multi_buffer
26777 .read(cx)
26778 .excerpt_buffer_ids()
26779 .into_iter()
26780 .next()
26781 .unwrap();
26782 let first_buffer = multi_buffer.read(cx).buffer(first_buffer_id).unwrap();
26783 first_buffer.update(cx, |buffer, cx| {
26784 buffer.set_language(Some(markdown_language.clone()), cx);
26785 });
26786
26787 editor
26788 });
26789 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
26790
26791 cx.update_editor(|editor, window, cx| {
26792 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26793 editor.paste(&Paste, window, cx);
26794 });
26795
26796 cx.assert_editor_state(&format!(
26797 "this will embed -> [link]({url})ˇ\nthis will replace -> {url}ˇ"
26798 ));
26799}
26800
26801#[gpui::test]
26802async fn test_race_in_multibuffer_save(cx: &mut TestAppContext) {
26803 init_test(cx, |_| {});
26804
26805 let fs = FakeFs::new(cx.executor());
26806 fs.insert_tree(
26807 path!("/project"),
26808 json!({
26809 "first.rs": "# First Document\nSome content here.",
26810 "second.rs": "Plain text content for second file.",
26811 }),
26812 )
26813 .await;
26814
26815 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
26816 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
26817 let cx = &mut VisualTestContext::from_window(*workspace, cx);
26818
26819 let language = rust_lang();
26820 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
26821 language_registry.add(language.clone());
26822 let mut fake_servers = language_registry.register_fake_lsp(
26823 "Rust",
26824 FakeLspAdapter {
26825 ..FakeLspAdapter::default()
26826 },
26827 );
26828
26829 let buffer1 = project
26830 .update(cx, |project, cx| {
26831 project.open_local_buffer(PathBuf::from(path!("/project/first.rs")), cx)
26832 })
26833 .await
26834 .unwrap();
26835 let buffer2 = project
26836 .update(cx, |project, cx| {
26837 project.open_local_buffer(PathBuf::from(path!("/project/second.rs")), cx)
26838 })
26839 .await
26840 .unwrap();
26841
26842 let multi_buffer = cx.new(|cx| {
26843 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
26844 multi_buffer.set_excerpts_for_path(
26845 PathKey::for_buffer(&buffer1, cx),
26846 buffer1.clone(),
26847 [Point::zero()..buffer1.read(cx).max_point()],
26848 3,
26849 cx,
26850 );
26851 multi_buffer.set_excerpts_for_path(
26852 PathKey::for_buffer(&buffer2, cx),
26853 buffer2.clone(),
26854 [Point::zero()..buffer1.read(cx).max_point()],
26855 3,
26856 cx,
26857 );
26858 multi_buffer
26859 });
26860
26861 let (editor, cx) = cx.add_window_view(|window, cx| {
26862 Editor::new(
26863 EditorMode::full(),
26864 multi_buffer,
26865 Some(project.clone()),
26866 window,
26867 cx,
26868 )
26869 });
26870
26871 let fake_language_server = fake_servers.next().await.unwrap();
26872
26873 buffer1.update(cx, |buffer, cx| buffer.edit([(0..0, "hello!")], None, cx));
26874
26875 let save = editor.update_in(cx, |editor, window, cx| {
26876 assert!(editor.is_dirty(cx));
26877
26878 editor.save(
26879 SaveOptions {
26880 format: true,
26881 autosave: true,
26882 },
26883 project,
26884 window,
26885 cx,
26886 )
26887 });
26888 let (start_edit_tx, start_edit_rx) = oneshot::channel();
26889 let (done_edit_tx, done_edit_rx) = oneshot::channel();
26890 let mut done_edit_rx = Some(done_edit_rx);
26891 let mut start_edit_tx = Some(start_edit_tx);
26892
26893 fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| {
26894 start_edit_tx.take().unwrap().send(()).unwrap();
26895 let done_edit_rx = done_edit_rx.take().unwrap();
26896 async move {
26897 done_edit_rx.await.unwrap();
26898 Ok(None)
26899 }
26900 });
26901
26902 start_edit_rx.await.unwrap();
26903 buffer2
26904 .update(cx, |buffer, cx| buffer.edit([(0..0, "world!")], None, cx))
26905 .unwrap();
26906
26907 done_edit_tx.send(()).unwrap();
26908
26909 save.await.unwrap();
26910 cx.update(|_, cx| assert!(editor.is_dirty(cx)));
26911}
26912
26913#[track_caller]
26914fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
26915 editor
26916 .all_inlays(cx)
26917 .into_iter()
26918 .filter_map(|inlay| inlay.get_color())
26919 .map(Rgba::from)
26920 .collect()
26921}
26922
26923#[gpui::test]
26924fn test_duplicate_line_up_on_last_line_without_newline(cx: &mut TestAppContext) {
26925 init_test(cx, |_| {});
26926
26927 let editor = cx.add_window(|window, cx| {
26928 let buffer = MultiBuffer::build_simple("line1\nline2", cx);
26929 build_editor(buffer, window, cx)
26930 });
26931
26932 editor
26933 .update(cx, |editor, window, cx| {
26934 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26935 s.select_display_ranges([
26936 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
26937 ])
26938 });
26939
26940 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
26941
26942 assert_eq!(
26943 editor.display_text(cx),
26944 "line1\nline2\nline2",
26945 "Duplicating last line upward should create duplicate above, not on same line"
26946 );
26947
26948 assert_eq!(
26949 editor
26950 .selections
26951 .display_ranges(&editor.display_snapshot(cx)),
26952 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)],
26953 "Selection should move to the duplicated line"
26954 );
26955 })
26956 .unwrap();
26957}
26958
26959#[gpui::test]
26960async fn test_copy_line_without_trailing_newline(cx: &mut TestAppContext) {
26961 init_test(cx, |_| {});
26962
26963 let mut cx = EditorTestContext::new(cx).await;
26964
26965 cx.set_state("line1\nline2ˇ");
26966
26967 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
26968
26969 let clipboard_text = cx
26970 .read_from_clipboard()
26971 .and_then(|item| item.text().as_deref().map(str::to_string));
26972
26973 assert_eq!(
26974 clipboard_text,
26975 Some("line2\n".to_string()),
26976 "Copying a line without trailing newline should include a newline"
26977 );
26978
26979 cx.set_state("line1\nˇ");
26980
26981 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
26982
26983 cx.assert_editor_state("line1\nline2\nˇ");
26984}
26985
26986#[gpui::test]
26987async fn test_end_of_editor_context(cx: &mut TestAppContext) {
26988 init_test(cx, |_| {});
26989
26990 let mut cx = EditorTestContext::new(cx).await;
26991
26992 cx.set_state("line1\nline2ˇ");
26993 cx.update_editor(|e, window, cx| {
26994 e.set_mode(EditorMode::SingleLine);
26995 assert!(e.key_context(window, cx).contains("end_of_input"));
26996 });
26997 cx.set_state("ˇline1\nline2");
26998 cx.update_editor(|e, window, cx| {
26999 assert!(!e.key_context(window, cx).contains("end_of_input"));
27000 });
27001 cx.set_state("line1ˇ\nline2");
27002 cx.update_editor(|e, window, cx| {
27003 assert!(!e.key_context(window, cx).contains("end_of_input"));
27004 });
27005}
27006
27007#[gpui::test]
27008async fn test_sticky_scroll(cx: &mut TestAppContext) {
27009 init_test(cx, |_| {});
27010 let mut cx = EditorTestContext::new(cx).await;
27011
27012 let buffer = indoc! {"
27013 ˇfn foo() {
27014 let abc = 123;
27015 }
27016 struct Bar;
27017 impl Bar {
27018 fn new() -> Self {
27019 Self
27020 }
27021 }
27022 fn baz() {
27023 }
27024 "};
27025 cx.set_state(&buffer);
27026
27027 cx.update_editor(|e, _, cx| {
27028 e.buffer()
27029 .read(cx)
27030 .as_singleton()
27031 .unwrap()
27032 .update(cx, |buffer, cx| {
27033 buffer.set_language(Some(rust_lang()), cx);
27034 })
27035 });
27036
27037 let mut sticky_headers = |offset: ScrollOffset| {
27038 cx.update_editor(|e, window, cx| {
27039 e.scroll(gpui::Point { x: 0., y: offset }, None, window, cx);
27040 EditorElement::sticky_headers(&e, &e.snapshot(window, cx), cx)
27041 .into_iter()
27042 .map(
27043 |StickyHeader {
27044 start_point,
27045 offset,
27046 ..
27047 }| { (start_point, offset) },
27048 )
27049 .collect::<Vec<_>>()
27050 })
27051 };
27052
27053 let fn_foo = Point { row: 0, column: 0 };
27054 let impl_bar = Point { row: 4, column: 0 };
27055 let fn_new = Point { row: 5, column: 4 };
27056
27057 assert_eq!(sticky_headers(0.0), vec![]);
27058 assert_eq!(sticky_headers(0.5), vec![(fn_foo, 0.0)]);
27059 assert_eq!(sticky_headers(1.0), vec![(fn_foo, 0.0)]);
27060 assert_eq!(sticky_headers(1.5), vec![(fn_foo, -0.5)]);
27061 assert_eq!(sticky_headers(2.0), vec![]);
27062 assert_eq!(sticky_headers(2.5), vec![]);
27063 assert_eq!(sticky_headers(3.0), vec![]);
27064 assert_eq!(sticky_headers(3.5), vec![]);
27065 assert_eq!(sticky_headers(4.0), vec![]);
27066 assert_eq!(sticky_headers(4.5), vec![(impl_bar, 0.0), (fn_new, 1.0)]);
27067 assert_eq!(sticky_headers(5.0), vec![(impl_bar, 0.0), (fn_new, 1.0)]);
27068 assert_eq!(sticky_headers(5.5), vec![(impl_bar, 0.0), (fn_new, 0.5)]);
27069 assert_eq!(sticky_headers(6.0), vec![(impl_bar, 0.0)]);
27070 assert_eq!(sticky_headers(6.5), vec![(impl_bar, 0.0)]);
27071 assert_eq!(sticky_headers(7.0), vec![(impl_bar, 0.0)]);
27072 assert_eq!(sticky_headers(7.5), vec![(impl_bar, -0.5)]);
27073 assert_eq!(sticky_headers(8.0), vec![]);
27074 assert_eq!(sticky_headers(8.5), vec![]);
27075 assert_eq!(sticky_headers(9.0), vec![]);
27076 assert_eq!(sticky_headers(9.5), vec![]);
27077 assert_eq!(sticky_headers(10.0), vec![]);
27078}
27079
27080#[gpui::test]
27081async fn test_scroll_by_clicking_sticky_header(cx: &mut TestAppContext) {
27082 init_test(cx, |_| {});
27083 cx.update(|cx| {
27084 SettingsStore::update_global(cx, |store, cx| {
27085 store.update_user_settings(cx, |settings| {
27086 settings.editor.sticky_scroll = Some(settings::StickyScrollContent {
27087 enabled: Some(true),
27088 })
27089 });
27090 });
27091 });
27092 let mut cx = EditorTestContext::new(cx).await;
27093
27094 let line_height = cx.editor(|editor, window, _cx| {
27095 editor
27096 .style()
27097 .unwrap()
27098 .text
27099 .line_height_in_pixels(window.rem_size())
27100 });
27101
27102 let buffer = indoc! {"
27103 ˇfn foo() {
27104 let abc = 123;
27105 }
27106 struct Bar;
27107 impl Bar {
27108 fn new() -> Self {
27109 Self
27110 }
27111 }
27112 fn baz() {
27113 }
27114 "};
27115 cx.set_state(&buffer);
27116
27117 cx.update_editor(|e, _, cx| {
27118 e.buffer()
27119 .read(cx)
27120 .as_singleton()
27121 .unwrap()
27122 .update(cx, |buffer, cx| {
27123 buffer.set_language(Some(rust_lang()), cx);
27124 })
27125 });
27126
27127 let fn_foo = || empty_range(0, 0);
27128 let impl_bar = || empty_range(4, 0);
27129 let fn_new = || empty_range(5, 4);
27130
27131 let mut scroll_and_click = |scroll_offset: ScrollOffset, click_offset: ScrollOffset| {
27132 cx.update_editor(|e, window, cx| {
27133 e.scroll(
27134 gpui::Point {
27135 x: 0.,
27136 y: scroll_offset,
27137 },
27138 None,
27139 window,
27140 cx,
27141 );
27142 });
27143 cx.simulate_click(
27144 gpui::Point {
27145 x: px(0.),
27146 y: click_offset as f32 * line_height,
27147 },
27148 Modifiers::none(),
27149 );
27150 cx.update_editor(|e, _, cx| (e.scroll_position(cx), display_ranges(e, cx)))
27151 };
27152
27153 assert_eq!(
27154 scroll_and_click(
27155 4.5, // impl Bar is halfway off the screen
27156 0.0 // click top of screen
27157 ),
27158 // scrolled to impl Bar
27159 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
27160 );
27161
27162 assert_eq!(
27163 scroll_and_click(
27164 4.5, // impl Bar is halfway off the screen
27165 0.25 // click middle of impl Bar
27166 ),
27167 // scrolled to impl Bar
27168 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
27169 );
27170
27171 assert_eq!(
27172 scroll_and_click(
27173 4.5, // impl Bar is halfway off the screen
27174 1.5 // click below impl Bar (e.g. fn new())
27175 ),
27176 // scrolled to fn new() - this is below the impl Bar header which has persisted
27177 (gpui::Point { x: 0., y: 4. }, vec![fn_new()])
27178 );
27179
27180 assert_eq!(
27181 scroll_and_click(
27182 5.5, // fn new is halfway underneath impl Bar
27183 0.75 // click on the overlap of impl Bar and fn new()
27184 ),
27185 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
27186 );
27187
27188 assert_eq!(
27189 scroll_and_click(
27190 5.5, // fn new is halfway underneath impl Bar
27191 1.25 // click on the visible part of fn new()
27192 ),
27193 (gpui::Point { x: 0., y: 4. }, vec![fn_new()])
27194 );
27195
27196 assert_eq!(
27197 scroll_and_click(
27198 1.5, // fn foo is halfway off the screen
27199 0.0 // click top of screen
27200 ),
27201 (gpui::Point { x: 0., y: 0. }, vec![fn_foo()])
27202 );
27203
27204 assert_eq!(
27205 scroll_and_click(
27206 1.5, // fn foo is halfway off the screen
27207 0.75 // click visible part of let abc...
27208 )
27209 .0,
27210 // no change in scroll
27211 // we don't assert on the visible_range because if we clicked the gutter, our line is fully selected
27212 (gpui::Point { x: 0., y: 1.5 })
27213 );
27214}
27215
27216#[gpui::test]
27217async fn test_next_prev_reference(cx: &mut TestAppContext) {
27218 const CYCLE_POSITIONS: &[&'static str] = &[
27219 indoc! {"
27220 fn foo() {
27221 let ˇabc = 123;
27222 let x = abc + 1;
27223 let y = abc + 2;
27224 let z = abc + 2;
27225 }
27226 "},
27227 indoc! {"
27228 fn foo() {
27229 let abc = 123;
27230 let x = ˇabc + 1;
27231 let y = abc + 2;
27232 let z = abc + 2;
27233 }
27234 "},
27235 indoc! {"
27236 fn foo() {
27237 let abc = 123;
27238 let x = abc + 1;
27239 let y = ˇabc + 2;
27240 let z = abc + 2;
27241 }
27242 "},
27243 indoc! {"
27244 fn foo() {
27245 let abc = 123;
27246 let x = abc + 1;
27247 let y = abc + 2;
27248 let z = ˇabc + 2;
27249 }
27250 "},
27251 ];
27252
27253 init_test(cx, |_| {});
27254
27255 let mut cx = EditorLspTestContext::new_rust(
27256 lsp::ServerCapabilities {
27257 references_provider: Some(lsp::OneOf::Left(true)),
27258 ..Default::default()
27259 },
27260 cx,
27261 )
27262 .await;
27263
27264 // importantly, the cursor is in the middle
27265 cx.set_state(indoc! {"
27266 fn foo() {
27267 let aˇbc = 123;
27268 let x = abc + 1;
27269 let y = abc + 2;
27270 let z = abc + 2;
27271 }
27272 "});
27273
27274 let reference_ranges = [
27275 lsp::Position::new(1, 8),
27276 lsp::Position::new(2, 12),
27277 lsp::Position::new(3, 12),
27278 lsp::Position::new(4, 12),
27279 ]
27280 .map(|start| lsp::Range::new(start, lsp::Position::new(start.line, start.character + 3)));
27281
27282 cx.lsp
27283 .set_request_handler::<lsp::request::References, _, _>(move |params, _cx| async move {
27284 Ok(Some(
27285 reference_ranges
27286 .map(|range| lsp::Location {
27287 uri: params.text_document_position.text_document.uri.clone(),
27288 range,
27289 })
27290 .to_vec(),
27291 ))
27292 });
27293
27294 let _move = async |direction, count, cx: &mut EditorLspTestContext| {
27295 cx.update_editor(|editor, window, cx| {
27296 editor.go_to_reference_before_or_after_position(direction, count, window, cx)
27297 })
27298 .unwrap()
27299 .await
27300 .unwrap()
27301 };
27302
27303 _move(Direction::Next, 1, &mut cx).await;
27304 cx.assert_editor_state(CYCLE_POSITIONS[1]);
27305
27306 _move(Direction::Next, 1, &mut cx).await;
27307 cx.assert_editor_state(CYCLE_POSITIONS[2]);
27308
27309 _move(Direction::Next, 1, &mut cx).await;
27310 cx.assert_editor_state(CYCLE_POSITIONS[3]);
27311
27312 // loops back to the start
27313 _move(Direction::Next, 1, &mut cx).await;
27314 cx.assert_editor_state(CYCLE_POSITIONS[0]);
27315
27316 // loops back to the end
27317 _move(Direction::Prev, 1, &mut cx).await;
27318 cx.assert_editor_state(CYCLE_POSITIONS[3]);
27319
27320 _move(Direction::Prev, 1, &mut cx).await;
27321 cx.assert_editor_state(CYCLE_POSITIONS[2]);
27322
27323 _move(Direction::Prev, 1, &mut cx).await;
27324 cx.assert_editor_state(CYCLE_POSITIONS[1]);
27325
27326 _move(Direction::Prev, 1, &mut cx).await;
27327 cx.assert_editor_state(CYCLE_POSITIONS[0]);
27328
27329 _move(Direction::Next, 3, &mut cx).await;
27330 cx.assert_editor_state(CYCLE_POSITIONS[3]);
27331
27332 _move(Direction::Prev, 2, &mut cx).await;
27333 cx.assert_editor_state(CYCLE_POSITIONS[1]);
27334}