1use super::*;
2use crate::{
3 JoinLines,
4 code_context_menus::CodeContextMenu,
5 edit_prediction_tests::FakeEditPredictionProvider,
6 linked_editing_ranges::LinkedEditingRanges,
7 scroll::scroll_amount::ScrollAmount,
8 test::{
9 assert_text_with_selections, build_editor,
10 editor_lsp_test_context::{EditorLspTestContext, git_commit_lang},
11 editor_test_context::EditorTestContext,
12 select_ranges,
13 },
14};
15use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
16use futures::StreamExt;
17use gpui::{
18 BackgroundExecutor, DismissEvent, Rgba, SemanticVersion, TestAppContext, UpdateGlobal,
19 VisualTestContext, WindowBounds, WindowOptions, div,
20};
21use indoc::indoc;
22use language::{
23 BracketPairConfig,
24 Capability::ReadWrite,
25 DiagnosticSourceKind, FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher,
26 LanguageName, Override, Point,
27 language_settings::{
28 AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings, FormatterList,
29 LanguageSettingsContent, LspInsertMode, PrettierSettings, SelectedFormatter,
30 },
31 tree_sitter_python,
32};
33use language_settings::{Formatter, IndentGuideSettings};
34use lsp::CompletionParams;
35use multi_buffer::{IndentGuide, PathKey};
36use parking_lot::Mutex;
37use pretty_assertions::{assert_eq, assert_ne};
38use project::{
39 FakeFs,
40 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
41 project_settings::{LspSettings, ProjectSettings},
42};
43use serde_json::{self, json};
44use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
45use std::{
46 iter,
47 sync::atomic::{self, AtomicUsize},
48};
49use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
50use text::ToPoint as _;
51use unindent::Unindent;
52use util::{
53 assert_set_eq, path,
54 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
55 uri,
56};
57use workspace::{
58 CloseActiveItem, CloseAllItems, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry,
59 OpenOptions, ViewId,
60 invalid_buffer_view::InvalidBufferView,
61 item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
62 register_project_item,
63};
64
65#[gpui::test]
66fn test_edit_events(cx: &mut TestAppContext) {
67 init_test(cx, |_| {});
68
69 let buffer = cx.new(|cx| {
70 let mut buffer = language::Buffer::local("123456", cx);
71 buffer.set_group_interval(Duration::from_secs(1));
72 buffer
73 });
74
75 let events = Rc::new(RefCell::new(Vec::new()));
76 let editor1 = cx.add_window({
77 let events = events.clone();
78 |window, cx| {
79 let entity = cx.entity();
80 cx.subscribe_in(
81 &entity,
82 window,
83 move |_, _, event: &EditorEvent, _, _| match event {
84 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
85 EditorEvent::BufferEdited => {
86 events.borrow_mut().push(("editor1", "buffer edited"))
87 }
88 _ => {}
89 },
90 )
91 .detach();
92 Editor::for_buffer(buffer.clone(), None, window, cx)
93 }
94 });
95
96 let editor2 = cx.add_window({
97 let events = events.clone();
98 |window, cx| {
99 cx.subscribe_in(
100 &cx.entity(),
101 window,
102 move |_, _, event: &EditorEvent, _, _| match event {
103 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
104 EditorEvent::BufferEdited => {
105 events.borrow_mut().push(("editor2", "buffer edited"))
106 }
107 _ => {}
108 },
109 )
110 .detach();
111 Editor::for_buffer(buffer.clone(), None, window, cx)
112 }
113 });
114
115 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
116
117 // Mutating editor 1 will emit an `Edited` event only for that editor.
118 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
119 assert_eq!(
120 mem::take(&mut *events.borrow_mut()),
121 [
122 ("editor1", "edited"),
123 ("editor1", "buffer edited"),
124 ("editor2", "buffer edited"),
125 ]
126 );
127
128 // Mutating editor 2 will emit an `Edited` event only for that editor.
129 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
130 assert_eq!(
131 mem::take(&mut *events.borrow_mut()),
132 [
133 ("editor2", "edited"),
134 ("editor1", "buffer edited"),
135 ("editor2", "buffer edited"),
136 ]
137 );
138
139 // Undoing on editor 1 will emit an `Edited` event only for that editor.
140 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
141 assert_eq!(
142 mem::take(&mut *events.borrow_mut()),
143 [
144 ("editor1", "edited"),
145 ("editor1", "buffer edited"),
146 ("editor2", "buffer edited"),
147 ]
148 );
149
150 // Redoing on editor 1 will emit an `Edited` event only for that editor.
151 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
152 assert_eq!(
153 mem::take(&mut *events.borrow_mut()),
154 [
155 ("editor1", "edited"),
156 ("editor1", "buffer edited"),
157 ("editor2", "buffer edited"),
158 ]
159 );
160
161 // Undoing on editor 2 will emit an `Edited` event only for that editor.
162 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
163 assert_eq!(
164 mem::take(&mut *events.borrow_mut()),
165 [
166 ("editor2", "edited"),
167 ("editor1", "buffer edited"),
168 ("editor2", "buffer edited"),
169 ]
170 );
171
172 // Redoing on editor 2 will emit an `Edited` event only for that editor.
173 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
174 assert_eq!(
175 mem::take(&mut *events.borrow_mut()),
176 [
177 ("editor2", "edited"),
178 ("editor1", "buffer edited"),
179 ("editor2", "buffer edited"),
180 ]
181 );
182
183 // No event is emitted when the mutation is a no-op.
184 _ = editor2.update(cx, |editor, window, cx| {
185 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
186 s.select_ranges([0..0])
187 });
188
189 editor.backspace(&Backspace, window, cx);
190 });
191 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
192}
193
194#[gpui::test]
195fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
196 init_test(cx, |_| {});
197
198 let mut now = Instant::now();
199 let group_interval = Duration::from_millis(1);
200 let buffer = cx.new(|cx| {
201 let mut buf = language::Buffer::local("123456", cx);
202 buf.set_group_interval(group_interval);
203 buf
204 });
205 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
206 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
207
208 _ = editor.update(cx, |editor, window, cx| {
209 editor.start_transaction_at(now, window, cx);
210 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
211 s.select_ranges([2..4])
212 });
213
214 editor.insert("cd", window, cx);
215 editor.end_transaction_at(now, cx);
216 assert_eq!(editor.text(cx), "12cd56");
217 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
218
219 editor.start_transaction_at(now, window, cx);
220 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
221 s.select_ranges([4..5])
222 });
223 editor.insert("e", window, cx);
224 editor.end_transaction_at(now, cx);
225 assert_eq!(editor.text(cx), "12cde6");
226 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
227
228 now += group_interval + Duration::from_millis(1);
229 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
230 s.select_ranges([2..2])
231 });
232
233 // Simulate an edit in another editor
234 buffer.update(cx, |buffer, cx| {
235 buffer.start_transaction_at(now, cx);
236 buffer.edit([(0..1, "a")], None, cx);
237 buffer.edit([(1..1, "b")], None, cx);
238 buffer.end_transaction_at(now, cx);
239 });
240
241 assert_eq!(editor.text(cx), "ab2cde6");
242 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
243
244 // Last transaction happened past the group interval in a different editor.
245 // Undo it individually and don't restore selections.
246 editor.undo(&Undo, window, cx);
247 assert_eq!(editor.text(cx), "12cde6");
248 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
249
250 // First two transactions happened within the group interval in this editor.
251 // Undo them together and restore selections.
252 editor.undo(&Undo, window, cx);
253 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
254 assert_eq!(editor.text(cx), "123456");
255 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
256
257 // Redo the first two transactions together.
258 editor.redo(&Redo, window, cx);
259 assert_eq!(editor.text(cx), "12cde6");
260 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
261
262 // Redo the last transaction on its own.
263 editor.redo(&Redo, window, cx);
264 assert_eq!(editor.text(cx), "ab2cde6");
265 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
266
267 // Test empty transactions.
268 editor.start_transaction_at(now, window, cx);
269 editor.end_transaction_at(now, cx);
270 editor.undo(&Undo, window, cx);
271 assert_eq!(editor.text(cx), "12cde6");
272 });
273}
274
275#[gpui::test]
276fn test_ime_composition(cx: &mut TestAppContext) {
277 init_test(cx, |_| {});
278
279 let buffer = cx.new(|cx| {
280 let mut buffer = language::Buffer::local("abcde", cx);
281 // Ensure automatic grouping doesn't occur.
282 buffer.set_group_interval(Duration::ZERO);
283 buffer
284 });
285
286 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
287 cx.add_window(|window, cx| {
288 let mut editor = build_editor(buffer.clone(), window, cx);
289
290 // Start a new IME composition.
291 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
292 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
293 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
294 assert_eq!(editor.text(cx), "äbcde");
295 assert_eq!(
296 editor.marked_text_ranges(cx),
297 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
298 );
299
300 // Finalize IME composition.
301 editor.replace_text_in_range(None, "ā", window, cx);
302 assert_eq!(editor.text(cx), "ābcde");
303 assert_eq!(editor.marked_text_ranges(cx), None);
304
305 // IME composition edits are grouped and are undone/redone at once.
306 editor.undo(&Default::default(), window, cx);
307 assert_eq!(editor.text(cx), "abcde");
308 assert_eq!(editor.marked_text_ranges(cx), None);
309 editor.redo(&Default::default(), window, cx);
310 assert_eq!(editor.text(cx), "ābcde");
311 assert_eq!(editor.marked_text_ranges(cx), None);
312
313 // Start a new IME composition.
314 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
315 assert_eq!(
316 editor.marked_text_ranges(cx),
317 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
318 );
319
320 // Undoing during an IME composition cancels it.
321 editor.undo(&Default::default(), window, cx);
322 assert_eq!(editor.text(cx), "ābcde");
323 assert_eq!(editor.marked_text_ranges(cx), None);
324
325 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
326 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
327 assert_eq!(editor.text(cx), "ābcdè");
328 assert_eq!(
329 editor.marked_text_ranges(cx),
330 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
331 );
332
333 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
334 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
335 assert_eq!(editor.text(cx), "ābcdę");
336 assert_eq!(editor.marked_text_ranges(cx), None);
337
338 // Start a new IME composition with multiple cursors.
339 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
340 s.select_ranges([
341 OffsetUtf16(1)..OffsetUtf16(1),
342 OffsetUtf16(3)..OffsetUtf16(3),
343 OffsetUtf16(5)..OffsetUtf16(5),
344 ])
345 });
346 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
347 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
348 assert_eq!(
349 editor.marked_text_ranges(cx),
350 Some(vec![
351 OffsetUtf16(0)..OffsetUtf16(3),
352 OffsetUtf16(4)..OffsetUtf16(7),
353 OffsetUtf16(8)..OffsetUtf16(11)
354 ])
355 );
356
357 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
358 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
359 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
360 assert_eq!(
361 editor.marked_text_ranges(cx),
362 Some(vec![
363 OffsetUtf16(1)..OffsetUtf16(2),
364 OffsetUtf16(5)..OffsetUtf16(6),
365 OffsetUtf16(9)..OffsetUtf16(10)
366 ])
367 );
368
369 // Finalize IME composition with multiple cursors.
370 editor.replace_text_in_range(Some(9..10), "2", window, cx);
371 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
372 assert_eq!(editor.marked_text_ranges(cx), None);
373
374 editor
375 });
376}
377
378#[gpui::test]
379fn test_selection_with_mouse(cx: &mut TestAppContext) {
380 init_test(cx, |_| {});
381
382 let editor = cx.add_window(|window, cx| {
383 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
384 build_editor(buffer, window, cx)
385 });
386
387 _ = editor.update(cx, |editor, window, cx| {
388 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
389 });
390 assert_eq!(
391 editor
392 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
393 .unwrap(),
394 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
395 );
396
397 _ = editor.update(cx, |editor, window, cx| {
398 editor.update_selection(
399 DisplayPoint::new(DisplayRow(3), 3),
400 0,
401 gpui::Point::<f32>::default(),
402 window,
403 cx,
404 );
405 });
406
407 assert_eq!(
408 editor
409 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
410 .unwrap(),
411 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
412 );
413
414 _ = editor.update(cx, |editor, window, cx| {
415 editor.update_selection(
416 DisplayPoint::new(DisplayRow(1), 1),
417 0,
418 gpui::Point::<f32>::default(),
419 window,
420 cx,
421 );
422 });
423
424 assert_eq!(
425 editor
426 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
427 .unwrap(),
428 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
429 );
430
431 _ = editor.update(cx, |editor, window, cx| {
432 editor.end_selection(window, cx);
433 editor.update_selection(
434 DisplayPoint::new(DisplayRow(3), 3),
435 0,
436 gpui::Point::<f32>::default(),
437 window,
438 cx,
439 );
440 });
441
442 assert_eq!(
443 editor
444 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
445 .unwrap(),
446 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
447 );
448
449 _ = editor.update(cx, |editor, window, cx| {
450 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
451 editor.update_selection(
452 DisplayPoint::new(DisplayRow(0), 0),
453 0,
454 gpui::Point::<f32>::default(),
455 window,
456 cx,
457 );
458 });
459
460 assert_eq!(
461 editor
462 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
463 .unwrap(),
464 [
465 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
466 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
467 ]
468 );
469
470 _ = editor.update(cx, |editor, window, cx| {
471 editor.end_selection(window, cx);
472 });
473
474 assert_eq!(
475 editor
476 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
477 .unwrap(),
478 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
479 );
480}
481
482#[gpui::test]
483fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
484 init_test(cx, |_| {});
485
486 let editor = cx.add_window(|window, cx| {
487 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
488 build_editor(buffer, window, cx)
489 });
490
491 _ = editor.update(cx, |editor, window, cx| {
492 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
493 });
494
495 _ = editor.update(cx, |editor, window, cx| {
496 editor.end_selection(window, cx);
497 });
498
499 _ = editor.update(cx, |editor, window, cx| {
500 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
501 });
502
503 _ = editor.update(cx, |editor, window, cx| {
504 editor.end_selection(window, cx);
505 });
506
507 assert_eq!(
508 editor
509 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
510 .unwrap(),
511 [
512 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
513 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
514 ]
515 );
516
517 _ = editor.update(cx, |editor, window, cx| {
518 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
519 });
520
521 _ = editor.update(cx, |editor, window, cx| {
522 editor.end_selection(window, cx);
523 });
524
525 assert_eq!(
526 editor
527 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
528 .unwrap(),
529 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
530 );
531}
532
533#[gpui::test]
534fn test_canceling_pending_selection(cx: &mut TestAppContext) {
535 init_test(cx, |_| {});
536
537 let editor = cx.add_window(|window, cx| {
538 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
539 build_editor(buffer, window, cx)
540 });
541
542 _ = editor.update(cx, |editor, window, cx| {
543 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
544 assert_eq!(
545 editor.selections.display_ranges(cx),
546 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
547 );
548 });
549
550 _ = editor.update(cx, |editor, window, cx| {
551 editor.update_selection(
552 DisplayPoint::new(DisplayRow(3), 3),
553 0,
554 gpui::Point::<f32>::default(),
555 window,
556 cx,
557 );
558 assert_eq!(
559 editor.selections.display_ranges(cx),
560 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
561 );
562 });
563
564 _ = editor.update(cx, |editor, window, cx| {
565 editor.cancel(&Cancel, window, cx);
566 editor.update_selection(
567 DisplayPoint::new(DisplayRow(1), 1),
568 0,
569 gpui::Point::<f32>::default(),
570 window,
571 cx,
572 );
573 assert_eq!(
574 editor.selections.display_ranges(cx),
575 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
576 );
577 });
578}
579
580#[gpui::test]
581fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
582 init_test(cx, |_| {});
583
584 let editor = cx.add_window(|window, cx| {
585 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
586 build_editor(buffer, window, cx)
587 });
588
589 _ = editor.update(cx, |editor, window, cx| {
590 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
591 assert_eq!(
592 editor.selections.display_ranges(cx),
593 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
594 );
595
596 editor.move_down(&Default::default(), window, cx);
597 assert_eq!(
598 editor.selections.display_ranges(cx),
599 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
600 );
601
602 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
603 assert_eq!(
604 editor.selections.display_ranges(cx),
605 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
606 );
607
608 editor.move_up(&Default::default(), window, cx);
609 assert_eq!(
610 editor.selections.display_ranges(cx),
611 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
612 );
613 });
614}
615
616#[gpui::test]
617fn test_clone(cx: &mut TestAppContext) {
618 init_test(cx, |_| {});
619
620 let (text, selection_ranges) = marked_text_ranges(
621 indoc! {"
622 one
623 two
624 threeˇ
625 four
626 fiveˇ
627 "},
628 true,
629 );
630
631 let editor = cx.add_window(|window, cx| {
632 let buffer = MultiBuffer::build_simple(&text, cx);
633 build_editor(buffer, window, cx)
634 });
635
636 _ = editor.update(cx, |editor, window, cx| {
637 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
638 s.select_ranges(selection_ranges.clone())
639 });
640 editor.fold_creases(
641 vec![
642 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
643 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
644 ],
645 true,
646 window,
647 cx,
648 );
649 });
650
651 let cloned_editor = editor
652 .update(cx, |editor, _, cx| {
653 cx.open_window(Default::default(), |window, cx| {
654 cx.new(|cx| editor.clone(window, cx))
655 })
656 })
657 .unwrap()
658 .unwrap();
659
660 let snapshot = editor
661 .update(cx, |e, window, cx| e.snapshot(window, cx))
662 .unwrap();
663 let cloned_snapshot = cloned_editor
664 .update(cx, |e, window, cx| e.snapshot(window, cx))
665 .unwrap();
666
667 assert_eq!(
668 cloned_editor
669 .update(cx, |e, _, cx| e.display_text(cx))
670 .unwrap(),
671 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
672 );
673 assert_eq!(
674 cloned_snapshot
675 .folds_in_range(0..text.len())
676 .collect::<Vec<_>>(),
677 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
678 );
679 assert_set_eq!(
680 cloned_editor
681 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
682 .unwrap(),
683 editor
684 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
685 .unwrap()
686 );
687 assert_set_eq!(
688 cloned_editor
689 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
690 .unwrap(),
691 editor
692 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
693 .unwrap()
694 );
695}
696
697#[gpui::test]
698async fn test_navigation_history(cx: &mut TestAppContext) {
699 init_test(cx, |_| {});
700
701 use workspace::item::Item;
702
703 let fs = FakeFs::new(cx.executor());
704 let project = Project::test(fs, [], cx).await;
705 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
706 let pane = workspace
707 .update(cx, |workspace, _, _| workspace.active_pane().clone())
708 .unwrap();
709
710 _ = workspace.update(cx, |_v, window, cx| {
711 cx.new(|cx| {
712 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
713 let mut editor = build_editor(buffer, window, cx);
714 let handle = cx.entity();
715 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
716
717 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
718 editor.nav_history.as_mut().unwrap().pop_backward(cx)
719 }
720
721 // Move the cursor a small distance.
722 // Nothing is added to the navigation history.
723 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
724 s.select_display_ranges([
725 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
726 ])
727 });
728 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
729 s.select_display_ranges([
730 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
731 ])
732 });
733 assert!(pop_history(&mut editor, cx).is_none());
734
735 // Move the cursor a large distance.
736 // The history can jump back to the previous position.
737 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
738 s.select_display_ranges([
739 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
740 ])
741 });
742 let nav_entry = pop_history(&mut editor, cx).unwrap();
743 editor.navigate(nav_entry.data.unwrap(), window, cx);
744 assert_eq!(nav_entry.item.id(), cx.entity_id());
745 assert_eq!(
746 editor.selections.display_ranges(cx),
747 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
748 );
749 assert!(pop_history(&mut editor, cx).is_none());
750
751 // Move the cursor a small distance via the mouse.
752 // Nothing is added to the navigation history.
753 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
754 editor.end_selection(window, cx);
755 assert_eq!(
756 editor.selections.display_ranges(cx),
757 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
758 );
759 assert!(pop_history(&mut editor, cx).is_none());
760
761 // Move the cursor a large distance via the mouse.
762 // The history can jump back to the previous position.
763 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
764 editor.end_selection(window, cx);
765 assert_eq!(
766 editor.selections.display_ranges(cx),
767 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
768 );
769 let nav_entry = pop_history(&mut editor, cx).unwrap();
770 editor.navigate(nav_entry.data.unwrap(), window, cx);
771 assert_eq!(nav_entry.item.id(), cx.entity_id());
772 assert_eq!(
773 editor.selections.display_ranges(cx),
774 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
775 );
776 assert!(pop_history(&mut editor, cx).is_none());
777
778 // Set scroll position to check later
779 editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), window, cx);
780 let original_scroll_position = editor.scroll_manager.anchor();
781
782 // Jump to the end of the document and adjust scroll
783 editor.move_to_end(&MoveToEnd, window, cx);
784 editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), window, cx);
785 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
786
787 let nav_entry = pop_history(&mut editor, cx).unwrap();
788 editor.navigate(nav_entry.data.unwrap(), window, cx);
789 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
790
791 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
792 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
793 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
794 let invalid_point = Point::new(9999, 0);
795 editor.navigate(
796 Box::new(NavigationData {
797 cursor_anchor: invalid_anchor,
798 cursor_position: invalid_point,
799 scroll_anchor: ScrollAnchor {
800 anchor: invalid_anchor,
801 offset: Default::default(),
802 },
803 scroll_top_row: invalid_point.row,
804 }),
805 window,
806 cx,
807 );
808 assert_eq!(
809 editor.selections.display_ranges(cx),
810 &[editor.max_point(cx)..editor.max_point(cx)]
811 );
812 assert_eq!(
813 editor.scroll_position(cx),
814 gpui::Point::new(0., editor.max_point(cx).row().as_f32())
815 );
816
817 editor
818 })
819 });
820}
821
822#[gpui::test]
823fn test_cancel(cx: &mut TestAppContext) {
824 init_test(cx, |_| {});
825
826 let editor = cx.add_window(|window, cx| {
827 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
828 build_editor(buffer, window, cx)
829 });
830
831 _ = editor.update(cx, |editor, window, cx| {
832 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
833 editor.update_selection(
834 DisplayPoint::new(DisplayRow(1), 1),
835 0,
836 gpui::Point::<f32>::default(),
837 window,
838 cx,
839 );
840 editor.end_selection(window, cx);
841
842 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
843 editor.update_selection(
844 DisplayPoint::new(DisplayRow(0), 3),
845 0,
846 gpui::Point::<f32>::default(),
847 window,
848 cx,
849 );
850 editor.end_selection(window, cx);
851 assert_eq!(
852 editor.selections.display_ranges(cx),
853 [
854 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
855 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
856 ]
857 );
858 });
859
860 _ = editor.update(cx, |editor, window, cx| {
861 editor.cancel(&Cancel, window, cx);
862 assert_eq!(
863 editor.selections.display_ranges(cx),
864 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
865 );
866 });
867
868 _ = editor.update(cx, |editor, window, cx| {
869 editor.cancel(&Cancel, window, cx);
870 assert_eq!(
871 editor.selections.display_ranges(cx),
872 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
873 );
874 });
875}
876
877#[gpui::test]
878fn test_fold_action(cx: &mut TestAppContext) {
879 init_test(cx, |_| {});
880
881 let editor = cx.add_window(|window, cx| {
882 let buffer = MultiBuffer::build_simple(
883 &"
884 impl Foo {
885 // Hello!
886
887 fn a() {
888 1
889 }
890
891 fn b() {
892 2
893 }
894
895 fn c() {
896 3
897 }
898 }
899 "
900 .unindent(),
901 cx,
902 );
903 build_editor(buffer, window, cx)
904 });
905
906 _ = editor.update(cx, |editor, window, cx| {
907 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
908 s.select_display_ranges([
909 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
910 ]);
911 });
912 editor.fold(&Fold, window, cx);
913 assert_eq!(
914 editor.display_text(cx),
915 "
916 impl Foo {
917 // Hello!
918
919 fn a() {
920 1
921 }
922
923 fn b() {⋯
924 }
925
926 fn c() {⋯
927 }
928 }
929 "
930 .unindent(),
931 );
932
933 editor.fold(&Fold, window, cx);
934 assert_eq!(
935 editor.display_text(cx),
936 "
937 impl Foo {⋯
938 }
939 "
940 .unindent(),
941 );
942
943 editor.unfold_lines(&UnfoldLines, window, cx);
944 assert_eq!(
945 editor.display_text(cx),
946 "
947 impl Foo {
948 // Hello!
949
950 fn a() {
951 1
952 }
953
954 fn b() {⋯
955 }
956
957 fn c() {⋯
958 }
959 }
960 "
961 .unindent(),
962 );
963
964 editor.unfold_lines(&UnfoldLines, window, cx);
965 assert_eq!(
966 editor.display_text(cx),
967 editor.buffer.read(cx).read(cx).text()
968 );
969 });
970}
971
972#[gpui::test]
973fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
974 init_test(cx, |_| {});
975
976 let editor = cx.add_window(|window, cx| {
977 let buffer = MultiBuffer::build_simple(
978 &"
979 class Foo:
980 # Hello!
981
982 def a():
983 print(1)
984
985 def b():
986 print(2)
987
988 def c():
989 print(3)
990 "
991 .unindent(),
992 cx,
993 );
994 build_editor(buffer, window, cx)
995 });
996
997 _ = editor.update(cx, |editor, window, cx| {
998 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
999 s.select_display_ranges([
1000 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
1001 ]);
1002 });
1003 editor.fold(&Fold, window, cx);
1004 assert_eq!(
1005 editor.display_text(cx),
1006 "
1007 class Foo:
1008 # Hello!
1009
1010 def a():
1011 print(1)
1012
1013 def b():⋯
1014
1015 def c():⋯
1016 "
1017 .unindent(),
1018 );
1019
1020 editor.fold(&Fold, window, cx);
1021 assert_eq!(
1022 editor.display_text(cx),
1023 "
1024 class Foo:⋯
1025 "
1026 .unindent(),
1027 );
1028
1029 editor.unfold_lines(&UnfoldLines, window, cx);
1030 assert_eq!(
1031 editor.display_text(cx),
1032 "
1033 class Foo:
1034 # Hello!
1035
1036 def a():
1037 print(1)
1038
1039 def b():⋯
1040
1041 def c():⋯
1042 "
1043 .unindent(),
1044 );
1045
1046 editor.unfold_lines(&UnfoldLines, window, cx);
1047 assert_eq!(
1048 editor.display_text(cx),
1049 editor.buffer.read(cx).read(cx).text()
1050 );
1051 });
1052}
1053
1054#[gpui::test]
1055fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1056 init_test(cx, |_| {});
1057
1058 let editor = cx.add_window(|window, cx| {
1059 let buffer = MultiBuffer::build_simple(
1060 &"
1061 class Foo:
1062 # Hello!
1063
1064 def a():
1065 print(1)
1066
1067 def b():
1068 print(2)
1069
1070
1071 def c():
1072 print(3)
1073
1074
1075 "
1076 .unindent(),
1077 cx,
1078 );
1079 build_editor(buffer, window, cx)
1080 });
1081
1082 _ = editor.update(cx, |editor, window, cx| {
1083 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1084 s.select_display_ranges([
1085 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1086 ]);
1087 });
1088 editor.fold(&Fold, window, cx);
1089 assert_eq!(
1090 editor.display_text(cx),
1091 "
1092 class Foo:
1093 # Hello!
1094
1095 def a():
1096 print(1)
1097
1098 def b():⋯
1099
1100
1101 def c():⋯
1102
1103
1104 "
1105 .unindent(),
1106 );
1107
1108 editor.fold(&Fold, window, cx);
1109 assert_eq!(
1110 editor.display_text(cx),
1111 "
1112 class Foo:⋯
1113
1114
1115 "
1116 .unindent(),
1117 );
1118
1119 editor.unfold_lines(&UnfoldLines, window, cx);
1120 assert_eq!(
1121 editor.display_text(cx),
1122 "
1123 class Foo:
1124 # Hello!
1125
1126 def a():
1127 print(1)
1128
1129 def b():⋯
1130
1131
1132 def c():⋯
1133
1134
1135 "
1136 .unindent(),
1137 );
1138
1139 editor.unfold_lines(&UnfoldLines, window, cx);
1140 assert_eq!(
1141 editor.display_text(cx),
1142 editor.buffer.read(cx).read(cx).text()
1143 );
1144 });
1145}
1146
1147#[gpui::test]
1148fn test_fold_at_level(cx: &mut TestAppContext) {
1149 init_test(cx, |_| {});
1150
1151 let editor = cx.add_window(|window, cx| {
1152 let buffer = MultiBuffer::build_simple(
1153 &"
1154 class Foo:
1155 # Hello!
1156
1157 def a():
1158 print(1)
1159
1160 def b():
1161 print(2)
1162
1163
1164 class Bar:
1165 # World!
1166
1167 def a():
1168 print(1)
1169
1170 def b():
1171 print(2)
1172
1173
1174 "
1175 .unindent(),
1176 cx,
1177 );
1178 build_editor(buffer, window, cx)
1179 });
1180
1181 _ = editor.update(cx, |editor, window, cx| {
1182 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1183 assert_eq!(
1184 editor.display_text(cx),
1185 "
1186 class Foo:
1187 # Hello!
1188
1189 def a():⋯
1190
1191 def b():⋯
1192
1193
1194 class Bar:
1195 # World!
1196
1197 def a():⋯
1198
1199 def b():⋯
1200
1201
1202 "
1203 .unindent(),
1204 );
1205
1206 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1207 assert_eq!(
1208 editor.display_text(cx),
1209 "
1210 class Foo:⋯
1211
1212
1213 class Bar:⋯
1214
1215
1216 "
1217 .unindent(),
1218 );
1219
1220 editor.unfold_all(&UnfoldAll, window, cx);
1221 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1222 assert_eq!(
1223 editor.display_text(cx),
1224 "
1225 class Foo:
1226 # Hello!
1227
1228 def a():
1229 print(1)
1230
1231 def b():
1232 print(2)
1233
1234
1235 class Bar:
1236 # World!
1237
1238 def a():
1239 print(1)
1240
1241 def b():
1242 print(2)
1243
1244
1245 "
1246 .unindent(),
1247 );
1248
1249 assert_eq!(
1250 editor.display_text(cx),
1251 editor.buffer.read(cx).read(cx).text()
1252 );
1253 });
1254}
1255
1256#[gpui::test]
1257fn test_move_cursor(cx: &mut TestAppContext) {
1258 init_test(cx, |_| {});
1259
1260 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1261 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1262
1263 buffer.update(cx, |buffer, cx| {
1264 buffer.edit(
1265 vec![
1266 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1267 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1268 ],
1269 None,
1270 cx,
1271 );
1272 });
1273 _ = editor.update(cx, |editor, window, cx| {
1274 assert_eq!(
1275 editor.selections.display_ranges(cx),
1276 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1277 );
1278
1279 editor.move_down(&MoveDown, window, cx);
1280 assert_eq!(
1281 editor.selections.display_ranges(cx),
1282 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1283 );
1284
1285 editor.move_right(&MoveRight, window, cx);
1286 assert_eq!(
1287 editor.selections.display_ranges(cx),
1288 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1289 );
1290
1291 editor.move_left(&MoveLeft, window, cx);
1292 assert_eq!(
1293 editor.selections.display_ranges(cx),
1294 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1295 );
1296
1297 editor.move_up(&MoveUp, window, cx);
1298 assert_eq!(
1299 editor.selections.display_ranges(cx),
1300 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1301 );
1302
1303 editor.move_to_end(&MoveToEnd, window, cx);
1304 assert_eq!(
1305 editor.selections.display_ranges(cx),
1306 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1307 );
1308
1309 editor.move_to_beginning(&MoveToBeginning, window, cx);
1310 assert_eq!(
1311 editor.selections.display_ranges(cx),
1312 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1313 );
1314
1315 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1316 s.select_display_ranges([
1317 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1318 ]);
1319 });
1320 editor.select_to_beginning(&SelectToBeginning, window, cx);
1321 assert_eq!(
1322 editor.selections.display_ranges(cx),
1323 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1324 );
1325
1326 editor.select_to_end(&SelectToEnd, window, cx);
1327 assert_eq!(
1328 editor.selections.display_ranges(cx),
1329 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1330 );
1331 });
1332}
1333
1334#[gpui::test]
1335fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1336 init_test(cx, |_| {});
1337
1338 let editor = cx.add_window(|window, cx| {
1339 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1340 build_editor(buffer, window, cx)
1341 });
1342
1343 assert_eq!('🟥'.len_utf8(), 4);
1344 assert_eq!('α'.len_utf8(), 2);
1345
1346 _ = editor.update(cx, |editor, window, cx| {
1347 editor.fold_creases(
1348 vec![
1349 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1350 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1351 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1352 ],
1353 true,
1354 window,
1355 cx,
1356 );
1357 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1358
1359 editor.move_right(&MoveRight, window, cx);
1360 assert_eq!(
1361 editor.selections.display_ranges(cx),
1362 &[empty_range(0, "🟥".len())]
1363 );
1364 editor.move_right(&MoveRight, window, cx);
1365 assert_eq!(
1366 editor.selections.display_ranges(cx),
1367 &[empty_range(0, "🟥🟧".len())]
1368 );
1369 editor.move_right(&MoveRight, window, cx);
1370 assert_eq!(
1371 editor.selections.display_ranges(cx),
1372 &[empty_range(0, "🟥🟧⋯".len())]
1373 );
1374
1375 editor.move_down(&MoveDown, window, cx);
1376 assert_eq!(
1377 editor.selections.display_ranges(cx),
1378 &[empty_range(1, "ab⋯e".len())]
1379 );
1380 editor.move_left(&MoveLeft, window, cx);
1381 assert_eq!(
1382 editor.selections.display_ranges(cx),
1383 &[empty_range(1, "ab⋯".len())]
1384 );
1385 editor.move_left(&MoveLeft, window, cx);
1386 assert_eq!(
1387 editor.selections.display_ranges(cx),
1388 &[empty_range(1, "ab".len())]
1389 );
1390 editor.move_left(&MoveLeft, window, cx);
1391 assert_eq!(
1392 editor.selections.display_ranges(cx),
1393 &[empty_range(1, "a".len())]
1394 );
1395
1396 editor.move_down(&MoveDown, window, cx);
1397 assert_eq!(
1398 editor.selections.display_ranges(cx),
1399 &[empty_range(2, "α".len())]
1400 );
1401 editor.move_right(&MoveRight, window, cx);
1402 assert_eq!(
1403 editor.selections.display_ranges(cx),
1404 &[empty_range(2, "αβ".len())]
1405 );
1406 editor.move_right(&MoveRight, window, cx);
1407 assert_eq!(
1408 editor.selections.display_ranges(cx),
1409 &[empty_range(2, "αβ⋯".len())]
1410 );
1411 editor.move_right(&MoveRight, window, cx);
1412 assert_eq!(
1413 editor.selections.display_ranges(cx),
1414 &[empty_range(2, "αβ⋯ε".len())]
1415 );
1416
1417 editor.move_up(&MoveUp, window, cx);
1418 assert_eq!(
1419 editor.selections.display_ranges(cx),
1420 &[empty_range(1, "ab⋯e".len())]
1421 );
1422 editor.move_down(&MoveDown, window, cx);
1423 assert_eq!(
1424 editor.selections.display_ranges(cx),
1425 &[empty_range(2, "αβ⋯ε".len())]
1426 );
1427 editor.move_up(&MoveUp, window, cx);
1428 assert_eq!(
1429 editor.selections.display_ranges(cx),
1430 &[empty_range(1, "ab⋯e".len())]
1431 );
1432
1433 editor.move_up(&MoveUp, window, cx);
1434 assert_eq!(
1435 editor.selections.display_ranges(cx),
1436 &[empty_range(0, "🟥🟧".len())]
1437 );
1438 editor.move_left(&MoveLeft, window, cx);
1439 assert_eq!(
1440 editor.selections.display_ranges(cx),
1441 &[empty_range(0, "🟥".len())]
1442 );
1443 editor.move_left(&MoveLeft, window, cx);
1444 assert_eq!(
1445 editor.selections.display_ranges(cx),
1446 &[empty_range(0, "".len())]
1447 );
1448 });
1449}
1450
1451#[gpui::test]
1452fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1453 init_test(cx, |_| {});
1454
1455 let editor = cx.add_window(|window, cx| {
1456 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1457 build_editor(buffer, window, cx)
1458 });
1459 _ = editor.update(cx, |editor, window, cx| {
1460 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1461 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1462 });
1463
1464 // moving above start of document should move selection to start of document,
1465 // but the next move down should still be at the original goal_x
1466 editor.move_up(&MoveUp, window, cx);
1467 assert_eq!(
1468 editor.selections.display_ranges(cx),
1469 &[empty_range(0, "".len())]
1470 );
1471
1472 editor.move_down(&MoveDown, window, cx);
1473 assert_eq!(
1474 editor.selections.display_ranges(cx),
1475 &[empty_range(1, "abcd".len())]
1476 );
1477
1478 editor.move_down(&MoveDown, window, cx);
1479 assert_eq!(
1480 editor.selections.display_ranges(cx),
1481 &[empty_range(2, "αβγ".len())]
1482 );
1483
1484 editor.move_down(&MoveDown, window, cx);
1485 assert_eq!(
1486 editor.selections.display_ranges(cx),
1487 &[empty_range(3, "abcd".len())]
1488 );
1489
1490 editor.move_down(&MoveDown, window, cx);
1491 assert_eq!(
1492 editor.selections.display_ranges(cx),
1493 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1494 );
1495
1496 // moving past end of document should not change goal_x
1497 editor.move_down(&MoveDown, window, cx);
1498 assert_eq!(
1499 editor.selections.display_ranges(cx),
1500 &[empty_range(5, "".len())]
1501 );
1502
1503 editor.move_down(&MoveDown, window, cx);
1504 assert_eq!(
1505 editor.selections.display_ranges(cx),
1506 &[empty_range(5, "".len())]
1507 );
1508
1509 editor.move_up(&MoveUp, window, cx);
1510 assert_eq!(
1511 editor.selections.display_ranges(cx),
1512 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1513 );
1514
1515 editor.move_up(&MoveUp, window, cx);
1516 assert_eq!(
1517 editor.selections.display_ranges(cx),
1518 &[empty_range(3, "abcd".len())]
1519 );
1520
1521 editor.move_up(&MoveUp, window, cx);
1522 assert_eq!(
1523 editor.selections.display_ranges(cx),
1524 &[empty_range(2, "αβγ".len())]
1525 );
1526 });
1527}
1528
1529#[gpui::test]
1530fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1531 init_test(cx, |_| {});
1532 let move_to_beg = MoveToBeginningOfLine {
1533 stop_at_soft_wraps: true,
1534 stop_at_indent: true,
1535 };
1536
1537 let delete_to_beg = DeleteToBeginningOfLine {
1538 stop_at_indent: false,
1539 };
1540
1541 let move_to_end = MoveToEndOfLine {
1542 stop_at_soft_wraps: true,
1543 };
1544
1545 let editor = cx.add_window(|window, cx| {
1546 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1547 build_editor(buffer, window, cx)
1548 });
1549 _ = editor.update(cx, |editor, window, cx| {
1550 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1551 s.select_display_ranges([
1552 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1553 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1554 ]);
1555 });
1556 });
1557
1558 _ = editor.update(cx, |editor, window, cx| {
1559 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1560 assert_eq!(
1561 editor.selections.display_ranges(cx),
1562 &[
1563 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1564 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1565 ]
1566 );
1567 });
1568
1569 _ = editor.update(cx, |editor, window, cx| {
1570 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1571 assert_eq!(
1572 editor.selections.display_ranges(cx),
1573 &[
1574 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1575 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1576 ]
1577 );
1578 });
1579
1580 _ = editor.update(cx, |editor, window, cx| {
1581 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1582 assert_eq!(
1583 editor.selections.display_ranges(cx),
1584 &[
1585 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1586 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1587 ]
1588 );
1589 });
1590
1591 _ = editor.update(cx, |editor, window, cx| {
1592 editor.move_to_end_of_line(&move_to_end, window, cx);
1593 assert_eq!(
1594 editor.selections.display_ranges(cx),
1595 &[
1596 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1597 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1598 ]
1599 );
1600 });
1601
1602 // Moving to the end of line again is a no-op.
1603 _ = editor.update(cx, |editor, window, cx| {
1604 editor.move_to_end_of_line(&move_to_end, window, cx);
1605 assert_eq!(
1606 editor.selections.display_ranges(cx),
1607 &[
1608 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1609 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1610 ]
1611 );
1612 });
1613
1614 _ = editor.update(cx, |editor, window, cx| {
1615 editor.move_left(&MoveLeft, window, cx);
1616 editor.select_to_beginning_of_line(
1617 &SelectToBeginningOfLine {
1618 stop_at_soft_wraps: true,
1619 stop_at_indent: true,
1620 },
1621 window,
1622 cx,
1623 );
1624 assert_eq!(
1625 editor.selections.display_ranges(cx),
1626 &[
1627 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1628 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1629 ]
1630 );
1631 });
1632
1633 _ = editor.update(cx, |editor, window, cx| {
1634 editor.select_to_beginning_of_line(
1635 &SelectToBeginningOfLine {
1636 stop_at_soft_wraps: true,
1637 stop_at_indent: true,
1638 },
1639 window,
1640 cx,
1641 );
1642 assert_eq!(
1643 editor.selections.display_ranges(cx),
1644 &[
1645 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1646 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1647 ]
1648 );
1649 });
1650
1651 _ = editor.update(cx, |editor, window, cx| {
1652 editor.select_to_beginning_of_line(
1653 &SelectToBeginningOfLine {
1654 stop_at_soft_wraps: true,
1655 stop_at_indent: true,
1656 },
1657 window,
1658 cx,
1659 );
1660 assert_eq!(
1661 editor.selections.display_ranges(cx),
1662 &[
1663 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1664 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1665 ]
1666 );
1667 });
1668
1669 _ = editor.update(cx, |editor, window, cx| {
1670 editor.select_to_end_of_line(
1671 &SelectToEndOfLine {
1672 stop_at_soft_wraps: true,
1673 },
1674 window,
1675 cx,
1676 );
1677 assert_eq!(
1678 editor.selections.display_ranges(cx),
1679 &[
1680 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1681 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1682 ]
1683 );
1684 });
1685
1686 _ = editor.update(cx, |editor, window, cx| {
1687 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1688 assert_eq!(editor.display_text(cx), "ab\n de");
1689 assert_eq!(
1690 editor.selections.display_ranges(cx),
1691 &[
1692 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1693 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1694 ]
1695 );
1696 });
1697
1698 _ = editor.update(cx, |editor, window, cx| {
1699 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1700 assert_eq!(editor.display_text(cx), "\n");
1701 assert_eq!(
1702 editor.selections.display_ranges(cx),
1703 &[
1704 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1705 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1706 ]
1707 );
1708 });
1709}
1710
1711#[gpui::test]
1712fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1713 init_test(cx, |_| {});
1714 let move_to_beg = MoveToBeginningOfLine {
1715 stop_at_soft_wraps: false,
1716 stop_at_indent: false,
1717 };
1718
1719 let move_to_end = MoveToEndOfLine {
1720 stop_at_soft_wraps: false,
1721 };
1722
1723 let editor = cx.add_window(|window, cx| {
1724 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1725 build_editor(buffer, window, cx)
1726 });
1727
1728 _ = editor.update(cx, |editor, window, cx| {
1729 editor.set_wrap_width(Some(140.0.into()), cx);
1730
1731 // We expect the following lines after wrapping
1732 // ```
1733 // thequickbrownfox
1734 // jumpedoverthelazydo
1735 // gs
1736 // ```
1737 // The final `gs` was soft-wrapped onto a new line.
1738 assert_eq!(
1739 "thequickbrownfox\njumpedoverthelaz\nydogs",
1740 editor.display_text(cx),
1741 );
1742
1743 // First, let's assert behavior on the first line, that was not soft-wrapped.
1744 // Start the cursor at the `k` on the first line
1745 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1746 s.select_display_ranges([
1747 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1748 ]);
1749 });
1750
1751 // Moving to the beginning of the line should put us at the beginning of the line.
1752 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1753 assert_eq!(
1754 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1755 editor.selections.display_ranges(cx)
1756 );
1757
1758 // Moving to the end of the line should put us at the end of the line.
1759 editor.move_to_end_of_line(&move_to_end, window, cx);
1760 assert_eq!(
1761 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1762 editor.selections.display_ranges(cx)
1763 );
1764
1765 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1766 // Start the cursor at the last line (`y` that was wrapped to a new line)
1767 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1768 s.select_display_ranges([
1769 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1770 ]);
1771 });
1772
1773 // Moving to the beginning of the line should put us at the start of the second line of
1774 // display text, i.e., the `j`.
1775 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1776 assert_eq!(
1777 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1778 editor.selections.display_ranges(cx)
1779 );
1780
1781 // Moving to the beginning of the line again should be a no-op.
1782 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1783 assert_eq!(
1784 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1785 editor.selections.display_ranges(cx)
1786 );
1787
1788 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1789 // next display line.
1790 editor.move_to_end_of_line(&move_to_end, window, cx);
1791 assert_eq!(
1792 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1793 editor.selections.display_ranges(cx)
1794 );
1795
1796 // Moving to the end of the line again should be a no-op.
1797 editor.move_to_end_of_line(&move_to_end, window, cx);
1798 assert_eq!(
1799 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1800 editor.selections.display_ranges(cx)
1801 );
1802 });
1803}
1804
1805#[gpui::test]
1806fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1807 init_test(cx, |_| {});
1808
1809 let move_to_beg = MoveToBeginningOfLine {
1810 stop_at_soft_wraps: true,
1811 stop_at_indent: true,
1812 };
1813
1814 let select_to_beg = SelectToBeginningOfLine {
1815 stop_at_soft_wraps: true,
1816 stop_at_indent: true,
1817 };
1818
1819 let delete_to_beg = DeleteToBeginningOfLine {
1820 stop_at_indent: true,
1821 };
1822
1823 let move_to_end = MoveToEndOfLine {
1824 stop_at_soft_wraps: false,
1825 };
1826
1827 let editor = cx.add_window(|window, cx| {
1828 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1829 build_editor(buffer, window, cx)
1830 });
1831
1832 _ = editor.update(cx, |editor, window, cx| {
1833 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1834 s.select_display_ranges([
1835 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1836 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1837 ]);
1838 });
1839
1840 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1841 // and the second cursor at the first non-whitespace character in the line.
1842 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1843 assert_eq!(
1844 editor.selections.display_ranges(cx),
1845 &[
1846 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1847 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1848 ]
1849 );
1850
1851 // Moving to the beginning of the line again should be a no-op for the first cursor,
1852 // and should move the second cursor to the beginning of the line.
1853 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1854 assert_eq!(
1855 editor.selections.display_ranges(cx),
1856 &[
1857 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1858 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1859 ]
1860 );
1861
1862 // Moving to the beginning of the line again should still be a no-op for the first cursor,
1863 // and should move the second cursor back to the first non-whitespace character in the line.
1864 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1865 assert_eq!(
1866 editor.selections.display_ranges(cx),
1867 &[
1868 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1869 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1870 ]
1871 );
1872
1873 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
1874 // and to the first non-whitespace character in the line for the second cursor.
1875 editor.move_to_end_of_line(&move_to_end, window, cx);
1876 editor.move_left(&MoveLeft, window, cx);
1877 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1878 assert_eq!(
1879 editor.selections.display_ranges(cx),
1880 &[
1881 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1882 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1883 ]
1884 );
1885
1886 // Selecting to the beginning of the line again should be a no-op for the first cursor,
1887 // and should select to the beginning of the line for the second cursor.
1888 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1889 assert_eq!(
1890 editor.selections.display_ranges(cx),
1891 &[
1892 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1893 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1894 ]
1895 );
1896
1897 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
1898 // and should delete to the first non-whitespace character in the line for the second cursor.
1899 editor.move_to_end_of_line(&move_to_end, window, cx);
1900 editor.move_left(&MoveLeft, window, cx);
1901 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1902 assert_eq!(editor.text(cx), "c\n f");
1903 });
1904}
1905
1906#[gpui::test]
1907fn test_beginning_of_line_with_cursor_between_line_start_and_indent(cx: &mut TestAppContext) {
1908 init_test(cx, |_| {});
1909
1910 let move_to_beg = MoveToBeginningOfLine {
1911 stop_at_soft_wraps: true,
1912 stop_at_indent: true,
1913 };
1914
1915 let editor = cx.add_window(|window, cx| {
1916 let buffer = MultiBuffer::build_simple(" hello\nworld", cx);
1917 build_editor(buffer, window, cx)
1918 });
1919
1920 _ = editor.update(cx, |editor, window, cx| {
1921 // test cursor between line_start and indent_start
1922 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1923 s.select_display_ranges([
1924 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3)
1925 ]);
1926 });
1927
1928 // cursor should move to line_start
1929 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1930 assert_eq!(
1931 editor.selections.display_ranges(cx),
1932 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1933 );
1934
1935 // cursor should move to indent_start
1936 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1937 assert_eq!(
1938 editor.selections.display_ranges(cx),
1939 &[DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 4)]
1940 );
1941
1942 // cursor should move to back to line_start
1943 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1944 assert_eq!(
1945 editor.selections.display_ranges(cx),
1946 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1947 );
1948 });
1949}
1950
1951#[gpui::test]
1952fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1953 init_test(cx, |_| {});
1954
1955 let editor = cx.add_window(|window, cx| {
1956 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1957 build_editor(buffer, window, cx)
1958 });
1959 _ = editor.update(cx, |editor, window, cx| {
1960 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1961 s.select_display_ranges([
1962 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
1963 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
1964 ])
1965 });
1966 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1967 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1968
1969 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1970 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
1971
1972 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1973 assert_selection_ranges("use ˇstd::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1974
1975 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1976 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1977
1978 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1979 assert_selection_ranges("ˇuse std::str::{foo, ˇbar}\n\n {baz.qux()}", editor, cx);
1980
1981 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1982 assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1983
1984 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1985 assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
1986
1987 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1988 assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1989
1990 editor.move_right(&MoveRight, window, cx);
1991 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1992 assert_selection_ranges(
1993 "use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}",
1994 editor,
1995 cx,
1996 );
1997
1998 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1999 assert_selection_ranges(
2000 "use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
2001 editor,
2002 cx,
2003 );
2004
2005 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
2006 assert_selection_ranges(
2007 "use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
2008 editor,
2009 cx,
2010 );
2011 });
2012}
2013
2014#[gpui::test]
2015fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
2016 init_test(cx, |_| {});
2017
2018 let editor = cx.add_window(|window, cx| {
2019 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
2020 build_editor(buffer, window, cx)
2021 });
2022
2023 _ = editor.update(cx, |editor, window, cx| {
2024 editor.set_wrap_width(Some(140.0.into()), cx);
2025 assert_eq!(
2026 editor.display_text(cx),
2027 "use one::{\n two::three::\n four::five\n};"
2028 );
2029
2030 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2031 s.select_display_ranges([
2032 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
2033 ]);
2034 });
2035
2036 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2037 assert_eq!(
2038 editor.selections.display_ranges(cx),
2039 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
2040 );
2041
2042 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2043 assert_eq!(
2044 editor.selections.display_ranges(cx),
2045 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2046 );
2047
2048 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2049 assert_eq!(
2050 editor.selections.display_ranges(cx),
2051 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2052 );
2053
2054 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2055 assert_eq!(
2056 editor.selections.display_ranges(cx),
2057 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2058 );
2059
2060 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2061 assert_eq!(
2062 editor.selections.display_ranges(cx),
2063 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2064 );
2065
2066 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2067 assert_eq!(
2068 editor.selections.display_ranges(cx),
2069 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2070 );
2071 });
2072}
2073
2074#[gpui::test]
2075async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2076 init_test(cx, |_| {});
2077 let mut cx = EditorTestContext::new(cx).await;
2078
2079 let line_height = cx.editor(|editor, window, _| {
2080 editor
2081 .style()
2082 .unwrap()
2083 .text
2084 .line_height_in_pixels(window.rem_size())
2085 });
2086 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2087
2088 cx.set_state(
2089 &r#"ˇone
2090 two
2091
2092 three
2093 fourˇ
2094 five
2095
2096 six"#
2097 .unindent(),
2098 );
2099
2100 cx.update_editor(|editor, window, cx| {
2101 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2102 });
2103 cx.assert_editor_state(
2104 &r#"one
2105 two
2106 ˇ
2107 three
2108 four
2109 five
2110 ˇ
2111 six"#
2112 .unindent(),
2113 );
2114
2115 cx.update_editor(|editor, window, cx| {
2116 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2117 });
2118 cx.assert_editor_state(
2119 &r#"one
2120 two
2121
2122 three
2123 four
2124 five
2125 ˇ
2126 sixˇ"#
2127 .unindent(),
2128 );
2129
2130 cx.update_editor(|editor, window, cx| {
2131 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2132 });
2133 cx.assert_editor_state(
2134 &r#"one
2135 two
2136
2137 three
2138 four
2139 five
2140
2141 sixˇ"#
2142 .unindent(),
2143 );
2144
2145 cx.update_editor(|editor, window, cx| {
2146 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2147 });
2148 cx.assert_editor_state(
2149 &r#"one
2150 two
2151
2152 three
2153 four
2154 five
2155 ˇ
2156 six"#
2157 .unindent(),
2158 );
2159
2160 cx.update_editor(|editor, window, cx| {
2161 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2162 });
2163 cx.assert_editor_state(
2164 &r#"one
2165 two
2166 ˇ
2167 three
2168 four
2169 five
2170
2171 six"#
2172 .unindent(),
2173 );
2174
2175 cx.update_editor(|editor, window, cx| {
2176 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2177 });
2178 cx.assert_editor_state(
2179 &r#"ˇone
2180 two
2181
2182 three
2183 four
2184 five
2185
2186 six"#
2187 .unindent(),
2188 );
2189}
2190
2191#[gpui::test]
2192async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2193 init_test(cx, |_| {});
2194 let mut cx = EditorTestContext::new(cx).await;
2195 let line_height = cx.editor(|editor, window, _| {
2196 editor
2197 .style()
2198 .unwrap()
2199 .text
2200 .line_height_in_pixels(window.rem_size())
2201 });
2202 let window = cx.window;
2203 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2204
2205 cx.set_state(
2206 r#"ˇone
2207 two
2208 three
2209 four
2210 five
2211 six
2212 seven
2213 eight
2214 nine
2215 ten
2216 "#,
2217 );
2218
2219 cx.update_editor(|editor, window, cx| {
2220 assert_eq!(
2221 editor.snapshot(window, cx).scroll_position(),
2222 gpui::Point::new(0., 0.)
2223 );
2224 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2225 assert_eq!(
2226 editor.snapshot(window, cx).scroll_position(),
2227 gpui::Point::new(0., 3.)
2228 );
2229 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2230 assert_eq!(
2231 editor.snapshot(window, cx).scroll_position(),
2232 gpui::Point::new(0., 6.)
2233 );
2234 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2235 assert_eq!(
2236 editor.snapshot(window, cx).scroll_position(),
2237 gpui::Point::new(0., 3.)
2238 );
2239
2240 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2241 assert_eq!(
2242 editor.snapshot(window, cx).scroll_position(),
2243 gpui::Point::new(0., 1.)
2244 );
2245 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2246 assert_eq!(
2247 editor.snapshot(window, cx).scroll_position(),
2248 gpui::Point::new(0., 3.)
2249 );
2250 });
2251}
2252
2253#[gpui::test]
2254async fn test_autoscroll(cx: &mut TestAppContext) {
2255 init_test(cx, |_| {});
2256 let mut cx = EditorTestContext::new(cx).await;
2257
2258 let line_height = cx.update_editor(|editor, window, cx| {
2259 editor.set_vertical_scroll_margin(2, cx);
2260 editor
2261 .style()
2262 .unwrap()
2263 .text
2264 .line_height_in_pixels(window.rem_size())
2265 });
2266 let window = cx.window;
2267 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2268
2269 cx.set_state(
2270 r#"ˇone
2271 two
2272 three
2273 four
2274 five
2275 six
2276 seven
2277 eight
2278 nine
2279 ten
2280 "#,
2281 );
2282 cx.update_editor(|editor, window, cx| {
2283 assert_eq!(
2284 editor.snapshot(window, cx).scroll_position(),
2285 gpui::Point::new(0., 0.0)
2286 );
2287 });
2288
2289 // Add a cursor below the visible area. Since both cursors cannot fit
2290 // on screen, the editor autoscrolls to reveal the newest cursor, and
2291 // allows the vertical scroll margin below that cursor.
2292 cx.update_editor(|editor, window, cx| {
2293 editor.change_selections(Default::default(), window, cx, |selections| {
2294 selections.select_ranges([
2295 Point::new(0, 0)..Point::new(0, 0),
2296 Point::new(6, 0)..Point::new(6, 0),
2297 ]);
2298 })
2299 });
2300 cx.update_editor(|editor, window, cx| {
2301 assert_eq!(
2302 editor.snapshot(window, cx).scroll_position(),
2303 gpui::Point::new(0., 3.0)
2304 );
2305 });
2306
2307 // Move down. The editor cursor scrolls down to track the newest cursor.
2308 cx.update_editor(|editor, window, cx| {
2309 editor.move_down(&Default::default(), window, cx);
2310 });
2311 cx.update_editor(|editor, window, cx| {
2312 assert_eq!(
2313 editor.snapshot(window, cx).scroll_position(),
2314 gpui::Point::new(0., 4.0)
2315 );
2316 });
2317
2318 // Add a cursor above the visible area. Since both cursors fit on screen,
2319 // the editor scrolls to show both.
2320 cx.update_editor(|editor, window, cx| {
2321 editor.change_selections(Default::default(), window, cx, |selections| {
2322 selections.select_ranges([
2323 Point::new(1, 0)..Point::new(1, 0),
2324 Point::new(6, 0)..Point::new(6, 0),
2325 ]);
2326 })
2327 });
2328 cx.update_editor(|editor, window, cx| {
2329 assert_eq!(
2330 editor.snapshot(window, cx).scroll_position(),
2331 gpui::Point::new(0., 1.0)
2332 );
2333 });
2334}
2335
2336#[gpui::test]
2337async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2338 init_test(cx, |_| {});
2339 let mut cx = EditorTestContext::new(cx).await;
2340
2341 let line_height = cx.editor(|editor, window, _cx| {
2342 editor
2343 .style()
2344 .unwrap()
2345 .text
2346 .line_height_in_pixels(window.rem_size())
2347 });
2348 let window = cx.window;
2349 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2350 cx.set_state(
2351 &r#"
2352 ˇone
2353 two
2354 threeˇ
2355 four
2356 five
2357 six
2358 seven
2359 eight
2360 nine
2361 ten
2362 "#
2363 .unindent(),
2364 );
2365
2366 cx.update_editor(|editor, window, cx| {
2367 editor.move_page_down(&MovePageDown::default(), window, cx)
2368 });
2369 cx.assert_editor_state(
2370 &r#"
2371 one
2372 two
2373 three
2374 ˇfour
2375 five
2376 sixˇ
2377 seven
2378 eight
2379 nine
2380 ten
2381 "#
2382 .unindent(),
2383 );
2384
2385 cx.update_editor(|editor, window, cx| {
2386 editor.move_page_down(&MovePageDown::default(), window, cx)
2387 });
2388 cx.assert_editor_state(
2389 &r#"
2390 one
2391 two
2392 three
2393 four
2394 five
2395 six
2396 ˇseven
2397 eight
2398 nineˇ
2399 ten
2400 "#
2401 .unindent(),
2402 );
2403
2404 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2405 cx.assert_editor_state(
2406 &r#"
2407 one
2408 two
2409 three
2410 ˇfour
2411 five
2412 sixˇ
2413 seven
2414 eight
2415 nine
2416 ten
2417 "#
2418 .unindent(),
2419 );
2420
2421 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2422 cx.assert_editor_state(
2423 &r#"
2424 ˇone
2425 two
2426 threeˇ
2427 four
2428 five
2429 six
2430 seven
2431 eight
2432 nine
2433 ten
2434 "#
2435 .unindent(),
2436 );
2437
2438 // Test select collapsing
2439 cx.update_editor(|editor, window, cx| {
2440 editor.move_page_down(&MovePageDown::default(), window, cx);
2441 editor.move_page_down(&MovePageDown::default(), window, cx);
2442 editor.move_page_down(&MovePageDown::default(), window, cx);
2443 });
2444 cx.assert_editor_state(
2445 &r#"
2446 one
2447 two
2448 three
2449 four
2450 five
2451 six
2452 seven
2453 eight
2454 nine
2455 ˇten
2456 ˇ"#
2457 .unindent(),
2458 );
2459}
2460
2461#[gpui::test]
2462async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2463 init_test(cx, |_| {});
2464 let mut cx = EditorTestContext::new(cx).await;
2465 cx.set_state("one «two threeˇ» four");
2466 cx.update_editor(|editor, window, cx| {
2467 editor.delete_to_beginning_of_line(
2468 &DeleteToBeginningOfLine {
2469 stop_at_indent: false,
2470 },
2471 window,
2472 cx,
2473 );
2474 assert_eq!(editor.text(cx), " four");
2475 });
2476}
2477
2478#[gpui::test]
2479async fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2480 init_test(cx, |_| {});
2481
2482 let mut cx = EditorTestContext::new(cx).await;
2483
2484 // For an empty selection, the preceding word fragment is deleted.
2485 // For non-empty selections, only selected characters are deleted.
2486 cx.set_state("onˇe two t«hreˇ»e four");
2487 cx.update_editor(|editor, window, cx| {
2488 editor.delete_to_previous_word_start(
2489 &DeleteToPreviousWordStart {
2490 ignore_newlines: false,
2491 ignore_brackets: false,
2492 },
2493 window,
2494 cx,
2495 );
2496 });
2497 cx.assert_editor_state("ˇe two tˇe four");
2498
2499 cx.set_state("e tˇwo te «fˇ»our");
2500 cx.update_editor(|editor, window, cx| {
2501 editor.delete_to_next_word_end(
2502 &DeleteToNextWordEnd {
2503 ignore_newlines: false,
2504 ignore_brackets: false,
2505 },
2506 window,
2507 cx,
2508 );
2509 });
2510 cx.assert_editor_state("e tˇ te ˇour");
2511}
2512
2513#[gpui::test]
2514async fn test_delete_whitespaces(cx: &mut TestAppContext) {
2515 init_test(cx, |_| {});
2516
2517 let mut cx = EditorTestContext::new(cx).await;
2518
2519 cx.set_state("here is some text ˇwith a space");
2520 cx.update_editor(|editor, window, cx| {
2521 editor.delete_to_previous_word_start(
2522 &DeleteToPreviousWordStart {
2523 ignore_newlines: false,
2524 ignore_brackets: true,
2525 },
2526 window,
2527 cx,
2528 );
2529 });
2530 // Continuous whitespace sequences are removed entirely, words behind them are not affected by the deletion action.
2531 cx.assert_editor_state("here is some textˇwith a space");
2532
2533 cx.set_state("here is some text ˇwith a space");
2534 cx.update_editor(|editor, window, cx| {
2535 editor.delete_to_previous_word_start(
2536 &DeleteToPreviousWordStart {
2537 ignore_newlines: false,
2538 ignore_brackets: false,
2539 },
2540 window,
2541 cx,
2542 );
2543 });
2544 cx.assert_editor_state("here is some textˇwith a space");
2545
2546 cx.set_state("here is some textˇ with a space");
2547 cx.update_editor(|editor, window, cx| {
2548 editor.delete_to_next_word_end(
2549 &DeleteToNextWordEnd {
2550 ignore_newlines: false,
2551 ignore_brackets: true,
2552 },
2553 window,
2554 cx,
2555 );
2556 });
2557 // Same happens in the other direction.
2558 cx.assert_editor_state("here is some textˇwith a space");
2559
2560 cx.set_state("here is some textˇ with a space");
2561 cx.update_editor(|editor, window, cx| {
2562 editor.delete_to_next_word_end(
2563 &DeleteToNextWordEnd {
2564 ignore_newlines: false,
2565 ignore_brackets: false,
2566 },
2567 window,
2568 cx,
2569 );
2570 });
2571 cx.assert_editor_state("here is some textˇwith a space");
2572
2573 cx.set_state("here is some textˇ with a space");
2574 cx.update_editor(|editor, window, cx| {
2575 editor.delete_to_next_word_end(
2576 &DeleteToNextWordEnd {
2577 ignore_newlines: true,
2578 ignore_brackets: false,
2579 },
2580 window,
2581 cx,
2582 );
2583 });
2584 cx.assert_editor_state("here is some textˇwith a space");
2585 cx.update_editor(|editor, window, cx| {
2586 editor.delete_to_previous_word_start(
2587 &DeleteToPreviousWordStart {
2588 ignore_newlines: true,
2589 ignore_brackets: false,
2590 },
2591 window,
2592 cx,
2593 );
2594 });
2595 cx.assert_editor_state("here is some ˇwith a space");
2596 cx.update_editor(|editor, window, cx| {
2597 editor.delete_to_previous_word_start(
2598 &DeleteToPreviousWordStart {
2599 ignore_newlines: true,
2600 ignore_brackets: false,
2601 },
2602 window,
2603 cx,
2604 );
2605 });
2606 // Single whitespaces are removed with the word behind them.
2607 cx.assert_editor_state("here is ˇwith a space");
2608 cx.update_editor(|editor, window, cx| {
2609 editor.delete_to_previous_word_start(
2610 &DeleteToPreviousWordStart {
2611 ignore_newlines: true,
2612 ignore_brackets: false,
2613 },
2614 window,
2615 cx,
2616 );
2617 });
2618 cx.assert_editor_state("here ˇwith a space");
2619 cx.update_editor(|editor, window, cx| {
2620 editor.delete_to_previous_word_start(
2621 &DeleteToPreviousWordStart {
2622 ignore_newlines: true,
2623 ignore_brackets: false,
2624 },
2625 window,
2626 cx,
2627 );
2628 });
2629 cx.assert_editor_state("ˇwith a space");
2630 cx.update_editor(|editor, window, cx| {
2631 editor.delete_to_previous_word_start(
2632 &DeleteToPreviousWordStart {
2633 ignore_newlines: true,
2634 ignore_brackets: false,
2635 },
2636 window,
2637 cx,
2638 );
2639 });
2640 cx.assert_editor_state("ˇwith a space");
2641 cx.update_editor(|editor, window, cx| {
2642 editor.delete_to_next_word_end(
2643 &DeleteToNextWordEnd {
2644 ignore_newlines: true,
2645 ignore_brackets: false,
2646 },
2647 window,
2648 cx,
2649 );
2650 });
2651 // Same happens in the other direction.
2652 cx.assert_editor_state("ˇ a space");
2653 cx.update_editor(|editor, window, cx| {
2654 editor.delete_to_next_word_end(
2655 &DeleteToNextWordEnd {
2656 ignore_newlines: true,
2657 ignore_brackets: false,
2658 },
2659 window,
2660 cx,
2661 );
2662 });
2663 cx.assert_editor_state("ˇ space");
2664 cx.update_editor(|editor, window, cx| {
2665 editor.delete_to_next_word_end(
2666 &DeleteToNextWordEnd {
2667 ignore_newlines: true,
2668 ignore_brackets: false,
2669 },
2670 window,
2671 cx,
2672 );
2673 });
2674 cx.assert_editor_state("ˇ");
2675 cx.update_editor(|editor, window, cx| {
2676 editor.delete_to_next_word_end(
2677 &DeleteToNextWordEnd {
2678 ignore_newlines: true,
2679 ignore_brackets: false,
2680 },
2681 window,
2682 cx,
2683 );
2684 });
2685 cx.assert_editor_state("ˇ");
2686 cx.update_editor(|editor, window, cx| {
2687 editor.delete_to_previous_word_start(
2688 &DeleteToPreviousWordStart {
2689 ignore_newlines: true,
2690 ignore_brackets: false,
2691 },
2692 window,
2693 cx,
2694 );
2695 });
2696 cx.assert_editor_state("ˇ");
2697}
2698
2699#[gpui::test]
2700async fn test_delete_to_bracket(cx: &mut TestAppContext) {
2701 init_test(cx, |_| {});
2702
2703 let language = Arc::new(
2704 Language::new(
2705 LanguageConfig {
2706 brackets: BracketPairConfig {
2707 pairs: vec![
2708 BracketPair {
2709 start: "\"".to_string(),
2710 end: "\"".to_string(),
2711 close: true,
2712 surround: true,
2713 newline: false,
2714 },
2715 BracketPair {
2716 start: "(".to_string(),
2717 end: ")".to_string(),
2718 close: true,
2719 surround: true,
2720 newline: true,
2721 },
2722 ],
2723 ..BracketPairConfig::default()
2724 },
2725 ..LanguageConfig::default()
2726 },
2727 Some(tree_sitter_rust::LANGUAGE.into()),
2728 )
2729 .with_brackets_query(
2730 r#"
2731 ("(" @open ")" @close)
2732 ("\"" @open "\"" @close)
2733 "#,
2734 )
2735 .unwrap(),
2736 );
2737
2738 let mut cx = EditorTestContext::new(cx).await;
2739 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2740
2741 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2742 cx.update_editor(|editor, window, cx| {
2743 editor.delete_to_previous_word_start(
2744 &DeleteToPreviousWordStart {
2745 ignore_newlines: true,
2746 ignore_brackets: false,
2747 },
2748 window,
2749 cx,
2750 );
2751 });
2752 // Deletion stops before brackets if asked to not ignore them.
2753 cx.assert_editor_state(r#"macro!("ˇCOMMENT");"#);
2754 cx.update_editor(|editor, window, cx| {
2755 editor.delete_to_previous_word_start(
2756 &DeleteToPreviousWordStart {
2757 ignore_newlines: true,
2758 ignore_brackets: false,
2759 },
2760 window,
2761 cx,
2762 );
2763 });
2764 // Deletion has to remove a single bracket and then stop again.
2765 cx.assert_editor_state(r#"macro!(ˇCOMMENT");"#);
2766
2767 cx.update_editor(|editor, window, cx| {
2768 editor.delete_to_previous_word_start(
2769 &DeleteToPreviousWordStart {
2770 ignore_newlines: true,
2771 ignore_brackets: false,
2772 },
2773 window,
2774 cx,
2775 );
2776 });
2777 cx.assert_editor_state(r#"macro!ˇCOMMENT");"#);
2778
2779 cx.update_editor(|editor, window, cx| {
2780 editor.delete_to_previous_word_start(
2781 &DeleteToPreviousWordStart {
2782 ignore_newlines: true,
2783 ignore_brackets: false,
2784 },
2785 window,
2786 cx,
2787 );
2788 });
2789 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2790
2791 cx.update_editor(|editor, window, cx| {
2792 editor.delete_to_previous_word_start(
2793 &DeleteToPreviousWordStart {
2794 ignore_newlines: true,
2795 ignore_brackets: false,
2796 },
2797 window,
2798 cx,
2799 );
2800 });
2801 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2802
2803 cx.update_editor(|editor, window, cx| {
2804 editor.delete_to_next_word_end(
2805 &DeleteToNextWordEnd {
2806 ignore_newlines: true,
2807 ignore_brackets: false,
2808 },
2809 window,
2810 cx,
2811 );
2812 });
2813 // Brackets on the right are not paired anymore, hence deletion does not stop at them
2814 cx.assert_editor_state(r#"ˇ");"#);
2815
2816 cx.update_editor(|editor, window, cx| {
2817 editor.delete_to_next_word_end(
2818 &DeleteToNextWordEnd {
2819 ignore_newlines: true,
2820 ignore_brackets: false,
2821 },
2822 window,
2823 cx,
2824 );
2825 });
2826 cx.assert_editor_state(r#"ˇ"#);
2827
2828 cx.update_editor(|editor, window, cx| {
2829 editor.delete_to_next_word_end(
2830 &DeleteToNextWordEnd {
2831 ignore_newlines: true,
2832 ignore_brackets: false,
2833 },
2834 window,
2835 cx,
2836 );
2837 });
2838 cx.assert_editor_state(r#"ˇ"#);
2839
2840 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2841 cx.update_editor(|editor, window, cx| {
2842 editor.delete_to_previous_word_start(
2843 &DeleteToPreviousWordStart {
2844 ignore_newlines: true,
2845 ignore_brackets: true,
2846 },
2847 window,
2848 cx,
2849 );
2850 });
2851 cx.assert_editor_state(r#"macroˇCOMMENT");"#);
2852}
2853
2854#[gpui::test]
2855fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2856 init_test(cx, |_| {});
2857
2858 let editor = cx.add_window(|window, cx| {
2859 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2860 build_editor(buffer, window, cx)
2861 });
2862 let del_to_prev_word_start = DeleteToPreviousWordStart {
2863 ignore_newlines: false,
2864 ignore_brackets: false,
2865 };
2866 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2867 ignore_newlines: true,
2868 ignore_brackets: false,
2869 };
2870
2871 _ = editor.update(cx, |editor, window, cx| {
2872 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2873 s.select_display_ranges([
2874 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2875 ])
2876 });
2877 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2878 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2879 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2880 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2881 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2882 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2883 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2884 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
2885 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2886 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
2887 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2888 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2889 });
2890}
2891
2892#[gpui::test]
2893fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2894 init_test(cx, |_| {});
2895
2896 let editor = cx.add_window(|window, cx| {
2897 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2898 build_editor(buffer, window, cx)
2899 });
2900 let del_to_next_word_end = DeleteToNextWordEnd {
2901 ignore_newlines: false,
2902 ignore_brackets: false,
2903 };
2904 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2905 ignore_newlines: true,
2906 ignore_brackets: false,
2907 };
2908
2909 _ = editor.update(cx, |editor, window, cx| {
2910 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2911 s.select_display_ranges([
2912 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2913 ])
2914 });
2915 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2916 assert_eq!(
2917 editor.buffer.read(cx).read(cx).text(),
2918 "one\n two\nthree\n four"
2919 );
2920 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2921 assert_eq!(
2922 editor.buffer.read(cx).read(cx).text(),
2923 "\n two\nthree\n four"
2924 );
2925 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2926 assert_eq!(
2927 editor.buffer.read(cx).read(cx).text(),
2928 "two\nthree\n four"
2929 );
2930 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2931 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2932 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2933 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
2934 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2935 assert_eq!(editor.buffer.read(cx).read(cx).text(), "four");
2936 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2937 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2938 });
2939}
2940
2941#[gpui::test]
2942fn test_newline(cx: &mut TestAppContext) {
2943 init_test(cx, |_| {});
2944
2945 let editor = cx.add_window(|window, cx| {
2946 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2947 build_editor(buffer, window, cx)
2948 });
2949
2950 _ = editor.update(cx, |editor, window, cx| {
2951 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2952 s.select_display_ranges([
2953 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2954 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2955 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2956 ])
2957 });
2958
2959 editor.newline(&Newline, window, cx);
2960 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2961 });
2962}
2963
2964#[gpui::test]
2965fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2966 init_test(cx, |_| {});
2967
2968 let editor = cx.add_window(|window, cx| {
2969 let buffer = MultiBuffer::build_simple(
2970 "
2971 a
2972 b(
2973 X
2974 )
2975 c(
2976 X
2977 )
2978 "
2979 .unindent()
2980 .as_str(),
2981 cx,
2982 );
2983 let mut editor = build_editor(buffer, window, cx);
2984 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2985 s.select_ranges([
2986 Point::new(2, 4)..Point::new(2, 5),
2987 Point::new(5, 4)..Point::new(5, 5),
2988 ])
2989 });
2990 editor
2991 });
2992
2993 _ = editor.update(cx, |editor, window, cx| {
2994 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2995 editor.buffer.update(cx, |buffer, cx| {
2996 buffer.edit(
2997 [
2998 (Point::new(1, 2)..Point::new(3, 0), ""),
2999 (Point::new(4, 2)..Point::new(6, 0), ""),
3000 ],
3001 None,
3002 cx,
3003 );
3004 assert_eq!(
3005 buffer.read(cx).text(),
3006 "
3007 a
3008 b()
3009 c()
3010 "
3011 .unindent()
3012 );
3013 });
3014 assert_eq!(
3015 editor.selections.ranges(cx),
3016 &[
3017 Point::new(1, 2)..Point::new(1, 2),
3018 Point::new(2, 2)..Point::new(2, 2),
3019 ],
3020 );
3021
3022 editor.newline(&Newline, window, cx);
3023 assert_eq!(
3024 editor.text(cx),
3025 "
3026 a
3027 b(
3028 )
3029 c(
3030 )
3031 "
3032 .unindent()
3033 );
3034
3035 // The selections are moved after the inserted newlines
3036 assert_eq!(
3037 editor.selections.ranges(cx),
3038 &[
3039 Point::new(2, 0)..Point::new(2, 0),
3040 Point::new(4, 0)..Point::new(4, 0),
3041 ],
3042 );
3043 });
3044}
3045
3046#[gpui::test]
3047async fn test_newline_above(cx: &mut TestAppContext) {
3048 init_test(cx, |settings| {
3049 settings.defaults.tab_size = NonZeroU32::new(4)
3050 });
3051
3052 let language = Arc::new(
3053 Language::new(
3054 LanguageConfig::default(),
3055 Some(tree_sitter_rust::LANGUAGE.into()),
3056 )
3057 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3058 .unwrap(),
3059 );
3060
3061 let mut cx = EditorTestContext::new(cx).await;
3062 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3063 cx.set_state(indoc! {"
3064 const a: ˇA = (
3065 (ˇ
3066 «const_functionˇ»(ˇ),
3067 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3068 )ˇ
3069 ˇ);ˇ
3070 "});
3071
3072 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
3073 cx.assert_editor_state(indoc! {"
3074 ˇ
3075 const a: A = (
3076 ˇ
3077 (
3078 ˇ
3079 ˇ
3080 const_function(),
3081 ˇ
3082 ˇ
3083 ˇ
3084 ˇ
3085 something_else,
3086 ˇ
3087 )
3088 ˇ
3089 ˇ
3090 );
3091 "});
3092}
3093
3094#[gpui::test]
3095async fn test_newline_below(cx: &mut TestAppContext) {
3096 init_test(cx, |settings| {
3097 settings.defaults.tab_size = NonZeroU32::new(4)
3098 });
3099
3100 let language = Arc::new(
3101 Language::new(
3102 LanguageConfig::default(),
3103 Some(tree_sitter_rust::LANGUAGE.into()),
3104 )
3105 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3106 .unwrap(),
3107 );
3108
3109 let mut cx = EditorTestContext::new(cx).await;
3110 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3111 cx.set_state(indoc! {"
3112 const a: ˇA = (
3113 (ˇ
3114 «const_functionˇ»(ˇ),
3115 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3116 )ˇ
3117 ˇ);ˇ
3118 "});
3119
3120 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
3121 cx.assert_editor_state(indoc! {"
3122 const a: A = (
3123 ˇ
3124 (
3125 ˇ
3126 const_function(),
3127 ˇ
3128 ˇ
3129 something_else,
3130 ˇ
3131 ˇ
3132 ˇ
3133 ˇ
3134 )
3135 ˇ
3136 );
3137 ˇ
3138 ˇ
3139 "});
3140}
3141
3142#[gpui::test]
3143async fn test_newline_comments(cx: &mut TestAppContext) {
3144 init_test(cx, |settings| {
3145 settings.defaults.tab_size = NonZeroU32::new(4)
3146 });
3147
3148 let language = Arc::new(Language::new(
3149 LanguageConfig {
3150 line_comments: vec!["// ".into()],
3151 ..LanguageConfig::default()
3152 },
3153 None,
3154 ));
3155 {
3156 let mut cx = EditorTestContext::new(cx).await;
3157 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3158 cx.set_state(indoc! {"
3159 // Fooˇ
3160 "});
3161
3162 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3163 cx.assert_editor_state(indoc! {"
3164 // Foo
3165 // ˇ
3166 "});
3167 // Ensure that we add comment prefix when existing line contains space
3168 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3169 cx.assert_editor_state(
3170 indoc! {"
3171 // Foo
3172 //s
3173 // ˇ
3174 "}
3175 .replace("s", " ") // s is used as space placeholder to prevent format on save
3176 .as_str(),
3177 );
3178 // Ensure that we add comment prefix when existing line does not contain space
3179 cx.set_state(indoc! {"
3180 // Foo
3181 //ˇ
3182 "});
3183 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3184 cx.assert_editor_state(indoc! {"
3185 // Foo
3186 //
3187 // ˇ
3188 "});
3189 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
3190 cx.set_state(indoc! {"
3191 ˇ// Foo
3192 "});
3193 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3194 cx.assert_editor_state(indoc! {"
3195
3196 ˇ// Foo
3197 "});
3198 }
3199 // Ensure that comment continuations can be disabled.
3200 update_test_language_settings(cx, |settings| {
3201 settings.defaults.extend_comment_on_newline = Some(false);
3202 });
3203 let mut cx = EditorTestContext::new(cx).await;
3204 cx.set_state(indoc! {"
3205 // Fooˇ
3206 "});
3207 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3208 cx.assert_editor_state(indoc! {"
3209 // Foo
3210 ˇ
3211 "});
3212}
3213
3214#[gpui::test]
3215async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
3216 init_test(cx, |settings| {
3217 settings.defaults.tab_size = NonZeroU32::new(4)
3218 });
3219
3220 let language = Arc::new(Language::new(
3221 LanguageConfig {
3222 line_comments: vec!["// ".into(), "/// ".into()],
3223 ..LanguageConfig::default()
3224 },
3225 None,
3226 ));
3227 {
3228 let mut cx = EditorTestContext::new(cx).await;
3229 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3230 cx.set_state(indoc! {"
3231 //ˇ
3232 "});
3233 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3234 cx.assert_editor_state(indoc! {"
3235 //
3236 // ˇ
3237 "});
3238
3239 cx.set_state(indoc! {"
3240 ///ˇ
3241 "});
3242 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3243 cx.assert_editor_state(indoc! {"
3244 ///
3245 /// ˇ
3246 "});
3247 }
3248}
3249
3250#[gpui::test]
3251async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
3252 init_test(cx, |settings| {
3253 settings.defaults.tab_size = NonZeroU32::new(4)
3254 });
3255
3256 let language = Arc::new(
3257 Language::new(
3258 LanguageConfig {
3259 documentation_comment: Some(language::BlockCommentConfig {
3260 start: "/**".into(),
3261 end: "*/".into(),
3262 prefix: "* ".into(),
3263 tab_size: 1,
3264 }),
3265
3266 ..LanguageConfig::default()
3267 },
3268 Some(tree_sitter_rust::LANGUAGE.into()),
3269 )
3270 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
3271 .unwrap(),
3272 );
3273
3274 {
3275 let mut cx = EditorTestContext::new(cx).await;
3276 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3277 cx.set_state(indoc! {"
3278 /**ˇ
3279 "});
3280
3281 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3282 cx.assert_editor_state(indoc! {"
3283 /**
3284 * ˇ
3285 "});
3286 // Ensure that if cursor is before the comment start,
3287 // we do not actually insert a comment prefix.
3288 cx.set_state(indoc! {"
3289 ˇ/**
3290 "});
3291 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3292 cx.assert_editor_state(indoc! {"
3293
3294 ˇ/**
3295 "});
3296 // Ensure that if cursor is between it doesn't add comment prefix.
3297 cx.set_state(indoc! {"
3298 /*ˇ*
3299 "});
3300 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3301 cx.assert_editor_state(indoc! {"
3302 /*
3303 ˇ*
3304 "});
3305 // Ensure that if suffix exists on same line after cursor it adds new line.
3306 cx.set_state(indoc! {"
3307 /**ˇ*/
3308 "});
3309 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3310 cx.assert_editor_state(indoc! {"
3311 /**
3312 * ˇ
3313 */
3314 "});
3315 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3316 cx.set_state(indoc! {"
3317 /**ˇ */
3318 "});
3319 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3320 cx.assert_editor_state(indoc! {"
3321 /**
3322 * ˇ
3323 */
3324 "});
3325 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3326 cx.set_state(indoc! {"
3327 /** ˇ*/
3328 "});
3329 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3330 cx.assert_editor_state(
3331 indoc! {"
3332 /**s
3333 * ˇ
3334 */
3335 "}
3336 .replace("s", " ") // s is used as space placeholder to prevent format on save
3337 .as_str(),
3338 );
3339 // Ensure that delimiter space is preserved when newline on already
3340 // spaced delimiter.
3341 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3342 cx.assert_editor_state(
3343 indoc! {"
3344 /**s
3345 *s
3346 * ˇ
3347 */
3348 "}
3349 .replace("s", " ") // s is used as space placeholder to prevent format on save
3350 .as_str(),
3351 );
3352 // Ensure that delimiter space is preserved when space is not
3353 // on existing delimiter.
3354 cx.set_state(indoc! {"
3355 /**
3356 *ˇ
3357 */
3358 "});
3359 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3360 cx.assert_editor_state(indoc! {"
3361 /**
3362 *
3363 * ˇ
3364 */
3365 "});
3366 // Ensure that if suffix exists on same line after cursor it
3367 // doesn't add extra new line if prefix is not on same line.
3368 cx.set_state(indoc! {"
3369 /**
3370 ˇ*/
3371 "});
3372 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3373 cx.assert_editor_state(indoc! {"
3374 /**
3375
3376 ˇ*/
3377 "});
3378 // Ensure that it detects suffix after existing prefix.
3379 cx.set_state(indoc! {"
3380 /**ˇ/
3381 "});
3382 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3383 cx.assert_editor_state(indoc! {"
3384 /**
3385 ˇ/
3386 "});
3387 // Ensure that if suffix exists on same line before
3388 // cursor it does not add comment prefix.
3389 cx.set_state(indoc! {"
3390 /** */ˇ
3391 "});
3392 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3393 cx.assert_editor_state(indoc! {"
3394 /** */
3395 ˇ
3396 "});
3397 // Ensure that if suffix exists on same line before
3398 // cursor it does not add comment prefix.
3399 cx.set_state(indoc! {"
3400 /**
3401 *
3402 */ˇ
3403 "});
3404 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3405 cx.assert_editor_state(indoc! {"
3406 /**
3407 *
3408 */
3409 ˇ
3410 "});
3411
3412 // Ensure that inline comment followed by code
3413 // doesn't add comment prefix on newline
3414 cx.set_state(indoc! {"
3415 /** */ textˇ
3416 "});
3417 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3418 cx.assert_editor_state(indoc! {"
3419 /** */ text
3420 ˇ
3421 "});
3422
3423 // Ensure that text after comment end tag
3424 // doesn't add comment prefix on newline
3425 cx.set_state(indoc! {"
3426 /**
3427 *
3428 */ˇtext
3429 "});
3430 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3431 cx.assert_editor_state(indoc! {"
3432 /**
3433 *
3434 */
3435 ˇtext
3436 "});
3437
3438 // Ensure if not comment block it doesn't
3439 // add comment prefix on newline
3440 cx.set_state(indoc! {"
3441 * textˇ
3442 "});
3443 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3444 cx.assert_editor_state(indoc! {"
3445 * text
3446 ˇ
3447 "});
3448 }
3449 // Ensure that comment continuations can be disabled.
3450 update_test_language_settings(cx, |settings| {
3451 settings.defaults.extend_comment_on_newline = Some(false);
3452 });
3453 let mut cx = EditorTestContext::new(cx).await;
3454 cx.set_state(indoc! {"
3455 /**ˇ
3456 "});
3457 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3458 cx.assert_editor_state(indoc! {"
3459 /**
3460 ˇ
3461 "});
3462}
3463
3464#[gpui::test]
3465async fn test_newline_comments_with_block_comment(cx: &mut TestAppContext) {
3466 init_test(cx, |settings| {
3467 settings.defaults.tab_size = NonZeroU32::new(4)
3468 });
3469
3470 let lua_language = Arc::new(Language::new(
3471 LanguageConfig {
3472 line_comments: vec!["--".into()],
3473 block_comment: Some(language::BlockCommentConfig {
3474 start: "--[[".into(),
3475 prefix: "".into(),
3476 end: "]]".into(),
3477 tab_size: 0,
3478 }),
3479 ..LanguageConfig::default()
3480 },
3481 None,
3482 ));
3483
3484 let mut cx = EditorTestContext::new(cx).await;
3485 cx.update_buffer(|buffer, cx| buffer.set_language(Some(lua_language), cx));
3486
3487 // Line with line comment should extend
3488 cx.set_state(indoc! {"
3489 --ˇ
3490 "});
3491 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3492 cx.assert_editor_state(indoc! {"
3493 --
3494 --ˇ
3495 "});
3496
3497 // Line with block comment that matches line comment should not extend
3498 cx.set_state(indoc! {"
3499 --[[ˇ
3500 "});
3501 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3502 cx.assert_editor_state(indoc! {"
3503 --[[
3504 ˇ
3505 "});
3506}
3507
3508#[gpui::test]
3509fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3510 init_test(cx, |_| {});
3511
3512 let editor = cx.add_window(|window, cx| {
3513 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3514 let mut editor = build_editor(buffer, window, cx);
3515 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3516 s.select_ranges([3..4, 11..12, 19..20])
3517 });
3518 editor
3519 });
3520
3521 _ = editor.update(cx, |editor, window, cx| {
3522 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3523 editor.buffer.update(cx, |buffer, cx| {
3524 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3525 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3526 });
3527 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
3528
3529 editor.insert("Z", window, cx);
3530 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3531
3532 // The selections are moved after the inserted characters
3533 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
3534 });
3535}
3536
3537#[gpui::test]
3538async fn test_tab(cx: &mut TestAppContext) {
3539 init_test(cx, |settings| {
3540 settings.defaults.tab_size = NonZeroU32::new(3)
3541 });
3542
3543 let mut cx = EditorTestContext::new(cx).await;
3544 cx.set_state(indoc! {"
3545 ˇabˇc
3546 ˇ🏀ˇ🏀ˇefg
3547 dˇ
3548 "});
3549 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3550 cx.assert_editor_state(indoc! {"
3551 ˇab ˇc
3552 ˇ🏀 ˇ🏀 ˇefg
3553 d ˇ
3554 "});
3555
3556 cx.set_state(indoc! {"
3557 a
3558 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3559 "});
3560 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3561 cx.assert_editor_state(indoc! {"
3562 a
3563 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3564 "});
3565}
3566
3567#[gpui::test]
3568async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3569 init_test(cx, |_| {});
3570
3571 let mut cx = EditorTestContext::new(cx).await;
3572 let language = Arc::new(
3573 Language::new(
3574 LanguageConfig::default(),
3575 Some(tree_sitter_rust::LANGUAGE.into()),
3576 )
3577 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3578 .unwrap(),
3579 );
3580 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3581
3582 // test when all cursors are not at suggested indent
3583 // then simply move to their suggested indent location
3584 cx.set_state(indoc! {"
3585 const a: B = (
3586 c(
3587 ˇ
3588 ˇ )
3589 );
3590 "});
3591 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3592 cx.assert_editor_state(indoc! {"
3593 const a: B = (
3594 c(
3595 ˇ
3596 ˇ)
3597 );
3598 "});
3599
3600 // test cursor already at suggested indent not moving when
3601 // other cursors are yet to reach their suggested indents
3602 cx.set_state(indoc! {"
3603 ˇ
3604 const a: B = (
3605 c(
3606 d(
3607 ˇ
3608 )
3609 ˇ
3610 ˇ )
3611 );
3612 "});
3613 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3614 cx.assert_editor_state(indoc! {"
3615 ˇ
3616 const a: B = (
3617 c(
3618 d(
3619 ˇ
3620 )
3621 ˇ
3622 ˇ)
3623 );
3624 "});
3625 // test when all cursors are at suggested indent then tab is inserted
3626 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3627 cx.assert_editor_state(indoc! {"
3628 ˇ
3629 const a: B = (
3630 c(
3631 d(
3632 ˇ
3633 )
3634 ˇ
3635 ˇ)
3636 );
3637 "});
3638
3639 // test when current indent is less than suggested indent,
3640 // we adjust line to match suggested indent and move cursor to it
3641 //
3642 // when no other cursor is at word boundary, all of them should move
3643 cx.set_state(indoc! {"
3644 const a: B = (
3645 c(
3646 d(
3647 ˇ
3648 ˇ )
3649 ˇ )
3650 );
3651 "});
3652 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3653 cx.assert_editor_state(indoc! {"
3654 const a: B = (
3655 c(
3656 d(
3657 ˇ
3658 ˇ)
3659 ˇ)
3660 );
3661 "});
3662
3663 // test when current indent is less than suggested indent,
3664 // we adjust line to match suggested indent and move cursor to it
3665 //
3666 // when some other cursor is at word boundary, it should not move
3667 cx.set_state(indoc! {"
3668 const a: B = (
3669 c(
3670 d(
3671 ˇ
3672 ˇ )
3673 ˇ)
3674 );
3675 "});
3676 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3677 cx.assert_editor_state(indoc! {"
3678 const a: B = (
3679 c(
3680 d(
3681 ˇ
3682 ˇ)
3683 ˇ)
3684 );
3685 "});
3686
3687 // test when current indent is more than suggested indent,
3688 // we just move cursor to current indent instead of suggested indent
3689 //
3690 // when no other cursor is at word boundary, all of them should move
3691 cx.set_state(indoc! {"
3692 const a: B = (
3693 c(
3694 d(
3695 ˇ
3696 ˇ )
3697 ˇ )
3698 );
3699 "});
3700 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3701 cx.assert_editor_state(indoc! {"
3702 const a: B = (
3703 c(
3704 d(
3705 ˇ
3706 ˇ)
3707 ˇ)
3708 );
3709 "});
3710 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3711 cx.assert_editor_state(indoc! {"
3712 const a: B = (
3713 c(
3714 d(
3715 ˇ
3716 ˇ)
3717 ˇ)
3718 );
3719 "});
3720
3721 // test when current indent is more than suggested indent,
3722 // we just move cursor to current indent instead of suggested indent
3723 //
3724 // when some other cursor is at word boundary, it doesn't move
3725 cx.set_state(indoc! {"
3726 const a: B = (
3727 c(
3728 d(
3729 ˇ
3730 ˇ )
3731 ˇ)
3732 );
3733 "});
3734 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3735 cx.assert_editor_state(indoc! {"
3736 const a: B = (
3737 c(
3738 d(
3739 ˇ
3740 ˇ)
3741 ˇ)
3742 );
3743 "});
3744
3745 // handle auto-indent when there are multiple cursors on the same line
3746 cx.set_state(indoc! {"
3747 const a: B = (
3748 c(
3749 ˇ ˇ
3750 ˇ )
3751 );
3752 "});
3753 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3754 cx.assert_editor_state(indoc! {"
3755 const a: B = (
3756 c(
3757 ˇ
3758 ˇ)
3759 );
3760 "});
3761}
3762
3763#[gpui::test]
3764async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3765 init_test(cx, |settings| {
3766 settings.defaults.tab_size = NonZeroU32::new(3)
3767 });
3768
3769 let mut cx = EditorTestContext::new(cx).await;
3770 cx.set_state(indoc! {"
3771 ˇ
3772 \t ˇ
3773 \t ˇ
3774 \t ˇ
3775 \t \t\t \t \t\t \t\t \t \t ˇ
3776 "});
3777
3778 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3779 cx.assert_editor_state(indoc! {"
3780 ˇ
3781 \t ˇ
3782 \t ˇ
3783 \t ˇ
3784 \t \t\t \t \t\t \t\t \t \t ˇ
3785 "});
3786}
3787
3788#[gpui::test]
3789async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3790 init_test(cx, |settings| {
3791 settings.defaults.tab_size = NonZeroU32::new(4)
3792 });
3793
3794 let language = Arc::new(
3795 Language::new(
3796 LanguageConfig::default(),
3797 Some(tree_sitter_rust::LANGUAGE.into()),
3798 )
3799 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3800 .unwrap(),
3801 );
3802
3803 let mut cx = EditorTestContext::new(cx).await;
3804 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3805 cx.set_state(indoc! {"
3806 fn a() {
3807 if b {
3808 \t ˇc
3809 }
3810 }
3811 "});
3812
3813 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3814 cx.assert_editor_state(indoc! {"
3815 fn a() {
3816 if b {
3817 ˇc
3818 }
3819 }
3820 "});
3821}
3822
3823#[gpui::test]
3824async fn test_indent_outdent(cx: &mut TestAppContext) {
3825 init_test(cx, |settings| {
3826 settings.defaults.tab_size = NonZeroU32::new(4);
3827 });
3828
3829 let mut cx = EditorTestContext::new(cx).await;
3830
3831 cx.set_state(indoc! {"
3832 «oneˇ» «twoˇ»
3833 three
3834 four
3835 "});
3836 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3837 cx.assert_editor_state(indoc! {"
3838 «oneˇ» «twoˇ»
3839 three
3840 four
3841 "});
3842
3843 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3844 cx.assert_editor_state(indoc! {"
3845 «oneˇ» «twoˇ»
3846 three
3847 four
3848 "});
3849
3850 // select across line ending
3851 cx.set_state(indoc! {"
3852 one two
3853 t«hree
3854 ˇ» four
3855 "});
3856 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3857 cx.assert_editor_state(indoc! {"
3858 one two
3859 t«hree
3860 ˇ» four
3861 "});
3862
3863 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3864 cx.assert_editor_state(indoc! {"
3865 one two
3866 t«hree
3867 ˇ» four
3868 "});
3869
3870 // Ensure that indenting/outdenting works when the cursor is at column 0.
3871 cx.set_state(indoc! {"
3872 one two
3873 ˇthree
3874 four
3875 "});
3876 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3877 cx.assert_editor_state(indoc! {"
3878 one two
3879 ˇthree
3880 four
3881 "});
3882
3883 cx.set_state(indoc! {"
3884 one two
3885 ˇ three
3886 four
3887 "});
3888 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3889 cx.assert_editor_state(indoc! {"
3890 one two
3891 ˇthree
3892 four
3893 "});
3894}
3895
3896#[gpui::test]
3897async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) {
3898 // This is a regression test for issue #33761
3899 init_test(cx, |_| {});
3900
3901 let mut cx = EditorTestContext::new(cx).await;
3902 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3903 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3904
3905 cx.set_state(
3906 r#"ˇ# ingress:
3907ˇ# api:
3908ˇ# enabled: false
3909ˇ# pathType: Prefix
3910ˇ# console:
3911ˇ# enabled: false
3912ˇ# pathType: Prefix
3913"#,
3914 );
3915
3916 // Press tab to indent all lines
3917 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3918
3919 cx.assert_editor_state(
3920 r#" ˇ# ingress:
3921 ˇ# api:
3922 ˇ# enabled: false
3923 ˇ# pathType: Prefix
3924 ˇ# console:
3925 ˇ# enabled: false
3926 ˇ# pathType: Prefix
3927"#,
3928 );
3929}
3930
3931#[gpui::test]
3932async fn test_indent_yaml_non_comments_with_multiple_cursors(cx: &mut TestAppContext) {
3933 // This is a test to make sure our fix for issue #33761 didn't break anything
3934 init_test(cx, |_| {});
3935
3936 let mut cx = EditorTestContext::new(cx).await;
3937 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3938 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3939
3940 cx.set_state(
3941 r#"ˇingress:
3942ˇ api:
3943ˇ enabled: false
3944ˇ pathType: Prefix
3945"#,
3946 );
3947
3948 // Press tab to indent all lines
3949 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3950
3951 cx.assert_editor_state(
3952 r#"ˇingress:
3953 ˇapi:
3954 ˇenabled: false
3955 ˇpathType: Prefix
3956"#,
3957 );
3958}
3959
3960#[gpui::test]
3961async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3962 init_test(cx, |settings| {
3963 settings.defaults.hard_tabs = Some(true);
3964 });
3965
3966 let mut cx = EditorTestContext::new(cx).await;
3967
3968 // select two ranges on one line
3969 cx.set_state(indoc! {"
3970 «oneˇ» «twoˇ»
3971 three
3972 four
3973 "});
3974 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3975 cx.assert_editor_state(indoc! {"
3976 \t«oneˇ» «twoˇ»
3977 three
3978 four
3979 "});
3980 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3981 cx.assert_editor_state(indoc! {"
3982 \t\t«oneˇ» «twoˇ»
3983 three
3984 four
3985 "});
3986 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3987 cx.assert_editor_state(indoc! {"
3988 \t«oneˇ» «twoˇ»
3989 three
3990 four
3991 "});
3992 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3993 cx.assert_editor_state(indoc! {"
3994 «oneˇ» «twoˇ»
3995 three
3996 four
3997 "});
3998
3999 // select across a line ending
4000 cx.set_state(indoc! {"
4001 one two
4002 t«hree
4003 ˇ»four
4004 "});
4005 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4006 cx.assert_editor_state(indoc! {"
4007 one two
4008 \tt«hree
4009 ˇ»four
4010 "});
4011 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4012 cx.assert_editor_state(indoc! {"
4013 one two
4014 \t\tt«hree
4015 ˇ»four
4016 "});
4017 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4018 cx.assert_editor_state(indoc! {"
4019 one two
4020 \tt«hree
4021 ˇ»four
4022 "});
4023 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4024 cx.assert_editor_state(indoc! {"
4025 one two
4026 t«hree
4027 ˇ»four
4028 "});
4029
4030 // Ensure that indenting/outdenting works when the cursor is at column 0.
4031 cx.set_state(indoc! {"
4032 one two
4033 ˇthree
4034 four
4035 "});
4036 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4037 cx.assert_editor_state(indoc! {"
4038 one two
4039 ˇthree
4040 four
4041 "});
4042 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4043 cx.assert_editor_state(indoc! {"
4044 one two
4045 \tˇthree
4046 four
4047 "});
4048 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4049 cx.assert_editor_state(indoc! {"
4050 one two
4051 ˇthree
4052 four
4053 "});
4054}
4055
4056#[gpui::test]
4057fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
4058 init_test(cx, |settings| {
4059 settings.languages.0.extend([
4060 (
4061 "TOML".into(),
4062 LanguageSettingsContent {
4063 tab_size: NonZeroU32::new(2),
4064 ..Default::default()
4065 },
4066 ),
4067 (
4068 "Rust".into(),
4069 LanguageSettingsContent {
4070 tab_size: NonZeroU32::new(4),
4071 ..Default::default()
4072 },
4073 ),
4074 ]);
4075 });
4076
4077 let toml_language = Arc::new(Language::new(
4078 LanguageConfig {
4079 name: "TOML".into(),
4080 ..Default::default()
4081 },
4082 None,
4083 ));
4084 let rust_language = Arc::new(Language::new(
4085 LanguageConfig {
4086 name: "Rust".into(),
4087 ..Default::default()
4088 },
4089 None,
4090 ));
4091
4092 let toml_buffer =
4093 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
4094 let rust_buffer =
4095 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
4096 let multibuffer = cx.new(|cx| {
4097 let mut multibuffer = MultiBuffer::new(ReadWrite);
4098 multibuffer.push_excerpts(
4099 toml_buffer.clone(),
4100 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
4101 cx,
4102 );
4103 multibuffer.push_excerpts(
4104 rust_buffer.clone(),
4105 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
4106 cx,
4107 );
4108 multibuffer
4109 });
4110
4111 cx.add_window(|window, cx| {
4112 let mut editor = build_editor(multibuffer, window, cx);
4113
4114 assert_eq!(
4115 editor.text(cx),
4116 indoc! {"
4117 a = 1
4118 b = 2
4119
4120 const c: usize = 3;
4121 "}
4122 );
4123
4124 select_ranges(
4125 &mut editor,
4126 indoc! {"
4127 «aˇ» = 1
4128 b = 2
4129
4130 «const c:ˇ» usize = 3;
4131 "},
4132 window,
4133 cx,
4134 );
4135
4136 editor.tab(&Tab, window, cx);
4137 assert_text_with_selections(
4138 &mut editor,
4139 indoc! {"
4140 «aˇ» = 1
4141 b = 2
4142
4143 «const c:ˇ» usize = 3;
4144 "},
4145 cx,
4146 );
4147 editor.backtab(&Backtab, window, cx);
4148 assert_text_with_selections(
4149 &mut editor,
4150 indoc! {"
4151 «aˇ» = 1
4152 b = 2
4153
4154 «const c:ˇ» usize = 3;
4155 "},
4156 cx,
4157 );
4158
4159 editor
4160 });
4161}
4162
4163#[gpui::test]
4164async fn test_backspace(cx: &mut TestAppContext) {
4165 init_test(cx, |_| {});
4166
4167 let mut cx = EditorTestContext::new(cx).await;
4168
4169 // Basic backspace
4170 cx.set_state(indoc! {"
4171 onˇe two three
4172 fou«rˇ» five six
4173 seven «ˇeight nine
4174 »ten
4175 "});
4176 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4177 cx.assert_editor_state(indoc! {"
4178 oˇe two three
4179 fouˇ five six
4180 seven ˇten
4181 "});
4182
4183 // Test backspace inside and around indents
4184 cx.set_state(indoc! {"
4185 zero
4186 ˇone
4187 ˇtwo
4188 ˇ ˇ ˇ three
4189 ˇ ˇ four
4190 "});
4191 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4192 cx.assert_editor_state(indoc! {"
4193 zero
4194 ˇone
4195 ˇtwo
4196 ˇ threeˇ four
4197 "});
4198}
4199
4200#[gpui::test]
4201async fn test_delete(cx: &mut TestAppContext) {
4202 init_test(cx, |_| {});
4203
4204 let mut cx = EditorTestContext::new(cx).await;
4205 cx.set_state(indoc! {"
4206 onˇe two three
4207 fou«rˇ» five six
4208 seven «ˇeight nine
4209 »ten
4210 "});
4211 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
4212 cx.assert_editor_state(indoc! {"
4213 onˇ two three
4214 fouˇ five six
4215 seven ˇten
4216 "});
4217}
4218
4219#[gpui::test]
4220fn test_delete_line(cx: &mut TestAppContext) {
4221 init_test(cx, |_| {});
4222
4223 let editor = cx.add_window(|window, cx| {
4224 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4225 build_editor(buffer, window, cx)
4226 });
4227 _ = editor.update(cx, |editor, window, cx| {
4228 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4229 s.select_display_ranges([
4230 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4231 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4232 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4233 ])
4234 });
4235 editor.delete_line(&DeleteLine, window, cx);
4236 assert_eq!(editor.display_text(cx), "ghi");
4237 assert_eq!(
4238 editor.selections.display_ranges(cx),
4239 vec![
4240 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
4241 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
4242 ]
4243 );
4244 });
4245
4246 let editor = cx.add_window(|window, cx| {
4247 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4248 build_editor(buffer, window, cx)
4249 });
4250 _ = editor.update(cx, |editor, window, cx| {
4251 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4252 s.select_display_ranges([
4253 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
4254 ])
4255 });
4256 editor.delete_line(&DeleteLine, window, cx);
4257 assert_eq!(editor.display_text(cx), "ghi\n");
4258 assert_eq!(
4259 editor.selections.display_ranges(cx),
4260 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
4261 );
4262 });
4263}
4264
4265#[gpui::test]
4266fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
4267 init_test(cx, |_| {});
4268
4269 cx.add_window(|window, cx| {
4270 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4271 let mut editor = build_editor(buffer.clone(), window, cx);
4272 let buffer = buffer.read(cx).as_singleton().unwrap();
4273
4274 assert_eq!(
4275 editor.selections.ranges::<Point>(cx),
4276 &[Point::new(0, 0)..Point::new(0, 0)]
4277 );
4278
4279 // When on single line, replace newline at end by space
4280 editor.join_lines(&JoinLines, window, cx);
4281 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4282 assert_eq!(
4283 editor.selections.ranges::<Point>(cx),
4284 &[Point::new(0, 3)..Point::new(0, 3)]
4285 );
4286
4287 // When multiple lines are selected, remove newlines that are spanned by the selection
4288 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4289 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
4290 });
4291 editor.join_lines(&JoinLines, window, cx);
4292 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
4293 assert_eq!(
4294 editor.selections.ranges::<Point>(cx),
4295 &[Point::new(0, 11)..Point::new(0, 11)]
4296 );
4297
4298 // Undo should be transactional
4299 editor.undo(&Undo, window, cx);
4300 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4301 assert_eq!(
4302 editor.selections.ranges::<Point>(cx),
4303 &[Point::new(0, 5)..Point::new(2, 2)]
4304 );
4305
4306 // When joining an empty line don't insert a space
4307 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4308 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
4309 });
4310 editor.join_lines(&JoinLines, window, cx);
4311 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
4312 assert_eq!(
4313 editor.selections.ranges::<Point>(cx),
4314 [Point::new(2, 3)..Point::new(2, 3)]
4315 );
4316
4317 // We can remove trailing newlines
4318 editor.join_lines(&JoinLines, window, cx);
4319 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4320 assert_eq!(
4321 editor.selections.ranges::<Point>(cx),
4322 [Point::new(2, 3)..Point::new(2, 3)]
4323 );
4324
4325 // We don't blow up on the last line
4326 editor.join_lines(&JoinLines, window, cx);
4327 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4328 assert_eq!(
4329 editor.selections.ranges::<Point>(cx),
4330 [Point::new(2, 3)..Point::new(2, 3)]
4331 );
4332
4333 // reset to test indentation
4334 editor.buffer.update(cx, |buffer, cx| {
4335 buffer.edit(
4336 [
4337 (Point::new(1, 0)..Point::new(1, 2), " "),
4338 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
4339 ],
4340 None,
4341 cx,
4342 )
4343 });
4344
4345 // We remove any leading spaces
4346 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
4347 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4348 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
4349 });
4350 editor.join_lines(&JoinLines, window, cx);
4351 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
4352
4353 // We don't insert a space for a line containing only spaces
4354 editor.join_lines(&JoinLines, window, cx);
4355 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
4356
4357 // We ignore any leading tabs
4358 editor.join_lines(&JoinLines, window, cx);
4359 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
4360
4361 editor
4362 });
4363}
4364
4365#[gpui::test]
4366fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
4367 init_test(cx, |_| {});
4368
4369 cx.add_window(|window, cx| {
4370 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4371 let mut editor = build_editor(buffer.clone(), window, cx);
4372 let buffer = buffer.read(cx).as_singleton().unwrap();
4373
4374 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4375 s.select_ranges([
4376 Point::new(0, 2)..Point::new(1, 1),
4377 Point::new(1, 2)..Point::new(1, 2),
4378 Point::new(3, 1)..Point::new(3, 2),
4379 ])
4380 });
4381
4382 editor.join_lines(&JoinLines, window, cx);
4383 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
4384
4385 assert_eq!(
4386 editor.selections.ranges::<Point>(cx),
4387 [
4388 Point::new(0, 7)..Point::new(0, 7),
4389 Point::new(1, 3)..Point::new(1, 3)
4390 ]
4391 );
4392 editor
4393 });
4394}
4395
4396#[gpui::test]
4397async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
4398 init_test(cx, |_| {});
4399
4400 let mut cx = EditorTestContext::new(cx).await;
4401
4402 let diff_base = r#"
4403 Line 0
4404 Line 1
4405 Line 2
4406 Line 3
4407 "#
4408 .unindent();
4409
4410 cx.set_state(
4411 &r#"
4412 ˇLine 0
4413 Line 1
4414 Line 2
4415 Line 3
4416 "#
4417 .unindent(),
4418 );
4419
4420 cx.set_head_text(&diff_base);
4421 executor.run_until_parked();
4422
4423 // Join lines
4424 cx.update_editor(|editor, window, cx| {
4425 editor.join_lines(&JoinLines, window, cx);
4426 });
4427 executor.run_until_parked();
4428
4429 cx.assert_editor_state(
4430 &r#"
4431 Line 0ˇ Line 1
4432 Line 2
4433 Line 3
4434 "#
4435 .unindent(),
4436 );
4437 // Join again
4438 cx.update_editor(|editor, window, cx| {
4439 editor.join_lines(&JoinLines, window, cx);
4440 });
4441 executor.run_until_parked();
4442
4443 cx.assert_editor_state(
4444 &r#"
4445 Line 0 Line 1ˇ Line 2
4446 Line 3
4447 "#
4448 .unindent(),
4449 );
4450}
4451
4452#[gpui::test]
4453async fn test_custom_newlines_cause_no_false_positive_diffs(
4454 executor: BackgroundExecutor,
4455 cx: &mut TestAppContext,
4456) {
4457 init_test(cx, |_| {});
4458 let mut cx = EditorTestContext::new(cx).await;
4459 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
4460 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
4461 executor.run_until_parked();
4462
4463 cx.update_editor(|editor, window, cx| {
4464 let snapshot = editor.snapshot(window, cx);
4465 assert_eq!(
4466 snapshot
4467 .buffer_snapshot
4468 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
4469 .collect::<Vec<_>>(),
4470 Vec::new(),
4471 "Should not have any diffs for files with custom newlines"
4472 );
4473 });
4474}
4475
4476#[gpui::test]
4477async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
4478 init_test(cx, |_| {});
4479
4480 let mut cx = EditorTestContext::new(cx).await;
4481
4482 // Test sort_lines_case_insensitive()
4483 cx.set_state(indoc! {"
4484 «z
4485 y
4486 x
4487 Z
4488 Y
4489 Xˇ»
4490 "});
4491 cx.update_editor(|e, window, cx| {
4492 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
4493 });
4494 cx.assert_editor_state(indoc! {"
4495 «x
4496 X
4497 y
4498 Y
4499 z
4500 Zˇ»
4501 "});
4502
4503 // Test sort_lines_by_length()
4504 //
4505 // Demonstrates:
4506 // - ∞ is 3 bytes UTF-8, but sorted by its char count (1)
4507 // - sort is stable
4508 cx.set_state(indoc! {"
4509 «123
4510 æ
4511 12
4512 ∞
4513 1
4514 æˇ»
4515 "});
4516 cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
4517 cx.assert_editor_state(indoc! {"
4518 «æ
4519 ∞
4520 1
4521 æ
4522 12
4523 123ˇ»
4524 "});
4525
4526 // Test reverse_lines()
4527 cx.set_state(indoc! {"
4528 «5
4529 4
4530 3
4531 2
4532 1ˇ»
4533 "});
4534 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4535 cx.assert_editor_state(indoc! {"
4536 «1
4537 2
4538 3
4539 4
4540 5ˇ»
4541 "});
4542
4543 // Skip testing shuffle_line()
4544
4545 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4546 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4547
4548 // Don't manipulate when cursor is on single line, but expand the selection
4549 cx.set_state(indoc! {"
4550 ddˇdd
4551 ccc
4552 bb
4553 a
4554 "});
4555 cx.update_editor(|e, window, cx| {
4556 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4557 });
4558 cx.assert_editor_state(indoc! {"
4559 «ddddˇ»
4560 ccc
4561 bb
4562 a
4563 "});
4564
4565 // Basic manipulate case
4566 // Start selection moves to column 0
4567 // End of selection shrinks to fit shorter line
4568 cx.set_state(indoc! {"
4569 dd«d
4570 ccc
4571 bb
4572 aaaaaˇ»
4573 "});
4574 cx.update_editor(|e, window, cx| {
4575 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4576 });
4577 cx.assert_editor_state(indoc! {"
4578 «aaaaa
4579 bb
4580 ccc
4581 dddˇ»
4582 "});
4583
4584 // Manipulate case with newlines
4585 cx.set_state(indoc! {"
4586 dd«d
4587 ccc
4588
4589 bb
4590 aaaaa
4591
4592 ˇ»
4593 "});
4594 cx.update_editor(|e, window, cx| {
4595 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4596 });
4597 cx.assert_editor_state(indoc! {"
4598 «
4599
4600 aaaaa
4601 bb
4602 ccc
4603 dddˇ»
4604
4605 "});
4606
4607 // Adding new line
4608 cx.set_state(indoc! {"
4609 aa«a
4610 bbˇ»b
4611 "});
4612 cx.update_editor(|e, window, cx| {
4613 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4614 });
4615 cx.assert_editor_state(indoc! {"
4616 «aaa
4617 bbb
4618 added_lineˇ»
4619 "});
4620
4621 // Removing line
4622 cx.set_state(indoc! {"
4623 aa«a
4624 bbbˇ»
4625 "});
4626 cx.update_editor(|e, window, cx| {
4627 e.manipulate_immutable_lines(window, cx, |lines| {
4628 lines.pop();
4629 })
4630 });
4631 cx.assert_editor_state(indoc! {"
4632 «aaaˇ»
4633 "});
4634
4635 // Removing all lines
4636 cx.set_state(indoc! {"
4637 aa«a
4638 bbbˇ»
4639 "});
4640 cx.update_editor(|e, window, cx| {
4641 e.manipulate_immutable_lines(window, cx, |lines| {
4642 lines.drain(..);
4643 })
4644 });
4645 cx.assert_editor_state(indoc! {"
4646 ˇ
4647 "});
4648}
4649
4650#[gpui::test]
4651async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4652 init_test(cx, |_| {});
4653
4654 let mut cx = EditorTestContext::new(cx).await;
4655
4656 // Consider continuous selection as single selection
4657 cx.set_state(indoc! {"
4658 Aaa«aa
4659 cˇ»c«c
4660 bb
4661 aaaˇ»aa
4662 "});
4663 cx.update_editor(|e, window, cx| {
4664 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4665 });
4666 cx.assert_editor_state(indoc! {"
4667 «Aaaaa
4668 ccc
4669 bb
4670 aaaaaˇ»
4671 "});
4672
4673 cx.set_state(indoc! {"
4674 Aaa«aa
4675 cˇ»c«c
4676 bb
4677 aaaˇ»aa
4678 "});
4679 cx.update_editor(|e, window, cx| {
4680 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4681 });
4682 cx.assert_editor_state(indoc! {"
4683 «Aaaaa
4684 ccc
4685 bbˇ»
4686 "});
4687
4688 // Consider non continuous selection as distinct dedup operations
4689 cx.set_state(indoc! {"
4690 «aaaaa
4691 bb
4692 aaaaa
4693 aaaaaˇ»
4694
4695 aaa«aaˇ»
4696 "});
4697 cx.update_editor(|e, window, cx| {
4698 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4699 });
4700 cx.assert_editor_state(indoc! {"
4701 «aaaaa
4702 bbˇ»
4703
4704 «aaaaaˇ»
4705 "});
4706}
4707
4708#[gpui::test]
4709async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4710 init_test(cx, |_| {});
4711
4712 let mut cx = EditorTestContext::new(cx).await;
4713
4714 cx.set_state(indoc! {"
4715 «Aaa
4716 aAa
4717 Aaaˇ»
4718 "});
4719 cx.update_editor(|e, window, cx| {
4720 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4721 });
4722 cx.assert_editor_state(indoc! {"
4723 «Aaa
4724 aAaˇ»
4725 "});
4726
4727 cx.set_state(indoc! {"
4728 «Aaa
4729 aAa
4730 aaAˇ»
4731 "});
4732 cx.update_editor(|e, window, cx| {
4733 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4734 });
4735 cx.assert_editor_state(indoc! {"
4736 «Aaaˇ»
4737 "});
4738}
4739
4740#[gpui::test]
4741async fn test_wrap_in_tag_single_selection(cx: &mut TestAppContext) {
4742 init_test(cx, |_| {});
4743
4744 let mut cx = EditorTestContext::new(cx).await;
4745
4746 let js_language = Arc::new(Language::new(
4747 LanguageConfig {
4748 name: "JavaScript".into(),
4749 wrap_characters: Some(language::WrapCharactersConfig {
4750 start_prefix: "<".into(),
4751 start_suffix: ">".into(),
4752 end_prefix: "</".into(),
4753 end_suffix: ">".into(),
4754 }),
4755 ..LanguageConfig::default()
4756 },
4757 None,
4758 ));
4759
4760 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
4761
4762 cx.set_state(indoc! {"
4763 «testˇ»
4764 "});
4765 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4766 cx.assert_editor_state(indoc! {"
4767 <«ˇ»>test</«ˇ»>
4768 "});
4769
4770 cx.set_state(indoc! {"
4771 «test
4772 testˇ»
4773 "});
4774 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4775 cx.assert_editor_state(indoc! {"
4776 <«ˇ»>test
4777 test</«ˇ»>
4778 "});
4779
4780 cx.set_state(indoc! {"
4781 teˇst
4782 "});
4783 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4784 cx.assert_editor_state(indoc! {"
4785 te<«ˇ»></«ˇ»>st
4786 "});
4787}
4788
4789#[gpui::test]
4790async fn test_wrap_in_tag_multi_selection(cx: &mut TestAppContext) {
4791 init_test(cx, |_| {});
4792
4793 let mut cx = EditorTestContext::new(cx).await;
4794
4795 let js_language = Arc::new(Language::new(
4796 LanguageConfig {
4797 name: "JavaScript".into(),
4798 wrap_characters: Some(language::WrapCharactersConfig {
4799 start_prefix: "<".into(),
4800 start_suffix: ">".into(),
4801 end_prefix: "</".into(),
4802 end_suffix: ">".into(),
4803 }),
4804 ..LanguageConfig::default()
4805 },
4806 None,
4807 ));
4808
4809 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
4810
4811 cx.set_state(indoc! {"
4812 «testˇ»
4813 «testˇ» «testˇ»
4814 «testˇ»
4815 "});
4816 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4817 cx.assert_editor_state(indoc! {"
4818 <«ˇ»>test</«ˇ»>
4819 <«ˇ»>test</«ˇ»> <«ˇ»>test</«ˇ»>
4820 <«ˇ»>test</«ˇ»>
4821 "});
4822
4823 cx.set_state(indoc! {"
4824 «test
4825 testˇ»
4826 «test
4827 testˇ»
4828 "});
4829 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4830 cx.assert_editor_state(indoc! {"
4831 <«ˇ»>test
4832 test</«ˇ»>
4833 <«ˇ»>test
4834 test</«ˇ»>
4835 "});
4836}
4837
4838#[gpui::test]
4839async fn test_wrap_in_tag_does_nothing_in_unsupported_languages(cx: &mut TestAppContext) {
4840 init_test(cx, |_| {});
4841
4842 let mut cx = EditorTestContext::new(cx).await;
4843
4844 let plaintext_language = Arc::new(Language::new(
4845 LanguageConfig {
4846 name: "Plain Text".into(),
4847 ..LanguageConfig::default()
4848 },
4849 None,
4850 ));
4851
4852 cx.update_buffer(|buffer, cx| buffer.set_language(Some(plaintext_language), cx));
4853
4854 cx.set_state(indoc! {"
4855 «testˇ»
4856 "});
4857 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4858 cx.assert_editor_state(indoc! {"
4859 «testˇ»
4860 "});
4861}
4862
4863#[gpui::test]
4864async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
4865 init_test(cx, |_| {});
4866
4867 let mut cx = EditorTestContext::new(cx).await;
4868
4869 // Manipulate with multiple selections on a single line
4870 cx.set_state(indoc! {"
4871 dd«dd
4872 cˇ»c«c
4873 bb
4874 aaaˇ»aa
4875 "});
4876 cx.update_editor(|e, window, cx| {
4877 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4878 });
4879 cx.assert_editor_state(indoc! {"
4880 «aaaaa
4881 bb
4882 ccc
4883 ddddˇ»
4884 "});
4885
4886 // Manipulate with multiple disjoin selections
4887 cx.set_state(indoc! {"
4888 5«
4889 4
4890 3
4891 2
4892 1ˇ»
4893
4894 dd«dd
4895 ccc
4896 bb
4897 aaaˇ»aa
4898 "});
4899 cx.update_editor(|e, window, cx| {
4900 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4901 });
4902 cx.assert_editor_state(indoc! {"
4903 «1
4904 2
4905 3
4906 4
4907 5ˇ»
4908
4909 «aaaaa
4910 bb
4911 ccc
4912 ddddˇ»
4913 "});
4914
4915 // Adding lines on each selection
4916 cx.set_state(indoc! {"
4917 2«
4918 1ˇ»
4919
4920 bb«bb
4921 aaaˇ»aa
4922 "});
4923 cx.update_editor(|e, window, cx| {
4924 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
4925 });
4926 cx.assert_editor_state(indoc! {"
4927 «2
4928 1
4929 added lineˇ»
4930
4931 «bbbb
4932 aaaaa
4933 added lineˇ»
4934 "});
4935
4936 // Removing lines on each selection
4937 cx.set_state(indoc! {"
4938 2«
4939 1ˇ»
4940
4941 bb«bb
4942 aaaˇ»aa
4943 "});
4944 cx.update_editor(|e, window, cx| {
4945 e.manipulate_immutable_lines(window, cx, |lines| {
4946 lines.pop();
4947 })
4948 });
4949 cx.assert_editor_state(indoc! {"
4950 «2ˇ»
4951
4952 «bbbbˇ»
4953 "});
4954}
4955
4956#[gpui::test]
4957async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
4958 init_test(cx, |settings| {
4959 settings.defaults.tab_size = NonZeroU32::new(3)
4960 });
4961
4962 let mut cx = EditorTestContext::new(cx).await;
4963
4964 // MULTI SELECTION
4965 // Ln.1 "«" tests empty lines
4966 // Ln.9 tests just leading whitespace
4967 cx.set_state(indoc! {"
4968 «
4969 abc // No indentationˇ»
4970 «\tabc // 1 tabˇ»
4971 \t\tabc « ˇ» // 2 tabs
4972 \t ab«c // Tab followed by space
4973 \tabc // Space followed by tab (3 spaces should be the result)
4974 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4975 abˇ»ˇc ˇ ˇ // Already space indented«
4976 \t
4977 \tabc\tdef // Only the leading tab is manipulatedˇ»
4978 "});
4979 cx.update_editor(|e, window, cx| {
4980 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4981 });
4982 cx.assert_editor_state(
4983 indoc! {"
4984 «
4985 abc // No indentation
4986 abc // 1 tab
4987 abc // 2 tabs
4988 abc // Tab followed by space
4989 abc // Space followed by tab (3 spaces should be the result)
4990 abc // Mixed indentation (tab conversion depends on the column)
4991 abc // Already space indented
4992 ·
4993 abc\tdef // Only the leading tab is manipulatedˇ»
4994 "}
4995 .replace("·", "")
4996 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4997 );
4998
4999 // Test on just a few lines, the others should remain unchanged
5000 // Only lines (3, 5, 10, 11) should change
5001 cx.set_state(
5002 indoc! {"
5003 ·
5004 abc // No indentation
5005 \tabcˇ // 1 tab
5006 \t\tabc // 2 tabs
5007 \t abcˇ // Tab followed by space
5008 \tabc // Space followed by tab (3 spaces should be the result)
5009 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5010 abc // Already space indented
5011 «\t
5012 \tabc\tdef // Only the leading tab is manipulatedˇ»
5013 "}
5014 .replace("·", "")
5015 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5016 );
5017 cx.update_editor(|e, window, cx| {
5018 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5019 });
5020 cx.assert_editor_state(
5021 indoc! {"
5022 ·
5023 abc // No indentation
5024 « abc // 1 tabˇ»
5025 \t\tabc // 2 tabs
5026 « abc // Tab followed by spaceˇ»
5027 \tabc // Space followed by tab (3 spaces should be the result)
5028 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5029 abc // Already space indented
5030 « ·
5031 abc\tdef // Only the leading tab is manipulatedˇ»
5032 "}
5033 .replace("·", "")
5034 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5035 );
5036
5037 // SINGLE SELECTION
5038 // Ln.1 "«" tests empty lines
5039 // Ln.9 tests just leading whitespace
5040 cx.set_state(indoc! {"
5041 «
5042 abc // No indentation
5043 \tabc // 1 tab
5044 \t\tabc // 2 tabs
5045 \t abc // Tab followed by space
5046 \tabc // Space followed by tab (3 spaces should be the result)
5047 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5048 abc // Already space indented
5049 \t
5050 \tabc\tdef // Only the leading tab is manipulatedˇ»
5051 "});
5052 cx.update_editor(|e, window, cx| {
5053 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5054 });
5055 cx.assert_editor_state(
5056 indoc! {"
5057 «
5058 abc // No indentation
5059 abc // 1 tab
5060 abc // 2 tabs
5061 abc // Tab followed by space
5062 abc // Space followed by tab (3 spaces should be the result)
5063 abc // Mixed indentation (tab conversion depends on the column)
5064 abc // Already space indented
5065 ·
5066 abc\tdef // Only the leading tab is manipulatedˇ»
5067 "}
5068 .replace("·", "")
5069 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5070 );
5071}
5072
5073#[gpui::test]
5074async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
5075 init_test(cx, |settings| {
5076 settings.defaults.tab_size = NonZeroU32::new(3)
5077 });
5078
5079 let mut cx = EditorTestContext::new(cx).await;
5080
5081 // MULTI SELECTION
5082 // Ln.1 "«" tests empty lines
5083 // Ln.11 tests just leading whitespace
5084 cx.set_state(indoc! {"
5085 «
5086 abˇ»ˇc // No indentation
5087 abc ˇ ˇ // 1 space (< 3 so dont convert)
5088 abc « // 2 spaces (< 3 so dont convert)
5089 abc // 3 spaces (convert)
5090 abc ˇ» // 5 spaces (1 tab + 2 spaces)
5091 «\tˇ»\t«\tˇ»abc // Already tab indented
5092 «\t abc // Tab followed by space
5093 \tabc // Space followed by tab (should be consumed due to tab)
5094 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5095 \tˇ» «\t
5096 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
5097 "});
5098 cx.update_editor(|e, window, cx| {
5099 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5100 });
5101 cx.assert_editor_state(indoc! {"
5102 «
5103 abc // No indentation
5104 abc // 1 space (< 3 so dont convert)
5105 abc // 2 spaces (< 3 so dont convert)
5106 \tabc // 3 spaces (convert)
5107 \t abc // 5 spaces (1 tab + 2 spaces)
5108 \t\t\tabc // Already tab indented
5109 \t abc // Tab followed by space
5110 \tabc // Space followed by tab (should be consumed due to tab)
5111 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5112 \t\t\t
5113 \tabc \t // Only the leading spaces should be convertedˇ»
5114 "});
5115
5116 // Test on just a few lines, the other should remain unchanged
5117 // Only lines (4, 8, 11, 12) should change
5118 cx.set_state(
5119 indoc! {"
5120 ·
5121 abc // No indentation
5122 abc // 1 space (< 3 so dont convert)
5123 abc // 2 spaces (< 3 so dont convert)
5124 « abc // 3 spaces (convert)ˇ»
5125 abc // 5 spaces (1 tab + 2 spaces)
5126 \t\t\tabc // Already tab indented
5127 \t abc // Tab followed by space
5128 \tabc ˇ // Space followed by tab (should be consumed due to tab)
5129 \t\t \tabc // Mixed indentation
5130 \t \t \t \tabc // Mixed indentation
5131 \t \tˇ
5132 « abc \t // Only the leading spaces should be convertedˇ»
5133 "}
5134 .replace("·", "")
5135 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5136 );
5137 cx.update_editor(|e, window, cx| {
5138 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5139 });
5140 cx.assert_editor_state(
5141 indoc! {"
5142 ·
5143 abc // No indentation
5144 abc // 1 space (< 3 so dont convert)
5145 abc // 2 spaces (< 3 so dont convert)
5146 «\tabc // 3 spaces (convert)ˇ»
5147 abc // 5 spaces (1 tab + 2 spaces)
5148 \t\t\tabc // Already tab indented
5149 \t abc // Tab followed by space
5150 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
5151 \t\t \tabc // Mixed indentation
5152 \t \t \t \tabc // Mixed indentation
5153 «\t\t\t
5154 \tabc \t // Only the leading spaces should be convertedˇ»
5155 "}
5156 .replace("·", "")
5157 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5158 );
5159
5160 // SINGLE SELECTION
5161 // Ln.1 "«" tests empty lines
5162 // Ln.11 tests just leading whitespace
5163 cx.set_state(indoc! {"
5164 «
5165 abc // No indentation
5166 abc // 1 space (< 3 so dont convert)
5167 abc // 2 spaces (< 3 so dont convert)
5168 abc // 3 spaces (convert)
5169 abc // 5 spaces (1 tab + 2 spaces)
5170 \t\t\tabc // Already tab indented
5171 \t abc // Tab followed by space
5172 \tabc // Space followed by tab (should be consumed due to tab)
5173 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5174 \t \t
5175 abc \t // Only the leading spaces should be convertedˇ»
5176 "});
5177 cx.update_editor(|e, window, cx| {
5178 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5179 });
5180 cx.assert_editor_state(indoc! {"
5181 «
5182 abc // No indentation
5183 abc // 1 space (< 3 so dont convert)
5184 abc // 2 spaces (< 3 so dont convert)
5185 \tabc // 3 spaces (convert)
5186 \t abc // 5 spaces (1 tab + 2 spaces)
5187 \t\t\tabc // Already tab indented
5188 \t abc // Tab followed by space
5189 \tabc // Space followed by tab (should be consumed due to tab)
5190 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5191 \t\t\t
5192 \tabc \t // Only the leading spaces should be convertedˇ»
5193 "});
5194}
5195
5196#[gpui::test]
5197async fn test_toggle_case(cx: &mut TestAppContext) {
5198 init_test(cx, |_| {});
5199
5200 let mut cx = EditorTestContext::new(cx).await;
5201
5202 // If all lower case -> upper case
5203 cx.set_state(indoc! {"
5204 «hello worldˇ»
5205 "});
5206 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5207 cx.assert_editor_state(indoc! {"
5208 «HELLO WORLDˇ»
5209 "});
5210
5211 // If all upper case -> lower case
5212 cx.set_state(indoc! {"
5213 «HELLO WORLDˇ»
5214 "});
5215 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5216 cx.assert_editor_state(indoc! {"
5217 «hello worldˇ»
5218 "});
5219
5220 // If any upper case characters are identified -> lower case
5221 // This matches JetBrains IDEs
5222 cx.set_state(indoc! {"
5223 «hEllo worldˇ»
5224 "});
5225 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5226 cx.assert_editor_state(indoc! {"
5227 «hello worldˇ»
5228 "});
5229}
5230
5231#[gpui::test]
5232async fn test_convert_to_sentence_case(cx: &mut TestAppContext) {
5233 init_test(cx, |_| {});
5234
5235 let mut cx = EditorTestContext::new(cx).await;
5236
5237 cx.set_state(indoc! {"
5238 «implement-windows-supportˇ»
5239 "});
5240 cx.update_editor(|e, window, cx| {
5241 e.convert_to_sentence_case(&ConvertToSentenceCase, window, cx)
5242 });
5243 cx.assert_editor_state(indoc! {"
5244 «Implement windows supportˇ»
5245 "});
5246}
5247
5248#[gpui::test]
5249async fn test_manipulate_text(cx: &mut TestAppContext) {
5250 init_test(cx, |_| {});
5251
5252 let mut cx = EditorTestContext::new(cx).await;
5253
5254 // Test convert_to_upper_case()
5255 cx.set_state(indoc! {"
5256 «hello worldˇ»
5257 "});
5258 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5259 cx.assert_editor_state(indoc! {"
5260 «HELLO WORLDˇ»
5261 "});
5262
5263 // Test convert_to_lower_case()
5264 cx.set_state(indoc! {"
5265 «HELLO WORLDˇ»
5266 "});
5267 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
5268 cx.assert_editor_state(indoc! {"
5269 «hello worldˇ»
5270 "});
5271
5272 // Test multiple line, single selection case
5273 cx.set_state(indoc! {"
5274 «The quick brown
5275 fox jumps over
5276 the lazy dogˇ»
5277 "});
5278 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
5279 cx.assert_editor_state(indoc! {"
5280 «The Quick Brown
5281 Fox Jumps Over
5282 The Lazy Dogˇ»
5283 "});
5284
5285 // Test multiple line, single selection case
5286 cx.set_state(indoc! {"
5287 «The quick brown
5288 fox jumps over
5289 the lazy dogˇ»
5290 "});
5291 cx.update_editor(|e, window, cx| {
5292 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
5293 });
5294 cx.assert_editor_state(indoc! {"
5295 «TheQuickBrown
5296 FoxJumpsOver
5297 TheLazyDogˇ»
5298 "});
5299
5300 // From here on out, test more complex cases of manipulate_text()
5301
5302 // Test no selection case - should affect words cursors are in
5303 // Cursor at beginning, middle, and end of word
5304 cx.set_state(indoc! {"
5305 ˇhello big beauˇtiful worldˇ
5306 "});
5307 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5308 cx.assert_editor_state(indoc! {"
5309 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
5310 "});
5311
5312 // Test multiple selections on a single line and across multiple lines
5313 cx.set_state(indoc! {"
5314 «Theˇ» quick «brown
5315 foxˇ» jumps «overˇ»
5316 the «lazyˇ» dog
5317 "});
5318 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5319 cx.assert_editor_state(indoc! {"
5320 «THEˇ» quick «BROWN
5321 FOXˇ» jumps «OVERˇ»
5322 the «LAZYˇ» dog
5323 "});
5324
5325 // Test case where text length grows
5326 cx.set_state(indoc! {"
5327 «tschüߡ»
5328 "});
5329 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5330 cx.assert_editor_state(indoc! {"
5331 «TSCHÜSSˇ»
5332 "});
5333
5334 // Test to make sure we don't crash when text shrinks
5335 cx.set_state(indoc! {"
5336 aaa_bbbˇ
5337 "});
5338 cx.update_editor(|e, window, cx| {
5339 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5340 });
5341 cx.assert_editor_state(indoc! {"
5342 «aaaBbbˇ»
5343 "});
5344
5345 // Test to make sure we all aware of the fact that each word can grow and shrink
5346 // Final selections should be aware of this fact
5347 cx.set_state(indoc! {"
5348 aaa_bˇbb bbˇb_ccc ˇccc_ddd
5349 "});
5350 cx.update_editor(|e, window, cx| {
5351 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5352 });
5353 cx.assert_editor_state(indoc! {"
5354 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
5355 "});
5356
5357 cx.set_state(indoc! {"
5358 «hElLo, WoRld!ˇ»
5359 "});
5360 cx.update_editor(|e, window, cx| {
5361 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
5362 });
5363 cx.assert_editor_state(indoc! {"
5364 «HeLlO, wOrLD!ˇ»
5365 "});
5366
5367 // Test selections with `line_mode = true`.
5368 cx.update_editor(|editor, _window, _cx| editor.selections.line_mode = true);
5369 cx.set_state(indoc! {"
5370 «The quick brown
5371 fox jumps over
5372 tˇ»he lazy dog
5373 "});
5374 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5375 cx.assert_editor_state(indoc! {"
5376 «THE QUICK BROWN
5377 FOX JUMPS OVER
5378 THE LAZY DOGˇ»
5379 "});
5380}
5381
5382#[gpui::test]
5383fn test_duplicate_line(cx: &mut TestAppContext) {
5384 init_test(cx, |_| {});
5385
5386 let editor = cx.add_window(|window, cx| {
5387 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5388 build_editor(buffer, window, cx)
5389 });
5390 _ = editor.update(cx, |editor, window, cx| {
5391 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5392 s.select_display_ranges([
5393 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5394 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5395 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5396 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5397 ])
5398 });
5399 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5400 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5401 assert_eq!(
5402 editor.selections.display_ranges(cx),
5403 vec![
5404 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
5405 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
5406 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5407 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5408 ]
5409 );
5410 });
5411
5412 let editor = cx.add_window(|window, cx| {
5413 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5414 build_editor(buffer, window, cx)
5415 });
5416 _ = editor.update(cx, |editor, window, cx| {
5417 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5418 s.select_display_ranges([
5419 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5420 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5421 ])
5422 });
5423 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5424 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5425 assert_eq!(
5426 editor.selections.display_ranges(cx),
5427 vec![
5428 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
5429 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
5430 ]
5431 );
5432 });
5433
5434 // With `move_upwards` the selections stay in place, except for
5435 // the lines inserted above them
5436 let editor = cx.add_window(|window, cx| {
5437 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5438 build_editor(buffer, window, cx)
5439 });
5440 _ = editor.update(cx, |editor, window, cx| {
5441 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5442 s.select_display_ranges([
5443 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5444 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5445 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5446 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5447 ])
5448 });
5449 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5450 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5451 assert_eq!(
5452 editor.selections.display_ranges(cx),
5453 vec![
5454 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5455 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5456 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
5457 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5458 ]
5459 );
5460 });
5461
5462 let editor = cx.add_window(|window, cx| {
5463 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5464 build_editor(buffer, window, cx)
5465 });
5466 _ = editor.update(cx, |editor, window, cx| {
5467 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5468 s.select_display_ranges([
5469 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5470 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5471 ])
5472 });
5473 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5474 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5475 assert_eq!(
5476 editor.selections.display_ranges(cx),
5477 vec![
5478 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5479 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5480 ]
5481 );
5482 });
5483
5484 let editor = cx.add_window(|window, cx| {
5485 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5486 build_editor(buffer, window, cx)
5487 });
5488 _ = editor.update(cx, |editor, window, cx| {
5489 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5490 s.select_display_ranges([
5491 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5492 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5493 ])
5494 });
5495 editor.duplicate_selection(&DuplicateSelection, window, cx);
5496 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
5497 assert_eq!(
5498 editor.selections.display_ranges(cx),
5499 vec![
5500 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5501 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
5502 ]
5503 );
5504 });
5505}
5506
5507#[gpui::test]
5508fn test_move_line_up_down(cx: &mut TestAppContext) {
5509 init_test(cx, |_| {});
5510
5511 let editor = cx.add_window(|window, cx| {
5512 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5513 build_editor(buffer, window, cx)
5514 });
5515 _ = editor.update(cx, |editor, window, cx| {
5516 editor.fold_creases(
5517 vec![
5518 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5519 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5520 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5521 ],
5522 true,
5523 window,
5524 cx,
5525 );
5526 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5527 s.select_display_ranges([
5528 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5529 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5530 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5531 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
5532 ])
5533 });
5534 assert_eq!(
5535 editor.display_text(cx),
5536 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
5537 );
5538
5539 editor.move_line_up(&MoveLineUp, window, cx);
5540 assert_eq!(
5541 editor.display_text(cx),
5542 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
5543 );
5544 assert_eq!(
5545 editor.selections.display_ranges(cx),
5546 vec![
5547 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5548 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5549 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5550 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5551 ]
5552 );
5553 });
5554
5555 _ = editor.update(cx, |editor, window, cx| {
5556 editor.move_line_down(&MoveLineDown, window, cx);
5557 assert_eq!(
5558 editor.display_text(cx),
5559 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
5560 );
5561 assert_eq!(
5562 editor.selections.display_ranges(cx),
5563 vec![
5564 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5565 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5566 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5567 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5568 ]
5569 );
5570 });
5571
5572 _ = editor.update(cx, |editor, window, cx| {
5573 editor.move_line_down(&MoveLineDown, window, cx);
5574 assert_eq!(
5575 editor.display_text(cx),
5576 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
5577 );
5578 assert_eq!(
5579 editor.selections.display_ranges(cx),
5580 vec![
5581 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5582 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5583 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5584 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5585 ]
5586 );
5587 });
5588
5589 _ = editor.update(cx, |editor, window, cx| {
5590 editor.move_line_up(&MoveLineUp, window, cx);
5591 assert_eq!(
5592 editor.display_text(cx),
5593 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
5594 );
5595 assert_eq!(
5596 editor.selections.display_ranges(cx),
5597 vec![
5598 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5599 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5600 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5601 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5602 ]
5603 );
5604 });
5605}
5606
5607#[gpui::test]
5608fn test_move_line_up_selection_at_end_of_fold(cx: &mut TestAppContext) {
5609 init_test(cx, |_| {});
5610 let editor = cx.add_window(|window, cx| {
5611 let buffer = MultiBuffer::build_simple("\n\n\n\n\n\naaaa\nbbbb\ncccc", cx);
5612 build_editor(buffer, window, cx)
5613 });
5614 _ = editor.update(cx, |editor, window, cx| {
5615 editor.fold_creases(
5616 vec![Crease::simple(
5617 Point::new(6, 4)..Point::new(7, 4),
5618 FoldPlaceholder::test(),
5619 )],
5620 true,
5621 window,
5622 cx,
5623 );
5624 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5625 s.select_ranges([Point::new(7, 4)..Point::new(7, 4)])
5626 });
5627 assert_eq!(editor.display_text(cx), "\n\n\n\n\n\naaaa⋯\ncccc");
5628 editor.move_line_up(&MoveLineUp, window, cx);
5629 let buffer_text = editor.buffer.read(cx).snapshot(cx).text();
5630 assert_eq!(buffer_text, "\n\n\n\n\naaaa\nbbbb\n\ncccc");
5631 });
5632}
5633
5634#[gpui::test]
5635fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
5636 init_test(cx, |_| {});
5637
5638 let editor = cx.add_window(|window, cx| {
5639 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5640 build_editor(buffer, window, cx)
5641 });
5642 _ = editor.update(cx, |editor, window, cx| {
5643 let snapshot = editor.buffer.read(cx).snapshot(cx);
5644 editor.insert_blocks(
5645 [BlockProperties {
5646 style: BlockStyle::Fixed,
5647 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
5648 height: Some(1),
5649 render: Arc::new(|_| div().into_any()),
5650 priority: 0,
5651 }],
5652 Some(Autoscroll::fit()),
5653 cx,
5654 );
5655 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5656 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
5657 });
5658 editor.move_line_down(&MoveLineDown, window, cx);
5659 });
5660}
5661
5662#[gpui::test]
5663async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
5664 init_test(cx, |_| {});
5665
5666 let mut cx = EditorTestContext::new(cx).await;
5667 cx.set_state(
5668 &"
5669 ˇzero
5670 one
5671 two
5672 three
5673 four
5674 five
5675 "
5676 .unindent(),
5677 );
5678
5679 // Create a four-line block that replaces three lines of text.
5680 cx.update_editor(|editor, window, cx| {
5681 let snapshot = editor.snapshot(window, cx);
5682 let snapshot = &snapshot.buffer_snapshot;
5683 let placement = BlockPlacement::Replace(
5684 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
5685 );
5686 editor.insert_blocks(
5687 [BlockProperties {
5688 placement,
5689 height: Some(4),
5690 style: BlockStyle::Sticky,
5691 render: Arc::new(|_| gpui::div().into_any_element()),
5692 priority: 0,
5693 }],
5694 None,
5695 cx,
5696 );
5697 });
5698
5699 // Move down so that the cursor touches the block.
5700 cx.update_editor(|editor, window, cx| {
5701 editor.move_down(&Default::default(), window, cx);
5702 });
5703 cx.assert_editor_state(
5704 &"
5705 zero
5706 «one
5707 two
5708 threeˇ»
5709 four
5710 five
5711 "
5712 .unindent(),
5713 );
5714
5715 // Move down past the block.
5716 cx.update_editor(|editor, window, cx| {
5717 editor.move_down(&Default::default(), window, cx);
5718 });
5719 cx.assert_editor_state(
5720 &"
5721 zero
5722 one
5723 two
5724 three
5725 ˇfour
5726 five
5727 "
5728 .unindent(),
5729 );
5730}
5731
5732#[gpui::test]
5733fn test_transpose(cx: &mut TestAppContext) {
5734 init_test(cx, |_| {});
5735
5736 _ = cx.add_window(|window, cx| {
5737 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
5738 editor.set_style(EditorStyle::default(), window, cx);
5739 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5740 s.select_ranges([1..1])
5741 });
5742 editor.transpose(&Default::default(), window, cx);
5743 assert_eq!(editor.text(cx), "bac");
5744 assert_eq!(editor.selections.ranges(cx), [2..2]);
5745
5746 editor.transpose(&Default::default(), window, cx);
5747 assert_eq!(editor.text(cx), "bca");
5748 assert_eq!(editor.selections.ranges(cx), [3..3]);
5749
5750 editor.transpose(&Default::default(), window, cx);
5751 assert_eq!(editor.text(cx), "bac");
5752 assert_eq!(editor.selections.ranges(cx), [3..3]);
5753
5754 editor
5755 });
5756
5757 _ = cx.add_window(|window, cx| {
5758 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5759 editor.set_style(EditorStyle::default(), window, cx);
5760 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5761 s.select_ranges([3..3])
5762 });
5763 editor.transpose(&Default::default(), window, cx);
5764 assert_eq!(editor.text(cx), "acb\nde");
5765 assert_eq!(editor.selections.ranges(cx), [3..3]);
5766
5767 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5768 s.select_ranges([4..4])
5769 });
5770 editor.transpose(&Default::default(), window, cx);
5771 assert_eq!(editor.text(cx), "acbd\ne");
5772 assert_eq!(editor.selections.ranges(cx), [5..5]);
5773
5774 editor.transpose(&Default::default(), window, cx);
5775 assert_eq!(editor.text(cx), "acbde\n");
5776 assert_eq!(editor.selections.ranges(cx), [6..6]);
5777
5778 editor.transpose(&Default::default(), window, cx);
5779 assert_eq!(editor.text(cx), "acbd\ne");
5780 assert_eq!(editor.selections.ranges(cx), [6..6]);
5781
5782 editor
5783 });
5784
5785 _ = cx.add_window(|window, cx| {
5786 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5787 editor.set_style(EditorStyle::default(), window, cx);
5788 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5789 s.select_ranges([1..1, 2..2, 4..4])
5790 });
5791 editor.transpose(&Default::default(), window, cx);
5792 assert_eq!(editor.text(cx), "bacd\ne");
5793 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
5794
5795 editor.transpose(&Default::default(), window, cx);
5796 assert_eq!(editor.text(cx), "bcade\n");
5797 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
5798
5799 editor.transpose(&Default::default(), window, cx);
5800 assert_eq!(editor.text(cx), "bcda\ne");
5801 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5802
5803 editor.transpose(&Default::default(), window, cx);
5804 assert_eq!(editor.text(cx), "bcade\n");
5805 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5806
5807 editor.transpose(&Default::default(), window, cx);
5808 assert_eq!(editor.text(cx), "bcaed\n");
5809 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
5810
5811 editor
5812 });
5813
5814 _ = cx.add_window(|window, cx| {
5815 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
5816 editor.set_style(EditorStyle::default(), window, cx);
5817 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5818 s.select_ranges([4..4])
5819 });
5820 editor.transpose(&Default::default(), window, cx);
5821 assert_eq!(editor.text(cx), "🏀🍐✋");
5822 assert_eq!(editor.selections.ranges(cx), [8..8]);
5823
5824 editor.transpose(&Default::default(), window, cx);
5825 assert_eq!(editor.text(cx), "🏀✋🍐");
5826 assert_eq!(editor.selections.ranges(cx), [11..11]);
5827
5828 editor.transpose(&Default::default(), window, cx);
5829 assert_eq!(editor.text(cx), "🏀🍐✋");
5830 assert_eq!(editor.selections.ranges(cx), [11..11]);
5831
5832 editor
5833 });
5834}
5835
5836#[gpui::test]
5837async fn test_rewrap(cx: &mut TestAppContext) {
5838 init_test(cx, |settings| {
5839 settings.languages.0.extend([
5840 (
5841 "Markdown".into(),
5842 LanguageSettingsContent {
5843 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5844 preferred_line_length: Some(40),
5845 ..Default::default()
5846 },
5847 ),
5848 (
5849 "Plain Text".into(),
5850 LanguageSettingsContent {
5851 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5852 preferred_line_length: Some(40),
5853 ..Default::default()
5854 },
5855 ),
5856 (
5857 "C++".into(),
5858 LanguageSettingsContent {
5859 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5860 preferred_line_length: Some(40),
5861 ..Default::default()
5862 },
5863 ),
5864 (
5865 "Python".into(),
5866 LanguageSettingsContent {
5867 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5868 preferred_line_length: Some(40),
5869 ..Default::default()
5870 },
5871 ),
5872 (
5873 "Rust".into(),
5874 LanguageSettingsContent {
5875 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5876 preferred_line_length: Some(40),
5877 ..Default::default()
5878 },
5879 ),
5880 ])
5881 });
5882
5883 let mut cx = EditorTestContext::new(cx).await;
5884
5885 let cpp_language = Arc::new(Language::new(
5886 LanguageConfig {
5887 name: "C++".into(),
5888 line_comments: vec!["// ".into()],
5889 ..LanguageConfig::default()
5890 },
5891 None,
5892 ));
5893 let python_language = Arc::new(Language::new(
5894 LanguageConfig {
5895 name: "Python".into(),
5896 line_comments: vec!["# ".into()],
5897 ..LanguageConfig::default()
5898 },
5899 None,
5900 ));
5901 let markdown_language = Arc::new(Language::new(
5902 LanguageConfig {
5903 name: "Markdown".into(),
5904 rewrap_prefixes: vec![
5905 regex::Regex::new("\\d+\\.\\s+").unwrap(),
5906 regex::Regex::new("[-*+]\\s+").unwrap(),
5907 ],
5908 ..LanguageConfig::default()
5909 },
5910 None,
5911 ));
5912 let rust_language = Arc::new(
5913 Language::new(
5914 LanguageConfig {
5915 name: "Rust".into(),
5916 line_comments: vec!["// ".into(), "/// ".into()],
5917 ..LanguageConfig::default()
5918 },
5919 Some(tree_sitter_rust::LANGUAGE.into()),
5920 )
5921 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
5922 .unwrap(),
5923 );
5924
5925 let plaintext_language = Arc::new(Language::new(
5926 LanguageConfig {
5927 name: "Plain Text".into(),
5928 ..LanguageConfig::default()
5929 },
5930 None,
5931 ));
5932
5933 // Test basic rewrapping of a long line with a cursor
5934 assert_rewrap(
5935 indoc! {"
5936 // ˇThis is a long comment that needs to be wrapped.
5937 "},
5938 indoc! {"
5939 // ˇThis is a long comment that needs to
5940 // be wrapped.
5941 "},
5942 cpp_language.clone(),
5943 &mut cx,
5944 );
5945
5946 // Test rewrapping a full selection
5947 assert_rewrap(
5948 indoc! {"
5949 «// This selected long comment needs to be wrapped.ˇ»"
5950 },
5951 indoc! {"
5952 «// This selected long comment needs to
5953 // be wrapped.ˇ»"
5954 },
5955 cpp_language.clone(),
5956 &mut cx,
5957 );
5958
5959 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
5960 assert_rewrap(
5961 indoc! {"
5962 // ˇThis is the first line.
5963 // Thisˇ is the second line.
5964 // This is the thirdˇ line, all part of one paragraph.
5965 "},
5966 indoc! {"
5967 // ˇThis is the first line. Thisˇ is the
5968 // second line. This is the thirdˇ line,
5969 // all part of one paragraph.
5970 "},
5971 cpp_language.clone(),
5972 &mut cx,
5973 );
5974
5975 // Test multiple cursors in different paragraphs trigger separate rewraps
5976 assert_rewrap(
5977 indoc! {"
5978 // ˇThis is the first paragraph, first line.
5979 // ˇThis is the first paragraph, second line.
5980
5981 // ˇThis is the second paragraph, first line.
5982 // ˇThis is the second paragraph, second line.
5983 "},
5984 indoc! {"
5985 // ˇThis is the first paragraph, first
5986 // line. ˇThis is the first paragraph,
5987 // second line.
5988
5989 // ˇThis is the second paragraph, first
5990 // line. ˇThis is the second paragraph,
5991 // second line.
5992 "},
5993 cpp_language.clone(),
5994 &mut cx,
5995 );
5996
5997 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
5998 assert_rewrap(
5999 indoc! {"
6000 «// A regular long long comment to be wrapped.
6001 /// A documentation long comment to be wrapped.ˇ»
6002 "},
6003 indoc! {"
6004 «// A regular long long comment to be
6005 // wrapped.
6006 /// A documentation long comment to be
6007 /// wrapped.ˇ»
6008 "},
6009 rust_language.clone(),
6010 &mut cx,
6011 );
6012
6013 // Test that change in indentation level trigger seperate rewraps
6014 assert_rewrap(
6015 indoc! {"
6016 fn foo() {
6017 «// This is a long comment at the base indent.
6018 // This is a long comment at the next indent.ˇ»
6019 }
6020 "},
6021 indoc! {"
6022 fn foo() {
6023 «// This is a long comment at the
6024 // base indent.
6025 // This is a long comment at the
6026 // next indent.ˇ»
6027 }
6028 "},
6029 rust_language.clone(),
6030 &mut cx,
6031 );
6032
6033 // Test that different comment prefix characters (e.g., '#') are handled correctly
6034 assert_rewrap(
6035 indoc! {"
6036 # ˇThis is a long comment using a pound sign.
6037 "},
6038 indoc! {"
6039 # ˇThis is a long comment using a pound
6040 # sign.
6041 "},
6042 python_language,
6043 &mut cx,
6044 );
6045
6046 // Test rewrapping only affects comments, not code even when selected
6047 assert_rewrap(
6048 indoc! {"
6049 «/// This doc comment is long and should be wrapped.
6050 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6051 "},
6052 indoc! {"
6053 «/// This doc comment is long and should
6054 /// be wrapped.
6055 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6056 "},
6057 rust_language.clone(),
6058 &mut cx,
6059 );
6060
6061 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
6062 assert_rewrap(
6063 indoc! {"
6064 # Header
6065
6066 A long long long line of markdown text to wrap.ˇ
6067 "},
6068 indoc! {"
6069 # Header
6070
6071 A long long long line of markdown text
6072 to wrap.ˇ
6073 "},
6074 markdown_language.clone(),
6075 &mut cx,
6076 );
6077
6078 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
6079 assert_rewrap(
6080 indoc! {"
6081 «1. This is a numbered list item that is very long and needs to be wrapped properly.
6082 2. This is a numbered list item that is very long and needs to be wrapped properly.
6083 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
6084 "},
6085 indoc! {"
6086 «1. This is a numbered list item that is
6087 very long and needs to be wrapped
6088 properly.
6089 2. This is a numbered list item that is
6090 very long and needs to be wrapped
6091 properly.
6092 - This is an unordered list item that is
6093 also very long and should not merge
6094 with the numbered item.ˇ»
6095 "},
6096 markdown_language.clone(),
6097 &mut cx,
6098 );
6099
6100 // Test that rewrapping add indents for rewrapping boundary if not exists already.
6101 assert_rewrap(
6102 indoc! {"
6103 «1. This is a numbered list item that is
6104 very long and needs to be wrapped
6105 properly.
6106 2. This is a numbered list item that is
6107 very long and needs to be wrapped
6108 properly.
6109 - This is an unordered list item that is
6110 also very long and should not merge with
6111 the numbered item.ˇ»
6112 "},
6113 indoc! {"
6114 «1. This is a numbered list item that is
6115 very long and needs to be wrapped
6116 properly.
6117 2. This is a numbered list item that is
6118 very long and needs to be wrapped
6119 properly.
6120 - This is an unordered list item that is
6121 also very long and should not merge
6122 with the numbered item.ˇ»
6123 "},
6124 markdown_language.clone(),
6125 &mut cx,
6126 );
6127
6128 // Test that rewrapping maintain indents even when they already exists.
6129 assert_rewrap(
6130 indoc! {"
6131 «1. This is a numbered list
6132 item that is very long and needs to be wrapped properly.
6133 2. This is a numbered list
6134 item that is very long and needs to be wrapped properly.
6135 - This is an unordered list item that is also very long and
6136 should not merge with the numbered item.ˇ»
6137 "},
6138 indoc! {"
6139 «1. This is a numbered list item that is
6140 very long and needs to be wrapped
6141 properly.
6142 2. This is a numbered list item that is
6143 very long and needs to be wrapped
6144 properly.
6145 - This is an unordered list item that is
6146 also very long and should not merge
6147 with the numbered item.ˇ»
6148 "},
6149 markdown_language,
6150 &mut cx,
6151 );
6152
6153 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
6154 assert_rewrap(
6155 indoc! {"
6156 ˇThis is a very long line of plain text that will be wrapped.
6157 "},
6158 indoc! {"
6159 ˇThis is a very long line of plain text
6160 that will be wrapped.
6161 "},
6162 plaintext_language.clone(),
6163 &mut cx,
6164 );
6165
6166 // Test that non-commented code acts as a paragraph boundary within a selection
6167 assert_rewrap(
6168 indoc! {"
6169 «// This is the first long comment block to be wrapped.
6170 fn my_func(a: u32);
6171 // This is the second long comment block to be wrapped.ˇ»
6172 "},
6173 indoc! {"
6174 «// This is the first long comment block
6175 // to be wrapped.
6176 fn my_func(a: u32);
6177 // This is the second long comment block
6178 // to be wrapped.ˇ»
6179 "},
6180 rust_language,
6181 &mut cx,
6182 );
6183
6184 // Test rewrapping multiple selections, including ones with blank lines or tabs
6185 assert_rewrap(
6186 indoc! {"
6187 «ˇThis is a very long line that will be wrapped.
6188
6189 This is another paragraph in the same selection.»
6190
6191 «\tThis is a very long indented line that will be wrapped.ˇ»
6192 "},
6193 indoc! {"
6194 «ˇThis is a very long line that will be
6195 wrapped.
6196
6197 This is another paragraph in the same
6198 selection.»
6199
6200 «\tThis is a very long indented line
6201 \tthat will be wrapped.ˇ»
6202 "},
6203 plaintext_language,
6204 &mut cx,
6205 );
6206
6207 // Test that an empty comment line acts as a paragraph boundary
6208 assert_rewrap(
6209 indoc! {"
6210 // ˇThis is a long comment that will be wrapped.
6211 //
6212 // And this is another long comment that will also be wrapped.ˇ
6213 "},
6214 indoc! {"
6215 // ˇThis is a long comment that will be
6216 // wrapped.
6217 //
6218 // And this is another long comment that
6219 // will also be wrapped.ˇ
6220 "},
6221 cpp_language,
6222 &mut cx,
6223 );
6224
6225 #[track_caller]
6226 fn assert_rewrap(
6227 unwrapped_text: &str,
6228 wrapped_text: &str,
6229 language: Arc<Language>,
6230 cx: &mut EditorTestContext,
6231 ) {
6232 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6233 cx.set_state(unwrapped_text);
6234 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6235 cx.assert_editor_state(wrapped_text);
6236 }
6237}
6238
6239#[gpui::test]
6240async fn test_rewrap_block_comments(cx: &mut TestAppContext) {
6241 init_test(cx, |settings| {
6242 settings.languages.0.extend([(
6243 "Rust".into(),
6244 LanguageSettingsContent {
6245 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6246 preferred_line_length: Some(40),
6247 ..Default::default()
6248 },
6249 )])
6250 });
6251
6252 let mut cx = EditorTestContext::new(cx).await;
6253
6254 let rust_lang = Arc::new(
6255 Language::new(
6256 LanguageConfig {
6257 name: "Rust".into(),
6258 line_comments: vec!["// ".into()],
6259 block_comment: Some(BlockCommentConfig {
6260 start: "/*".into(),
6261 end: "*/".into(),
6262 prefix: "* ".into(),
6263 tab_size: 1,
6264 }),
6265 documentation_comment: Some(BlockCommentConfig {
6266 start: "/**".into(),
6267 end: "*/".into(),
6268 prefix: "* ".into(),
6269 tab_size: 1,
6270 }),
6271
6272 ..LanguageConfig::default()
6273 },
6274 Some(tree_sitter_rust::LANGUAGE.into()),
6275 )
6276 .with_override_query("[(line_comment) (block_comment)] @comment.inclusive")
6277 .unwrap(),
6278 );
6279
6280 // regular block comment
6281 assert_rewrap(
6282 indoc! {"
6283 /*
6284 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6285 */
6286 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6287 "},
6288 indoc! {"
6289 /*
6290 *ˇ Lorem ipsum dolor sit amet,
6291 * consectetur adipiscing elit.
6292 */
6293 /*
6294 *ˇ Lorem ipsum dolor sit amet,
6295 * consectetur adipiscing elit.
6296 */
6297 "},
6298 rust_lang.clone(),
6299 &mut cx,
6300 );
6301
6302 // indent is respected
6303 assert_rewrap(
6304 indoc! {"
6305 {}
6306 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6307 "},
6308 indoc! {"
6309 {}
6310 /*
6311 *ˇ Lorem ipsum dolor sit amet,
6312 * consectetur adipiscing elit.
6313 */
6314 "},
6315 rust_lang.clone(),
6316 &mut cx,
6317 );
6318
6319 // short block comments with inline delimiters
6320 assert_rewrap(
6321 indoc! {"
6322 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6323 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6324 */
6325 /*
6326 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6327 "},
6328 indoc! {"
6329 /*
6330 *ˇ Lorem ipsum dolor sit amet,
6331 * consectetur adipiscing elit.
6332 */
6333 /*
6334 *ˇ Lorem ipsum dolor sit amet,
6335 * consectetur adipiscing elit.
6336 */
6337 /*
6338 *ˇ Lorem ipsum dolor sit amet,
6339 * consectetur adipiscing elit.
6340 */
6341 "},
6342 rust_lang.clone(),
6343 &mut cx,
6344 );
6345
6346 // multiline block comment with inline start/end delimiters
6347 assert_rewrap(
6348 indoc! {"
6349 /*ˇ Lorem ipsum dolor sit amet,
6350 * consectetur adipiscing elit. */
6351 "},
6352 indoc! {"
6353 /*
6354 *ˇ Lorem ipsum dolor sit amet,
6355 * consectetur adipiscing elit.
6356 */
6357 "},
6358 rust_lang.clone(),
6359 &mut cx,
6360 );
6361
6362 // block comment rewrap still respects paragraph bounds
6363 assert_rewrap(
6364 indoc! {"
6365 /*
6366 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6367 *
6368 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6369 */
6370 "},
6371 indoc! {"
6372 /*
6373 *ˇ Lorem ipsum dolor sit amet,
6374 * consectetur adipiscing elit.
6375 *
6376 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6377 */
6378 "},
6379 rust_lang.clone(),
6380 &mut cx,
6381 );
6382
6383 // documentation comments
6384 assert_rewrap(
6385 indoc! {"
6386 /**ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6387 /**
6388 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6389 */
6390 "},
6391 indoc! {"
6392 /**
6393 *ˇ Lorem ipsum dolor sit amet,
6394 * consectetur adipiscing elit.
6395 */
6396 /**
6397 *ˇ Lorem ipsum dolor sit amet,
6398 * consectetur adipiscing elit.
6399 */
6400 "},
6401 rust_lang.clone(),
6402 &mut cx,
6403 );
6404
6405 // different, adjacent comments
6406 assert_rewrap(
6407 indoc! {"
6408 /**
6409 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6410 */
6411 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6412 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6413 "},
6414 indoc! {"
6415 /**
6416 *ˇ Lorem ipsum dolor sit amet,
6417 * consectetur adipiscing elit.
6418 */
6419 /*
6420 *ˇ Lorem ipsum dolor sit amet,
6421 * consectetur adipiscing elit.
6422 */
6423 //ˇ Lorem ipsum dolor sit amet,
6424 // consectetur adipiscing elit.
6425 "},
6426 rust_lang.clone(),
6427 &mut cx,
6428 );
6429
6430 // selection w/ single short block comment
6431 assert_rewrap(
6432 indoc! {"
6433 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6434 "},
6435 indoc! {"
6436 «/*
6437 * Lorem ipsum dolor sit amet,
6438 * consectetur adipiscing elit.
6439 */ˇ»
6440 "},
6441 rust_lang.clone(),
6442 &mut cx,
6443 );
6444
6445 // rewrapping a single comment w/ abutting comments
6446 assert_rewrap(
6447 indoc! {"
6448 /* ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. */
6449 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6450 "},
6451 indoc! {"
6452 /*
6453 * ˇLorem ipsum dolor sit amet,
6454 * consectetur adipiscing elit.
6455 */
6456 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6457 "},
6458 rust_lang.clone(),
6459 &mut cx,
6460 );
6461
6462 // selection w/ non-abutting short block comments
6463 assert_rewrap(
6464 indoc! {"
6465 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6466
6467 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6468 "},
6469 indoc! {"
6470 «/*
6471 * Lorem ipsum dolor sit amet,
6472 * consectetur adipiscing elit.
6473 */
6474
6475 /*
6476 * Lorem ipsum dolor sit amet,
6477 * consectetur adipiscing elit.
6478 */ˇ»
6479 "},
6480 rust_lang.clone(),
6481 &mut cx,
6482 );
6483
6484 // selection of multiline block comments
6485 assert_rewrap(
6486 indoc! {"
6487 «/* Lorem ipsum dolor sit amet,
6488 * consectetur adipiscing elit. */ˇ»
6489 "},
6490 indoc! {"
6491 «/*
6492 * Lorem ipsum dolor sit amet,
6493 * consectetur adipiscing elit.
6494 */ˇ»
6495 "},
6496 rust_lang.clone(),
6497 &mut cx,
6498 );
6499
6500 // partial selection of multiline block comments
6501 assert_rewrap(
6502 indoc! {"
6503 «/* Lorem ipsum dolor sit amet,ˇ»
6504 * consectetur adipiscing elit. */
6505 /* Lorem ipsum dolor sit amet,
6506 «* consectetur adipiscing elit. */ˇ»
6507 "},
6508 indoc! {"
6509 «/*
6510 * Lorem ipsum dolor sit amet,ˇ»
6511 * consectetur adipiscing elit. */
6512 /* Lorem ipsum dolor sit amet,
6513 «* consectetur adipiscing elit.
6514 */ˇ»
6515 "},
6516 rust_lang.clone(),
6517 &mut cx,
6518 );
6519
6520 // selection w/ abutting short block comments
6521 // TODO: should not be combined; should rewrap as 2 comments
6522 assert_rewrap(
6523 indoc! {"
6524 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6525 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6526 "},
6527 // desired behavior:
6528 // indoc! {"
6529 // «/*
6530 // * Lorem ipsum dolor sit amet,
6531 // * consectetur adipiscing elit.
6532 // */
6533 // /*
6534 // * Lorem ipsum dolor sit amet,
6535 // * consectetur adipiscing elit.
6536 // */ˇ»
6537 // "},
6538 // actual behaviour:
6539 indoc! {"
6540 «/*
6541 * Lorem ipsum dolor sit amet,
6542 * consectetur adipiscing elit. Lorem
6543 * ipsum dolor sit amet, consectetur
6544 * adipiscing elit.
6545 */ˇ»
6546 "},
6547 rust_lang.clone(),
6548 &mut cx,
6549 );
6550
6551 // TODO: same as above, but with delimiters on separate line
6552 // assert_rewrap(
6553 // indoc! {"
6554 // «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6555 // */
6556 // /*
6557 // * Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6558 // "},
6559 // // desired:
6560 // // indoc! {"
6561 // // «/*
6562 // // * Lorem ipsum dolor sit amet,
6563 // // * consectetur adipiscing elit.
6564 // // */
6565 // // /*
6566 // // * Lorem ipsum dolor sit amet,
6567 // // * consectetur adipiscing elit.
6568 // // */ˇ»
6569 // // "},
6570 // // actual: (but with trailing w/s on the empty lines)
6571 // indoc! {"
6572 // «/*
6573 // * Lorem ipsum dolor sit amet,
6574 // * consectetur adipiscing elit.
6575 // *
6576 // */
6577 // /*
6578 // *
6579 // * Lorem ipsum dolor sit amet,
6580 // * consectetur adipiscing elit.
6581 // */ˇ»
6582 // "},
6583 // rust_lang.clone(),
6584 // &mut cx,
6585 // );
6586
6587 // TODO these are unhandled edge cases; not correct, just documenting known issues
6588 assert_rewrap(
6589 indoc! {"
6590 /*
6591 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6592 */
6593 /*
6594 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6595 /*ˇ Lorem ipsum dolor sit amet */ /* consectetur adipiscing elit. */
6596 "},
6597 // desired:
6598 // indoc! {"
6599 // /*
6600 // *ˇ Lorem ipsum dolor sit amet,
6601 // * consectetur adipiscing elit.
6602 // */
6603 // /*
6604 // *ˇ Lorem ipsum dolor sit amet,
6605 // * consectetur adipiscing elit.
6606 // */
6607 // /*
6608 // *ˇ Lorem ipsum dolor sit amet
6609 // */ /* consectetur adipiscing elit. */
6610 // "},
6611 // actual:
6612 indoc! {"
6613 /*
6614 //ˇ Lorem ipsum dolor sit amet,
6615 // consectetur adipiscing elit.
6616 */
6617 /*
6618 * //ˇ Lorem ipsum dolor sit amet,
6619 * consectetur adipiscing elit.
6620 */
6621 /*
6622 *ˇ Lorem ipsum dolor sit amet */ /*
6623 * consectetur adipiscing elit.
6624 */
6625 "},
6626 rust_lang,
6627 &mut cx,
6628 );
6629
6630 #[track_caller]
6631 fn assert_rewrap(
6632 unwrapped_text: &str,
6633 wrapped_text: &str,
6634 language: Arc<Language>,
6635 cx: &mut EditorTestContext,
6636 ) {
6637 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6638 cx.set_state(unwrapped_text);
6639 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6640 cx.assert_editor_state(wrapped_text);
6641 }
6642}
6643
6644#[gpui::test]
6645async fn test_hard_wrap(cx: &mut TestAppContext) {
6646 init_test(cx, |_| {});
6647 let mut cx = EditorTestContext::new(cx).await;
6648
6649 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
6650 cx.update_editor(|editor, _, cx| {
6651 editor.set_hard_wrap(Some(14), cx);
6652 });
6653
6654 cx.set_state(indoc!(
6655 "
6656 one two three ˇ
6657 "
6658 ));
6659 cx.simulate_input("four");
6660 cx.run_until_parked();
6661
6662 cx.assert_editor_state(indoc!(
6663 "
6664 one two three
6665 fourˇ
6666 "
6667 ));
6668
6669 cx.update_editor(|editor, window, cx| {
6670 editor.newline(&Default::default(), window, cx);
6671 });
6672 cx.run_until_parked();
6673 cx.assert_editor_state(indoc!(
6674 "
6675 one two three
6676 four
6677 ˇ
6678 "
6679 ));
6680
6681 cx.simulate_input("five");
6682 cx.run_until_parked();
6683 cx.assert_editor_state(indoc!(
6684 "
6685 one two three
6686 four
6687 fiveˇ
6688 "
6689 ));
6690
6691 cx.update_editor(|editor, window, cx| {
6692 editor.newline(&Default::default(), window, cx);
6693 });
6694 cx.run_until_parked();
6695 cx.simulate_input("# ");
6696 cx.run_until_parked();
6697 cx.assert_editor_state(indoc!(
6698 "
6699 one two three
6700 four
6701 five
6702 # ˇ
6703 "
6704 ));
6705
6706 cx.update_editor(|editor, window, cx| {
6707 editor.newline(&Default::default(), window, cx);
6708 });
6709 cx.run_until_parked();
6710 cx.assert_editor_state(indoc!(
6711 "
6712 one two three
6713 four
6714 five
6715 #\x20
6716 #ˇ
6717 "
6718 ));
6719
6720 cx.simulate_input(" 6");
6721 cx.run_until_parked();
6722 cx.assert_editor_state(indoc!(
6723 "
6724 one two three
6725 four
6726 five
6727 #
6728 # 6ˇ
6729 "
6730 ));
6731}
6732
6733#[gpui::test]
6734async fn test_cut_line_ends(cx: &mut TestAppContext) {
6735 init_test(cx, |_| {});
6736
6737 let mut cx = EditorTestContext::new(cx).await;
6738
6739 cx.set_state(indoc! {"
6740 The quick« brownˇ»
6741 fox jumps overˇ
6742 the lazy dog"});
6743 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6744 cx.assert_editor_state(indoc! {"
6745 The quickˇ
6746 ˇthe lazy dog"});
6747
6748 cx.set_state(indoc! {"
6749 The quick« brownˇ»
6750 fox jumps overˇ
6751 the lazy dog"});
6752 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
6753 cx.assert_editor_state(indoc! {"
6754 The quickˇ
6755 fox jumps overˇthe lazy dog"});
6756
6757 cx.set_state(indoc! {"
6758 The quick« brownˇ»
6759 fox jumps overˇ
6760 the lazy dog"});
6761 cx.update_editor(|e, window, cx| {
6762 e.cut_to_end_of_line(
6763 &CutToEndOfLine {
6764 stop_at_newlines: true,
6765 },
6766 window,
6767 cx,
6768 )
6769 });
6770 cx.assert_editor_state(indoc! {"
6771 The quickˇ
6772 fox jumps overˇ
6773 the lazy dog"});
6774
6775 cx.set_state(indoc! {"
6776 The quick« brownˇ»
6777 fox jumps overˇ
6778 the lazy dog"});
6779 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
6780 cx.assert_editor_state(indoc! {"
6781 The quickˇ
6782 fox jumps overˇthe lazy dog"});
6783}
6784
6785#[gpui::test]
6786async fn test_clipboard(cx: &mut TestAppContext) {
6787 init_test(cx, |_| {});
6788
6789 let mut cx = EditorTestContext::new(cx).await;
6790
6791 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
6792 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6793 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
6794
6795 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
6796 cx.set_state("two ˇfour ˇsix ˇ");
6797 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6798 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
6799
6800 // Paste again but with only two cursors. Since the number of cursors doesn't
6801 // match the number of slices in the clipboard, the entire clipboard text
6802 // is pasted at each cursor.
6803 cx.set_state("ˇtwo one✅ four three six five ˇ");
6804 cx.update_editor(|e, window, cx| {
6805 e.handle_input("( ", window, cx);
6806 e.paste(&Paste, window, cx);
6807 e.handle_input(") ", window, cx);
6808 });
6809 cx.assert_editor_state(
6810 &([
6811 "( one✅ ",
6812 "three ",
6813 "five ) ˇtwo one✅ four three six five ( one✅ ",
6814 "three ",
6815 "five ) ˇ",
6816 ]
6817 .join("\n")),
6818 );
6819
6820 // Cut with three selections, one of which is full-line.
6821 cx.set_state(indoc! {"
6822 1«2ˇ»3
6823 4ˇ567
6824 «8ˇ»9"});
6825 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6826 cx.assert_editor_state(indoc! {"
6827 1ˇ3
6828 ˇ9"});
6829
6830 // Paste with three selections, noticing how the copied selection that was full-line
6831 // gets inserted before the second cursor.
6832 cx.set_state(indoc! {"
6833 1ˇ3
6834 9ˇ
6835 «oˇ»ne"});
6836 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6837 cx.assert_editor_state(indoc! {"
6838 12ˇ3
6839 4567
6840 9ˇ
6841 8ˇne"});
6842
6843 // Copy with a single cursor only, which writes the whole line into the clipboard.
6844 cx.set_state(indoc! {"
6845 The quick brown
6846 fox juˇmps over
6847 the lazy dog"});
6848 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6849 assert_eq!(
6850 cx.read_from_clipboard()
6851 .and_then(|item| item.text().as_deref().map(str::to_string)),
6852 Some("fox jumps over\n".to_string())
6853 );
6854
6855 // Paste with three selections, noticing how the copied full-line selection is inserted
6856 // before the empty selections but replaces the selection that is non-empty.
6857 cx.set_state(indoc! {"
6858 Tˇhe quick brown
6859 «foˇ»x jumps over
6860 tˇhe lazy dog"});
6861 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6862 cx.assert_editor_state(indoc! {"
6863 fox jumps over
6864 Tˇhe quick brown
6865 fox jumps over
6866 ˇx jumps over
6867 fox jumps over
6868 tˇhe lazy dog"});
6869}
6870
6871#[gpui::test]
6872async fn test_copy_trim(cx: &mut TestAppContext) {
6873 init_test(cx, |_| {});
6874
6875 let mut cx = EditorTestContext::new(cx).await;
6876 cx.set_state(
6877 r#" «for selection in selections.iter() {
6878 let mut start = selection.start;
6879 let mut end = selection.end;
6880 let is_entire_line = selection.is_empty();
6881 if is_entire_line {
6882 start = Point::new(start.row, 0);ˇ»
6883 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6884 }
6885 "#,
6886 );
6887 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6888 assert_eq!(
6889 cx.read_from_clipboard()
6890 .and_then(|item| item.text().as_deref().map(str::to_string)),
6891 Some(
6892 "for selection in selections.iter() {
6893 let mut start = selection.start;
6894 let mut end = selection.end;
6895 let is_entire_line = selection.is_empty();
6896 if is_entire_line {
6897 start = Point::new(start.row, 0);"
6898 .to_string()
6899 ),
6900 "Regular copying preserves all indentation selected",
6901 );
6902 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6903 assert_eq!(
6904 cx.read_from_clipboard()
6905 .and_then(|item| item.text().as_deref().map(str::to_string)),
6906 Some(
6907 "for selection in selections.iter() {
6908let mut start = selection.start;
6909let mut end = selection.end;
6910let is_entire_line = selection.is_empty();
6911if is_entire_line {
6912 start = Point::new(start.row, 0);"
6913 .to_string()
6914 ),
6915 "Copying with stripping should strip all leading whitespaces"
6916 );
6917
6918 cx.set_state(
6919 r#" « for selection in selections.iter() {
6920 let mut start = selection.start;
6921 let mut end = selection.end;
6922 let is_entire_line = selection.is_empty();
6923 if is_entire_line {
6924 start = Point::new(start.row, 0);ˇ»
6925 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6926 }
6927 "#,
6928 );
6929 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6930 assert_eq!(
6931 cx.read_from_clipboard()
6932 .and_then(|item| item.text().as_deref().map(str::to_string)),
6933 Some(
6934 " for selection in selections.iter() {
6935 let mut start = selection.start;
6936 let mut end = selection.end;
6937 let is_entire_line = selection.is_empty();
6938 if is_entire_line {
6939 start = Point::new(start.row, 0);"
6940 .to_string()
6941 ),
6942 "Regular copying preserves all indentation selected",
6943 );
6944 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6945 assert_eq!(
6946 cx.read_from_clipboard()
6947 .and_then(|item| item.text().as_deref().map(str::to_string)),
6948 Some(
6949 "for selection in selections.iter() {
6950let mut start = selection.start;
6951let mut end = selection.end;
6952let is_entire_line = selection.is_empty();
6953if is_entire_line {
6954 start = Point::new(start.row, 0);"
6955 .to_string()
6956 ),
6957 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
6958 );
6959
6960 cx.set_state(
6961 r#" «ˇ for selection in selections.iter() {
6962 let mut start = selection.start;
6963 let mut end = selection.end;
6964 let is_entire_line = selection.is_empty();
6965 if is_entire_line {
6966 start = Point::new(start.row, 0);»
6967 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6968 }
6969 "#,
6970 );
6971 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6972 assert_eq!(
6973 cx.read_from_clipboard()
6974 .and_then(|item| item.text().as_deref().map(str::to_string)),
6975 Some(
6976 " for selection in selections.iter() {
6977 let mut start = selection.start;
6978 let mut end = selection.end;
6979 let is_entire_line = selection.is_empty();
6980 if is_entire_line {
6981 start = Point::new(start.row, 0);"
6982 .to_string()
6983 ),
6984 "Regular copying for reverse selection works the same",
6985 );
6986 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6987 assert_eq!(
6988 cx.read_from_clipboard()
6989 .and_then(|item| item.text().as_deref().map(str::to_string)),
6990 Some(
6991 "for selection in selections.iter() {
6992let mut start = selection.start;
6993let mut end = selection.end;
6994let is_entire_line = selection.is_empty();
6995if is_entire_line {
6996 start = Point::new(start.row, 0);"
6997 .to_string()
6998 ),
6999 "Copying with stripping for reverse selection works the same"
7000 );
7001
7002 cx.set_state(
7003 r#" for selection «in selections.iter() {
7004 let mut start = selection.start;
7005 let mut end = selection.end;
7006 let is_entire_line = selection.is_empty();
7007 if is_entire_line {
7008 start = Point::new(start.row, 0);ˇ»
7009 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7010 }
7011 "#,
7012 );
7013 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7014 assert_eq!(
7015 cx.read_from_clipboard()
7016 .and_then(|item| item.text().as_deref().map(str::to_string)),
7017 Some(
7018 "in selections.iter() {
7019 let mut start = selection.start;
7020 let mut end = selection.end;
7021 let is_entire_line = selection.is_empty();
7022 if is_entire_line {
7023 start = Point::new(start.row, 0);"
7024 .to_string()
7025 ),
7026 "When selecting past the indent, the copying works as usual",
7027 );
7028 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7029 assert_eq!(
7030 cx.read_from_clipboard()
7031 .and_then(|item| item.text().as_deref().map(str::to_string)),
7032 Some(
7033 "in selections.iter() {
7034 let mut start = selection.start;
7035 let mut end = selection.end;
7036 let is_entire_line = selection.is_empty();
7037 if is_entire_line {
7038 start = Point::new(start.row, 0);"
7039 .to_string()
7040 ),
7041 "When selecting past the indent, nothing is trimmed"
7042 );
7043
7044 cx.set_state(
7045 r#" «for selection in selections.iter() {
7046 let mut start = selection.start;
7047
7048 let mut end = selection.end;
7049 let is_entire_line = selection.is_empty();
7050 if is_entire_line {
7051 start = Point::new(start.row, 0);
7052ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
7053 }
7054 "#,
7055 );
7056 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7057 assert_eq!(
7058 cx.read_from_clipboard()
7059 .and_then(|item| item.text().as_deref().map(str::to_string)),
7060 Some(
7061 "for selection in selections.iter() {
7062let mut start = selection.start;
7063
7064let mut end = selection.end;
7065let is_entire_line = selection.is_empty();
7066if is_entire_line {
7067 start = Point::new(start.row, 0);
7068"
7069 .to_string()
7070 ),
7071 "Copying with stripping should ignore empty lines"
7072 );
7073}
7074
7075#[gpui::test]
7076async fn test_paste_multiline(cx: &mut TestAppContext) {
7077 init_test(cx, |_| {});
7078
7079 let mut cx = EditorTestContext::new(cx).await;
7080 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7081
7082 // Cut an indented block, without the leading whitespace.
7083 cx.set_state(indoc! {"
7084 const a: B = (
7085 c(),
7086 «d(
7087 e,
7088 f
7089 )ˇ»
7090 );
7091 "});
7092 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7093 cx.assert_editor_state(indoc! {"
7094 const a: B = (
7095 c(),
7096 ˇ
7097 );
7098 "});
7099
7100 // Paste it at the same position.
7101 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7102 cx.assert_editor_state(indoc! {"
7103 const a: B = (
7104 c(),
7105 d(
7106 e,
7107 f
7108 )ˇ
7109 );
7110 "});
7111
7112 // Paste it at a line with a lower indent level.
7113 cx.set_state(indoc! {"
7114 ˇ
7115 const a: B = (
7116 c(),
7117 );
7118 "});
7119 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7120 cx.assert_editor_state(indoc! {"
7121 d(
7122 e,
7123 f
7124 )ˇ
7125 const a: B = (
7126 c(),
7127 );
7128 "});
7129
7130 // Cut an indented block, with the leading whitespace.
7131 cx.set_state(indoc! {"
7132 const a: B = (
7133 c(),
7134 « d(
7135 e,
7136 f
7137 )
7138 ˇ»);
7139 "});
7140 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7141 cx.assert_editor_state(indoc! {"
7142 const a: B = (
7143 c(),
7144 ˇ);
7145 "});
7146
7147 // Paste it at the same position.
7148 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7149 cx.assert_editor_state(indoc! {"
7150 const a: B = (
7151 c(),
7152 d(
7153 e,
7154 f
7155 )
7156 ˇ);
7157 "});
7158
7159 // Paste it at a line with a higher indent level.
7160 cx.set_state(indoc! {"
7161 const a: B = (
7162 c(),
7163 d(
7164 e,
7165 fˇ
7166 )
7167 );
7168 "});
7169 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7170 cx.assert_editor_state(indoc! {"
7171 const a: B = (
7172 c(),
7173 d(
7174 e,
7175 f d(
7176 e,
7177 f
7178 )
7179 ˇ
7180 )
7181 );
7182 "});
7183
7184 // Copy an indented block, starting mid-line
7185 cx.set_state(indoc! {"
7186 const a: B = (
7187 c(),
7188 somethin«g(
7189 e,
7190 f
7191 )ˇ»
7192 );
7193 "});
7194 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7195
7196 // Paste it on a line with a lower indent level
7197 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
7198 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7199 cx.assert_editor_state(indoc! {"
7200 const a: B = (
7201 c(),
7202 something(
7203 e,
7204 f
7205 )
7206 );
7207 g(
7208 e,
7209 f
7210 )ˇ"});
7211}
7212
7213#[gpui::test]
7214async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
7215 init_test(cx, |_| {});
7216
7217 cx.write_to_clipboard(ClipboardItem::new_string(
7218 " d(\n e\n );\n".into(),
7219 ));
7220
7221 let mut cx = EditorTestContext::new(cx).await;
7222 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7223
7224 cx.set_state(indoc! {"
7225 fn a() {
7226 b();
7227 if c() {
7228 ˇ
7229 }
7230 }
7231 "});
7232
7233 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7234 cx.assert_editor_state(indoc! {"
7235 fn a() {
7236 b();
7237 if c() {
7238 d(
7239 e
7240 );
7241 ˇ
7242 }
7243 }
7244 "});
7245
7246 cx.set_state(indoc! {"
7247 fn a() {
7248 b();
7249 ˇ
7250 }
7251 "});
7252
7253 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7254 cx.assert_editor_state(indoc! {"
7255 fn a() {
7256 b();
7257 d(
7258 e
7259 );
7260 ˇ
7261 }
7262 "});
7263}
7264
7265#[gpui::test]
7266fn test_select_all(cx: &mut TestAppContext) {
7267 init_test(cx, |_| {});
7268
7269 let editor = cx.add_window(|window, cx| {
7270 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
7271 build_editor(buffer, window, cx)
7272 });
7273 _ = editor.update(cx, |editor, window, cx| {
7274 editor.select_all(&SelectAll, window, cx);
7275 assert_eq!(
7276 editor.selections.display_ranges(cx),
7277 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
7278 );
7279 });
7280}
7281
7282#[gpui::test]
7283fn test_select_line(cx: &mut TestAppContext) {
7284 init_test(cx, |_| {});
7285
7286 let editor = cx.add_window(|window, cx| {
7287 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
7288 build_editor(buffer, window, cx)
7289 });
7290 _ = editor.update(cx, |editor, window, cx| {
7291 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7292 s.select_display_ranges([
7293 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7294 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7295 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7296 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
7297 ])
7298 });
7299 editor.select_line(&SelectLine, window, cx);
7300 assert_eq!(
7301 editor.selections.display_ranges(cx),
7302 vec![
7303 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
7304 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
7305 ]
7306 );
7307 });
7308
7309 _ = editor.update(cx, |editor, window, cx| {
7310 editor.select_line(&SelectLine, window, cx);
7311 assert_eq!(
7312 editor.selections.display_ranges(cx),
7313 vec![
7314 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
7315 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
7316 ]
7317 );
7318 });
7319
7320 _ = editor.update(cx, |editor, window, cx| {
7321 editor.select_line(&SelectLine, window, cx);
7322 assert_eq!(
7323 editor.selections.display_ranges(cx),
7324 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
7325 );
7326 });
7327}
7328
7329#[gpui::test]
7330async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
7331 init_test(cx, |_| {});
7332 let mut cx = EditorTestContext::new(cx).await;
7333
7334 #[track_caller]
7335 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
7336 cx.set_state(initial_state);
7337 cx.update_editor(|e, window, cx| {
7338 e.split_selection_into_lines(&Default::default(), window, cx)
7339 });
7340 cx.assert_editor_state(expected_state);
7341 }
7342
7343 // Selection starts and ends at the middle of lines, left-to-right
7344 test(
7345 &mut cx,
7346 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
7347 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7348 );
7349 // Same thing, right-to-left
7350 test(
7351 &mut cx,
7352 "aa\nb«b\ncc\ndd\neˇ»e\nff",
7353 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7354 );
7355
7356 // Whole buffer, left-to-right, last line *doesn't* end with newline
7357 test(
7358 &mut cx,
7359 "«ˇaa\nbb\ncc\ndd\nee\nff»",
7360 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7361 );
7362 // Same thing, right-to-left
7363 test(
7364 &mut cx,
7365 "«aa\nbb\ncc\ndd\nee\nffˇ»",
7366 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7367 );
7368
7369 // Whole buffer, left-to-right, last line ends with newline
7370 test(
7371 &mut cx,
7372 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
7373 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7374 );
7375 // Same thing, right-to-left
7376 test(
7377 &mut cx,
7378 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
7379 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7380 );
7381
7382 // Starts at the end of a line, ends at the start of another
7383 test(
7384 &mut cx,
7385 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
7386 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
7387 );
7388}
7389
7390#[gpui::test]
7391async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
7392 init_test(cx, |_| {});
7393
7394 let editor = cx.add_window(|window, cx| {
7395 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
7396 build_editor(buffer, window, cx)
7397 });
7398
7399 // setup
7400 _ = editor.update(cx, |editor, window, cx| {
7401 editor.fold_creases(
7402 vec![
7403 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
7404 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
7405 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
7406 ],
7407 true,
7408 window,
7409 cx,
7410 );
7411 assert_eq!(
7412 editor.display_text(cx),
7413 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7414 );
7415 });
7416
7417 _ = editor.update(cx, |editor, window, cx| {
7418 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7419 s.select_display_ranges([
7420 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7421 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7422 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7423 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
7424 ])
7425 });
7426 editor.split_selection_into_lines(&Default::default(), window, cx);
7427 assert_eq!(
7428 editor.display_text(cx),
7429 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7430 );
7431 });
7432 EditorTestContext::for_editor(editor, cx)
7433 .await
7434 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
7435
7436 _ = editor.update(cx, |editor, window, cx| {
7437 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7438 s.select_display_ranges([
7439 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
7440 ])
7441 });
7442 editor.split_selection_into_lines(&Default::default(), window, cx);
7443 assert_eq!(
7444 editor.display_text(cx),
7445 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
7446 );
7447 assert_eq!(
7448 editor.selections.display_ranges(cx),
7449 [
7450 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
7451 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
7452 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
7453 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
7454 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
7455 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
7456 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
7457 ]
7458 );
7459 });
7460 EditorTestContext::for_editor(editor, cx)
7461 .await
7462 .assert_editor_state(
7463 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
7464 );
7465}
7466
7467#[gpui::test]
7468async fn test_add_selection_above_below(cx: &mut TestAppContext) {
7469 init_test(cx, |_| {});
7470
7471 let mut cx = EditorTestContext::new(cx).await;
7472
7473 cx.set_state(indoc!(
7474 r#"abc
7475 defˇghi
7476
7477 jk
7478 nlmo
7479 "#
7480 ));
7481
7482 cx.update_editor(|editor, window, cx| {
7483 editor.add_selection_above(&Default::default(), window, cx);
7484 });
7485
7486 cx.assert_editor_state(indoc!(
7487 r#"abcˇ
7488 defˇghi
7489
7490 jk
7491 nlmo
7492 "#
7493 ));
7494
7495 cx.update_editor(|editor, window, cx| {
7496 editor.add_selection_above(&Default::default(), window, cx);
7497 });
7498
7499 cx.assert_editor_state(indoc!(
7500 r#"abcˇ
7501 defˇghi
7502
7503 jk
7504 nlmo
7505 "#
7506 ));
7507
7508 cx.update_editor(|editor, window, cx| {
7509 editor.add_selection_below(&Default::default(), window, cx);
7510 });
7511
7512 cx.assert_editor_state(indoc!(
7513 r#"abc
7514 defˇghi
7515
7516 jk
7517 nlmo
7518 "#
7519 ));
7520
7521 cx.update_editor(|editor, window, cx| {
7522 editor.undo_selection(&Default::default(), window, cx);
7523 });
7524
7525 cx.assert_editor_state(indoc!(
7526 r#"abcˇ
7527 defˇghi
7528
7529 jk
7530 nlmo
7531 "#
7532 ));
7533
7534 cx.update_editor(|editor, window, cx| {
7535 editor.redo_selection(&Default::default(), window, cx);
7536 });
7537
7538 cx.assert_editor_state(indoc!(
7539 r#"abc
7540 defˇghi
7541
7542 jk
7543 nlmo
7544 "#
7545 ));
7546
7547 cx.update_editor(|editor, window, cx| {
7548 editor.add_selection_below(&Default::default(), window, cx);
7549 });
7550
7551 cx.assert_editor_state(indoc!(
7552 r#"abc
7553 defˇghi
7554 ˇ
7555 jk
7556 nlmo
7557 "#
7558 ));
7559
7560 cx.update_editor(|editor, window, cx| {
7561 editor.add_selection_below(&Default::default(), window, cx);
7562 });
7563
7564 cx.assert_editor_state(indoc!(
7565 r#"abc
7566 defˇghi
7567 ˇ
7568 jkˇ
7569 nlmo
7570 "#
7571 ));
7572
7573 cx.update_editor(|editor, window, cx| {
7574 editor.add_selection_below(&Default::default(), window, cx);
7575 });
7576
7577 cx.assert_editor_state(indoc!(
7578 r#"abc
7579 defˇghi
7580 ˇ
7581 jkˇ
7582 nlmˇo
7583 "#
7584 ));
7585
7586 cx.update_editor(|editor, window, cx| {
7587 editor.add_selection_below(&Default::default(), window, cx);
7588 });
7589
7590 cx.assert_editor_state(indoc!(
7591 r#"abc
7592 defˇghi
7593 ˇ
7594 jkˇ
7595 nlmˇo
7596 ˇ"#
7597 ));
7598
7599 // change selections
7600 cx.set_state(indoc!(
7601 r#"abc
7602 def«ˇg»hi
7603
7604 jk
7605 nlmo
7606 "#
7607 ));
7608
7609 cx.update_editor(|editor, window, cx| {
7610 editor.add_selection_below(&Default::default(), window, cx);
7611 });
7612
7613 cx.assert_editor_state(indoc!(
7614 r#"abc
7615 def«ˇg»hi
7616
7617 jk
7618 nlm«ˇo»
7619 "#
7620 ));
7621
7622 cx.update_editor(|editor, window, cx| {
7623 editor.add_selection_below(&Default::default(), window, cx);
7624 });
7625
7626 cx.assert_editor_state(indoc!(
7627 r#"abc
7628 def«ˇg»hi
7629
7630 jk
7631 nlm«ˇo»
7632 "#
7633 ));
7634
7635 cx.update_editor(|editor, window, cx| {
7636 editor.add_selection_above(&Default::default(), window, cx);
7637 });
7638
7639 cx.assert_editor_state(indoc!(
7640 r#"abc
7641 def«ˇg»hi
7642
7643 jk
7644 nlmo
7645 "#
7646 ));
7647
7648 cx.update_editor(|editor, window, cx| {
7649 editor.add_selection_above(&Default::default(), window, cx);
7650 });
7651
7652 cx.assert_editor_state(indoc!(
7653 r#"abc
7654 def«ˇg»hi
7655
7656 jk
7657 nlmo
7658 "#
7659 ));
7660
7661 // Change selections again
7662 cx.set_state(indoc!(
7663 r#"a«bc
7664 defgˇ»hi
7665
7666 jk
7667 nlmo
7668 "#
7669 ));
7670
7671 cx.update_editor(|editor, window, cx| {
7672 editor.add_selection_below(&Default::default(), window, cx);
7673 });
7674
7675 cx.assert_editor_state(indoc!(
7676 r#"a«bcˇ»
7677 d«efgˇ»hi
7678
7679 j«kˇ»
7680 nlmo
7681 "#
7682 ));
7683
7684 cx.update_editor(|editor, window, cx| {
7685 editor.add_selection_below(&Default::default(), window, cx);
7686 });
7687 cx.assert_editor_state(indoc!(
7688 r#"a«bcˇ»
7689 d«efgˇ»hi
7690
7691 j«kˇ»
7692 n«lmoˇ»
7693 "#
7694 ));
7695 cx.update_editor(|editor, window, cx| {
7696 editor.add_selection_above(&Default::default(), window, cx);
7697 });
7698
7699 cx.assert_editor_state(indoc!(
7700 r#"a«bcˇ»
7701 d«efgˇ»hi
7702
7703 j«kˇ»
7704 nlmo
7705 "#
7706 ));
7707
7708 // Change selections again
7709 cx.set_state(indoc!(
7710 r#"abc
7711 d«ˇefghi
7712
7713 jk
7714 nlm»o
7715 "#
7716 ));
7717
7718 cx.update_editor(|editor, window, cx| {
7719 editor.add_selection_above(&Default::default(), window, cx);
7720 });
7721
7722 cx.assert_editor_state(indoc!(
7723 r#"a«ˇbc»
7724 d«ˇef»ghi
7725
7726 j«ˇk»
7727 n«ˇlm»o
7728 "#
7729 ));
7730
7731 cx.update_editor(|editor, window, cx| {
7732 editor.add_selection_below(&Default::default(), window, cx);
7733 });
7734
7735 cx.assert_editor_state(indoc!(
7736 r#"abc
7737 d«ˇef»ghi
7738
7739 j«ˇk»
7740 n«ˇlm»o
7741 "#
7742 ));
7743}
7744
7745#[gpui::test]
7746async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
7747 init_test(cx, |_| {});
7748 let mut cx = EditorTestContext::new(cx).await;
7749
7750 cx.set_state(indoc!(
7751 r#"line onˇe
7752 liˇne two
7753 line three
7754 line four"#
7755 ));
7756
7757 cx.update_editor(|editor, window, cx| {
7758 editor.add_selection_below(&Default::default(), window, cx);
7759 });
7760
7761 // test multiple cursors expand in the same direction
7762 cx.assert_editor_state(indoc!(
7763 r#"line onˇe
7764 liˇne twˇo
7765 liˇne three
7766 line four"#
7767 ));
7768
7769 cx.update_editor(|editor, window, cx| {
7770 editor.add_selection_below(&Default::default(), window, cx);
7771 });
7772
7773 cx.update_editor(|editor, window, cx| {
7774 editor.add_selection_below(&Default::default(), window, cx);
7775 });
7776
7777 // test multiple cursors expand below overflow
7778 cx.assert_editor_state(indoc!(
7779 r#"line onˇe
7780 liˇne twˇo
7781 liˇne thˇree
7782 liˇne foˇur"#
7783 ));
7784
7785 cx.update_editor(|editor, window, cx| {
7786 editor.add_selection_above(&Default::default(), window, cx);
7787 });
7788
7789 // test multiple cursors retrieves back correctly
7790 cx.assert_editor_state(indoc!(
7791 r#"line onˇe
7792 liˇne twˇo
7793 liˇne thˇree
7794 line four"#
7795 ));
7796
7797 cx.update_editor(|editor, window, cx| {
7798 editor.add_selection_above(&Default::default(), window, cx);
7799 });
7800
7801 cx.update_editor(|editor, window, cx| {
7802 editor.add_selection_above(&Default::default(), window, cx);
7803 });
7804
7805 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
7806 cx.assert_editor_state(indoc!(
7807 r#"liˇne onˇe
7808 liˇne two
7809 line three
7810 line four"#
7811 ));
7812
7813 cx.update_editor(|editor, window, cx| {
7814 editor.undo_selection(&Default::default(), window, cx);
7815 });
7816
7817 // test undo
7818 cx.assert_editor_state(indoc!(
7819 r#"line onˇe
7820 liˇne twˇo
7821 line three
7822 line four"#
7823 ));
7824
7825 cx.update_editor(|editor, window, cx| {
7826 editor.redo_selection(&Default::default(), window, cx);
7827 });
7828
7829 // test redo
7830 cx.assert_editor_state(indoc!(
7831 r#"liˇne onˇe
7832 liˇne two
7833 line three
7834 line four"#
7835 ));
7836
7837 cx.set_state(indoc!(
7838 r#"abcd
7839 ef«ghˇ»
7840 ijkl
7841 «mˇ»nop"#
7842 ));
7843
7844 cx.update_editor(|editor, window, cx| {
7845 editor.add_selection_above(&Default::default(), window, cx);
7846 });
7847
7848 // test multiple selections expand in the same direction
7849 cx.assert_editor_state(indoc!(
7850 r#"ab«cdˇ»
7851 ef«ghˇ»
7852 «iˇ»jkl
7853 «mˇ»nop"#
7854 ));
7855
7856 cx.update_editor(|editor, window, cx| {
7857 editor.add_selection_above(&Default::default(), window, cx);
7858 });
7859
7860 // test multiple selection upward overflow
7861 cx.assert_editor_state(indoc!(
7862 r#"ab«cdˇ»
7863 «eˇ»f«ghˇ»
7864 «iˇ»jkl
7865 «mˇ»nop"#
7866 ));
7867
7868 cx.update_editor(|editor, window, cx| {
7869 editor.add_selection_below(&Default::default(), window, cx);
7870 });
7871
7872 // test multiple selection retrieves back correctly
7873 cx.assert_editor_state(indoc!(
7874 r#"abcd
7875 ef«ghˇ»
7876 «iˇ»jkl
7877 «mˇ»nop"#
7878 ));
7879
7880 cx.update_editor(|editor, window, cx| {
7881 editor.add_selection_below(&Default::default(), window, cx);
7882 });
7883
7884 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
7885 cx.assert_editor_state(indoc!(
7886 r#"abcd
7887 ef«ghˇ»
7888 ij«klˇ»
7889 «mˇ»nop"#
7890 ));
7891
7892 cx.update_editor(|editor, window, cx| {
7893 editor.undo_selection(&Default::default(), window, cx);
7894 });
7895
7896 // test undo
7897 cx.assert_editor_state(indoc!(
7898 r#"abcd
7899 ef«ghˇ»
7900 «iˇ»jkl
7901 «mˇ»nop"#
7902 ));
7903
7904 cx.update_editor(|editor, window, cx| {
7905 editor.redo_selection(&Default::default(), window, cx);
7906 });
7907
7908 // test redo
7909 cx.assert_editor_state(indoc!(
7910 r#"abcd
7911 ef«ghˇ»
7912 ij«klˇ»
7913 «mˇ»nop"#
7914 ));
7915}
7916
7917#[gpui::test]
7918async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
7919 init_test(cx, |_| {});
7920 let mut cx = EditorTestContext::new(cx).await;
7921
7922 cx.set_state(indoc!(
7923 r#"line onˇe
7924 liˇne two
7925 line three
7926 line four"#
7927 ));
7928
7929 cx.update_editor(|editor, window, cx| {
7930 editor.add_selection_below(&Default::default(), window, cx);
7931 editor.add_selection_below(&Default::default(), window, cx);
7932 editor.add_selection_below(&Default::default(), window, cx);
7933 });
7934
7935 // initial state with two multi cursor groups
7936 cx.assert_editor_state(indoc!(
7937 r#"line onˇe
7938 liˇne twˇo
7939 liˇne thˇree
7940 liˇne foˇur"#
7941 ));
7942
7943 // add single cursor in middle - simulate opt click
7944 cx.update_editor(|editor, window, cx| {
7945 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
7946 editor.begin_selection(new_cursor_point, true, 1, window, cx);
7947 editor.end_selection(window, cx);
7948 });
7949
7950 cx.assert_editor_state(indoc!(
7951 r#"line onˇe
7952 liˇne twˇo
7953 liˇneˇ thˇree
7954 liˇne foˇur"#
7955 ));
7956
7957 cx.update_editor(|editor, window, cx| {
7958 editor.add_selection_above(&Default::default(), window, cx);
7959 });
7960
7961 // test new added selection expands above and existing selection shrinks
7962 cx.assert_editor_state(indoc!(
7963 r#"line onˇe
7964 liˇneˇ twˇo
7965 liˇneˇ thˇree
7966 line four"#
7967 ));
7968
7969 cx.update_editor(|editor, window, cx| {
7970 editor.add_selection_above(&Default::default(), window, cx);
7971 });
7972
7973 // test new added selection expands above and existing selection shrinks
7974 cx.assert_editor_state(indoc!(
7975 r#"lineˇ onˇe
7976 liˇneˇ twˇo
7977 lineˇ three
7978 line four"#
7979 ));
7980
7981 // intial state with two selection groups
7982 cx.set_state(indoc!(
7983 r#"abcd
7984 ef«ghˇ»
7985 ijkl
7986 «mˇ»nop"#
7987 ));
7988
7989 cx.update_editor(|editor, window, cx| {
7990 editor.add_selection_above(&Default::default(), window, cx);
7991 editor.add_selection_above(&Default::default(), window, cx);
7992 });
7993
7994 cx.assert_editor_state(indoc!(
7995 r#"ab«cdˇ»
7996 «eˇ»f«ghˇ»
7997 «iˇ»jkl
7998 «mˇ»nop"#
7999 ));
8000
8001 // add single selection in middle - simulate opt drag
8002 cx.update_editor(|editor, window, cx| {
8003 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
8004 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8005 editor.update_selection(
8006 DisplayPoint::new(DisplayRow(2), 4),
8007 0,
8008 gpui::Point::<f32>::default(),
8009 window,
8010 cx,
8011 );
8012 editor.end_selection(window, cx);
8013 });
8014
8015 cx.assert_editor_state(indoc!(
8016 r#"ab«cdˇ»
8017 «eˇ»f«ghˇ»
8018 «iˇ»jk«lˇ»
8019 «mˇ»nop"#
8020 ));
8021
8022 cx.update_editor(|editor, window, cx| {
8023 editor.add_selection_below(&Default::default(), window, cx);
8024 });
8025
8026 // test new added selection expands below, others shrinks from above
8027 cx.assert_editor_state(indoc!(
8028 r#"abcd
8029 ef«ghˇ»
8030 «iˇ»jk«lˇ»
8031 «mˇ»no«pˇ»"#
8032 ));
8033}
8034
8035#[gpui::test]
8036async fn test_select_next(cx: &mut TestAppContext) {
8037 init_test(cx, |_| {});
8038
8039 let mut cx = EditorTestContext::new(cx).await;
8040 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8041
8042 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8043 .unwrap();
8044 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8045
8046 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8047 .unwrap();
8048 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8049
8050 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8051 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8052
8053 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8054 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8055
8056 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8057 .unwrap();
8058 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8059
8060 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8061 .unwrap();
8062 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8063
8064 // Test selection direction should be preserved
8065 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8066
8067 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8068 .unwrap();
8069 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
8070}
8071
8072#[gpui::test]
8073async fn test_select_all_matches(cx: &mut TestAppContext) {
8074 init_test(cx, |_| {});
8075
8076 let mut cx = EditorTestContext::new(cx).await;
8077
8078 // Test caret-only selections
8079 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8080 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8081 .unwrap();
8082 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8083
8084 // Test left-to-right selections
8085 cx.set_state("abc\n«abcˇ»\nabc");
8086 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8087 .unwrap();
8088 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
8089
8090 // Test right-to-left selections
8091 cx.set_state("abc\n«ˇabc»\nabc");
8092 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8093 .unwrap();
8094 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
8095
8096 // Test selecting whitespace with caret selection
8097 cx.set_state("abc\nˇ abc\nabc");
8098 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8099 .unwrap();
8100 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8101
8102 // Test selecting whitespace with left-to-right selection
8103 cx.set_state("abc\n«ˇ »abc\nabc");
8104 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8105 .unwrap();
8106 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
8107
8108 // Test no matches with right-to-left selection
8109 cx.set_state("abc\n« ˇ»abc\nabc");
8110 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8111 .unwrap();
8112 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8113
8114 // Test with a single word and clip_at_line_ends=true (#29823)
8115 cx.set_state("aˇbc");
8116 cx.update_editor(|e, window, cx| {
8117 e.set_clip_at_line_ends(true, cx);
8118 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8119 e.set_clip_at_line_ends(false, cx);
8120 });
8121 cx.assert_editor_state("«abcˇ»");
8122}
8123
8124#[gpui::test]
8125async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
8126 init_test(cx, |_| {});
8127
8128 let mut cx = EditorTestContext::new(cx).await;
8129
8130 let large_body_1 = "\nd".repeat(200);
8131 let large_body_2 = "\ne".repeat(200);
8132
8133 cx.set_state(&format!(
8134 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
8135 ));
8136 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
8137 let scroll_position = editor.scroll_position(cx);
8138 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
8139 scroll_position
8140 });
8141
8142 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8143 .unwrap();
8144 cx.assert_editor_state(&format!(
8145 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
8146 ));
8147 let scroll_position_after_selection =
8148 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
8149 assert_eq!(
8150 initial_scroll_position, scroll_position_after_selection,
8151 "Scroll position should not change after selecting all matches"
8152 );
8153}
8154
8155#[gpui::test]
8156async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
8157 init_test(cx, |_| {});
8158
8159 let mut cx = EditorLspTestContext::new_rust(
8160 lsp::ServerCapabilities {
8161 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8162 ..Default::default()
8163 },
8164 cx,
8165 )
8166 .await;
8167
8168 cx.set_state(indoc! {"
8169 line 1
8170 line 2
8171 linˇe 3
8172 line 4
8173 line 5
8174 "});
8175
8176 // Make an edit
8177 cx.update_editor(|editor, window, cx| {
8178 editor.handle_input("X", window, cx);
8179 });
8180
8181 // Move cursor to a different position
8182 cx.update_editor(|editor, window, cx| {
8183 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8184 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
8185 });
8186 });
8187
8188 cx.assert_editor_state(indoc! {"
8189 line 1
8190 line 2
8191 linXe 3
8192 line 4
8193 liˇne 5
8194 "});
8195
8196 cx.lsp
8197 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8198 Ok(Some(vec![lsp::TextEdit::new(
8199 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8200 "PREFIX ".to_string(),
8201 )]))
8202 });
8203
8204 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
8205 .unwrap()
8206 .await
8207 .unwrap();
8208
8209 cx.assert_editor_state(indoc! {"
8210 PREFIX line 1
8211 line 2
8212 linXe 3
8213 line 4
8214 liˇne 5
8215 "});
8216
8217 // Undo formatting
8218 cx.update_editor(|editor, window, cx| {
8219 editor.undo(&Default::default(), window, cx);
8220 });
8221
8222 // Verify cursor moved back to position after edit
8223 cx.assert_editor_state(indoc! {"
8224 line 1
8225 line 2
8226 linXˇe 3
8227 line 4
8228 line 5
8229 "});
8230}
8231
8232#[gpui::test]
8233async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) {
8234 init_test(cx, |_| {});
8235
8236 let mut cx = EditorTestContext::new(cx).await;
8237
8238 let provider = cx.new(|_| FakeEditPredictionProvider::default());
8239 cx.update_editor(|editor, window, cx| {
8240 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
8241 });
8242
8243 cx.set_state(indoc! {"
8244 line 1
8245 line 2
8246 linˇe 3
8247 line 4
8248 line 5
8249 line 6
8250 line 7
8251 line 8
8252 line 9
8253 line 10
8254 "});
8255
8256 let snapshot = cx.buffer_snapshot();
8257 let edit_position = snapshot.anchor_after(Point::new(2, 4));
8258
8259 cx.update(|_, cx| {
8260 provider.update(cx, |provider, _| {
8261 provider.set_edit_prediction(Some(edit_prediction::EditPrediction {
8262 id: None,
8263 edits: vec![(edit_position..edit_position, "X".into())],
8264 edit_preview: None,
8265 }))
8266 })
8267 });
8268
8269 cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
8270 cx.update_editor(|editor, window, cx| {
8271 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
8272 });
8273
8274 cx.assert_editor_state(indoc! {"
8275 line 1
8276 line 2
8277 lineXˇ 3
8278 line 4
8279 line 5
8280 line 6
8281 line 7
8282 line 8
8283 line 9
8284 line 10
8285 "});
8286
8287 cx.update_editor(|editor, window, cx| {
8288 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8289 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
8290 });
8291 });
8292
8293 cx.assert_editor_state(indoc! {"
8294 line 1
8295 line 2
8296 lineX 3
8297 line 4
8298 line 5
8299 line 6
8300 line 7
8301 line 8
8302 line 9
8303 liˇne 10
8304 "});
8305
8306 cx.update_editor(|editor, window, cx| {
8307 editor.undo(&Default::default(), window, cx);
8308 });
8309
8310 cx.assert_editor_state(indoc! {"
8311 line 1
8312 line 2
8313 lineˇ 3
8314 line 4
8315 line 5
8316 line 6
8317 line 7
8318 line 8
8319 line 9
8320 line 10
8321 "});
8322}
8323
8324#[gpui::test]
8325async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
8326 init_test(cx, |_| {});
8327
8328 let mut cx = EditorTestContext::new(cx).await;
8329 cx.set_state(
8330 r#"let foo = 2;
8331lˇet foo = 2;
8332let fooˇ = 2;
8333let foo = 2;
8334let foo = ˇ2;"#,
8335 );
8336
8337 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8338 .unwrap();
8339 cx.assert_editor_state(
8340 r#"let foo = 2;
8341«letˇ» foo = 2;
8342let «fooˇ» = 2;
8343let foo = 2;
8344let foo = «2ˇ»;"#,
8345 );
8346
8347 // noop for multiple selections with different contents
8348 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8349 .unwrap();
8350 cx.assert_editor_state(
8351 r#"let foo = 2;
8352«letˇ» foo = 2;
8353let «fooˇ» = 2;
8354let foo = 2;
8355let foo = «2ˇ»;"#,
8356 );
8357
8358 // Test last selection direction should be preserved
8359 cx.set_state(
8360 r#"let foo = 2;
8361let foo = 2;
8362let «fooˇ» = 2;
8363let «ˇfoo» = 2;
8364let foo = 2;"#,
8365 );
8366
8367 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8368 .unwrap();
8369 cx.assert_editor_state(
8370 r#"let foo = 2;
8371let foo = 2;
8372let «fooˇ» = 2;
8373let «ˇfoo» = 2;
8374let «ˇfoo» = 2;"#,
8375 );
8376}
8377
8378#[gpui::test]
8379async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
8380 init_test(cx, |_| {});
8381
8382 let mut cx =
8383 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
8384
8385 cx.assert_editor_state(indoc! {"
8386 ˇbbb
8387 ccc
8388
8389 bbb
8390 ccc
8391 "});
8392 cx.dispatch_action(SelectPrevious::default());
8393 cx.assert_editor_state(indoc! {"
8394 «bbbˇ»
8395 ccc
8396
8397 bbb
8398 ccc
8399 "});
8400 cx.dispatch_action(SelectPrevious::default());
8401 cx.assert_editor_state(indoc! {"
8402 «bbbˇ»
8403 ccc
8404
8405 «bbbˇ»
8406 ccc
8407 "});
8408}
8409
8410#[gpui::test]
8411async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
8412 init_test(cx, |_| {});
8413
8414 let mut cx = EditorTestContext::new(cx).await;
8415 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8416
8417 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8418 .unwrap();
8419 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8420
8421 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8422 .unwrap();
8423 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8424
8425 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8426 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8427
8428 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8429 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8430
8431 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8432 .unwrap();
8433 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
8434
8435 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8436 .unwrap();
8437 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8438}
8439
8440#[gpui::test]
8441async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
8442 init_test(cx, |_| {});
8443
8444 let mut cx = EditorTestContext::new(cx).await;
8445 cx.set_state("aˇ");
8446
8447 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8448 .unwrap();
8449 cx.assert_editor_state("«aˇ»");
8450 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8451 .unwrap();
8452 cx.assert_editor_state("«aˇ»");
8453}
8454
8455#[gpui::test]
8456async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
8457 init_test(cx, |_| {});
8458
8459 let mut cx = EditorTestContext::new(cx).await;
8460 cx.set_state(
8461 r#"let foo = 2;
8462lˇet foo = 2;
8463let fooˇ = 2;
8464let foo = 2;
8465let foo = ˇ2;"#,
8466 );
8467
8468 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8469 .unwrap();
8470 cx.assert_editor_state(
8471 r#"let foo = 2;
8472«letˇ» foo = 2;
8473let «fooˇ» = 2;
8474let foo = 2;
8475let foo = «2ˇ»;"#,
8476 );
8477
8478 // noop for multiple selections with different contents
8479 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8480 .unwrap();
8481 cx.assert_editor_state(
8482 r#"let foo = 2;
8483«letˇ» foo = 2;
8484let «fooˇ» = 2;
8485let foo = 2;
8486let foo = «2ˇ»;"#,
8487 );
8488}
8489
8490#[gpui::test]
8491async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
8492 init_test(cx, |_| {});
8493
8494 let mut cx = EditorTestContext::new(cx).await;
8495 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8496
8497 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8498 .unwrap();
8499 // selection direction is preserved
8500 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8501
8502 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8503 .unwrap();
8504 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8505
8506 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8507 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8508
8509 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8510 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8511
8512 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8513 .unwrap();
8514 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
8515
8516 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8517 .unwrap();
8518 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
8519}
8520
8521#[gpui::test]
8522async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
8523 init_test(cx, |_| {});
8524
8525 let language = Arc::new(Language::new(
8526 LanguageConfig::default(),
8527 Some(tree_sitter_rust::LANGUAGE.into()),
8528 ));
8529
8530 let text = r#"
8531 use mod1::mod2::{mod3, mod4};
8532
8533 fn fn_1(param1: bool, param2: &str) {
8534 let var1 = "text";
8535 }
8536 "#
8537 .unindent();
8538
8539 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8540 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8541 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8542
8543 editor
8544 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8545 .await;
8546
8547 editor.update_in(cx, |editor, window, cx| {
8548 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8549 s.select_display_ranges([
8550 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
8551 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
8552 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
8553 ]);
8554 });
8555 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8556 });
8557 editor.update(cx, |editor, cx| {
8558 assert_text_with_selections(
8559 editor,
8560 indoc! {r#"
8561 use mod1::mod2::{mod3, «mod4ˇ»};
8562
8563 fn fn_1«ˇ(param1: bool, param2: &str)» {
8564 let var1 = "«ˇtext»";
8565 }
8566 "#},
8567 cx,
8568 );
8569 });
8570
8571 editor.update_in(cx, |editor, window, cx| {
8572 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8573 });
8574 editor.update(cx, |editor, cx| {
8575 assert_text_with_selections(
8576 editor,
8577 indoc! {r#"
8578 use mod1::mod2::«{mod3, mod4}ˇ»;
8579
8580 «ˇfn fn_1(param1: bool, param2: &str) {
8581 let var1 = "text";
8582 }»
8583 "#},
8584 cx,
8585 );
8586 });
8587
8588 editor.update_in(cx, |editor, window, cx| {
8589 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8590 });
8591 assert_eq!(
8592 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
8593 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8594 );
8595
8596 // Trying to expand the selected syntax node one more time has no effect.
8597 editor.update_in(cx, |editor, window, cx| {
8598 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8599 });
8600 assert_eq!(
8601 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
8602 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8603 );
8604
8605 editor.update_in(cx, |editor, window, cx| {
8606 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8607 });
8608 editor.update(cx, |editor, cx| {
8609 assert_text_with_selections(
8610 editor,
8611 indoc! {r#"
8612 use mod1::mod2::«{mod3, mod4}ˇ»;
8613
8614 «ˇfn fn_1(param1: bool, param2: &str) {
8615 let var1 = "text";
8616 }»
8617 "#},
8618 cx,
8619 );
8620 });
8621
8622 editor.update_in(cx, |editor, window, cx| {
8623 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8624 });
8625 editor.update(cx, |editor, cx| {
8626 assert_text_with_selections(
8627 editor,
8628 indoc! {r#"
8629 use mod1::mod2::{mod3, «mod4ˇ»};
8630
8631 fn fn_1«ˇ(param1: bool, param2: &str)» {
8632 let var1 = "«ˇtext»";
8633 }
8634 "#},
8635 cx,
8636 );
8637 });
8638
8639 editor.update_in(cx, |editor, window, cx| {
8640 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8641 });
8642 editor.update(cx, |editor, cx| {
8643 assert_text_with_selections(
8644 editor,
8645 indoc! {r#"
8646 use mod1::mod2::{mod3, moˇd4};
8647
8648 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
8649 let var1 = "teˇxt";
8650 }
8651 "#},
8652 cx,
8653 );
8654 });
8655
8656 // Trying to shrink the selected syntax node one more time has no effect.
8657 editor.update_in(cx, |editor, window, cx| {
8658 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8659 });
8660 editor.update_in(cx, |editor, _, cx| {
8661 assert_text_with_selections(
8662 editor,
8663 indoc! {r#"
8664 use mod1::mod2::{mod3, moˇd4};
8665
8666 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
8667 let var1 = "teˇxt";
8668 }
8669 "#},
8670 cx,
8671 );
8672 });
8673
8674 // Ensure that we keep expanding the selection if the larger selection starts or ends within
8675 // a fold.
8676 editor.update_in(cx, |editor, window, cx| {
8677 editor.fold_creases(
8678 vec![
8679 Crease::simple(
8680 Point::new(0, 21)..Point::new(0, 24),
8681 FoldPlaceholder::test(),
8682 ),
8683 Crease::simple(
8684 Point::new(3, 20)..Point::new(3, 22),
8685 FoldPlaceholder::test(),
8686 ),
8687 ],
8688 true,
8689 window,
8690 cx,
8691 );
8692 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8693 });
8694 editor.update(cx, |editor, cx| {
8695 assert_text_with_selections(
8696 editor,
8697 indoc! {r#"
8698 use mod1::mod2::«{mod3, mod4}ˇ»;
8699
8700 fn fn_1«ˇ(param1: bool, param2: &str)» {
8701 let var1 = "«ˇtext»";
8702 }
8703 "#},
8704 cx,
8705 );
8706 });
8707}
8708
8709#[gpui::test]
8710async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
8711 init_test(cx, |_| {});
8712
8713 let language = Arc::new(Language::new(
8714 LanguageConfig::default(),
8715 Some(tree_sitter_rust::LANGUAGE.into()),
8716 ));
8717
8718 let text = "let a = 2;";
8719
8720 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8721 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8722 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8723
8724 editor
8725 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8726 .await;
8727
8728 // Test case 1: Cursor at end of word
8729 editor.update_in(cx, |editor, window, cx| {
8730 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8731 s.select_display_ranges([
8732 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
8733 ]);
8734 });
8735 });
8736 editor.update(cx, |editor, cx| {
8737 assert_text_with_selections(editor, "let aˇ = 2;", cx);
8738 });
8739 editor.update_in(cx, |editor, window, cx| {
8740 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8741 });
8742 editor.update(cx, |editor, cx| {
8743 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
8744 });
8745 editor.update_in(cx, |editor, window, cx| {
8746 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8747 });
8748 editor.update(cx, |editor, cx| {
8749 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
8750 });
8751
8752 // Test case 2: Cursor at end of statement
8753 editor.update_in(cx, |editor, window, cx| {
8754 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8755 s.select_display_ranges([
8756 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
8757 ]);
8758 });
8759 });
8760 editor.update(cx, |editor, cx| {
8761 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
8762 });
8763 editor.update_in(cx, |editor, window, cx| {
8764 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8765 });
8766 editor.update(cx, |editor, cx| {
8767 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
8768 });
8769}
8770
8771#[gpui::test]
8772async fn test_select_larger_syntax_node_for_cursor_at_symbol(cx: &mut TestAppContext) {
8773 init_test(cx, |_| {});
8774
8775 let language = Arc::new(Language::new(
8776 LanguageConfig {
8777 name: "JavaScript".into(),
8778 ..Default::default()
8779 },
8780 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
8781 ));
8782
8783 let text = r#"
8784 let a = {
8785 key: "value",
8786 };
8787 "#
8788 .unindent();
8789
8790 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8791 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8792 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8793
8794 editor
8795 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8796 .await;
8797
8798 // Test case 1: Cursor after '{'
8799 editor.update_in(cx, |editor, window, cx| {
8800 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8801 s.select_display_ranges([
8802 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 9)
8803 ]);
8804 });
8805 });
8806 editor.update(cx, |editor, cx| {
8807 assert_text_with_selections(
8808 editor,
8809 indoc! {r#"
8810 let a = {ˇ
8811 key: "value",
8812 };
8813 "#},
8814 cx,
8815 );
8816 });
8817 editor.update_in(cx, |editor, window, cx| {
8818 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8819 });
8820 editor.update(cx, |editor, cx| {
8821 assert_text_with_selections(
8822 editor,
8823 indoc! {r#"
8824 let a = «ˇ{
8825 key: "value",
8826 }»;
8827 "#},
8828 cx,
8829 );
8830 });
8831
8832 // Test case 2: Cursor after ':'
8833 editor.update_in(cx, |editor, window, cx| {
8834 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8835 s.select_display_ranges([
8836 DisplayPoint::new(DisplayRow(1), 8)..DisplayPoint::new(DisplayRow(1), 8)
8837 ]);
8838 });
8839 });
8840 editor.update(cx, |editor, cx| {
8841 assert_text_with_selections(
8842 editor,
8843 indoc! {r#"
8844 let a = {
8845 key:ˇ "value",
8846 };
8847 "#},
8848 cx,
8849 );
8850 });
8851 editor.update_in(cx, |editor, window, cx| {
8852 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8853 });
8854 editor.update(cx, |editor, cx| {
8855 assert_text_with_selections(
8856 editor,
8857 indoc! {r#"
8858 let a = {
8859 «ˇkey: "value"»,
8860 };
8861 "#},
8862 cx,
8863 );
8864 });
8865 editor.update_in(cx, |editor, window, cx| {
8866 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8867 });
8868 editor.update(cx, |editor, cx| {
8869 assert_text_with_selections(
8870 editor,
8871 indoc! {r#"
8872 let a = «ˇ{
8873 key: "value",
8874 }»;
8875 "#},
8876 cx,
8877 );
8878 });
8879
8880 // Test case 3: Cursor after ','
8881 editor.update_in(cx, |editor, window, cx| {
8882 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8883 s.select_display_ranges([
8884 DisplayPoint::new(DisplayRow(1), 17)..DisplayPoint::new(DisplayRow(1), 17)
8885 ]);
8886 });
8887 });
8888 editor.update(cx, |editor, cx| {
8889 assert_text_with_selections(
8890 editor,
8891 indoc! {r#"
8892 let a = {
8893 key: "value",ˇ
8894 };
8895 "#},
8896 cx,
8897 );
8898 });
8899 editor.update_in(cx, |editor, window, cx| {
8900 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8901 });
8902 editor.update(cx, |editor, cx| {
8903 assert_text_with_selections(
8904 editor,
8905 indoc! {r#"
8906 let a = «ˇ{
8907 key: "value",
8908 }»;
8909 "#},
8910 cx,
8911 );
8912 });
8913
8914 // Test case 4: Cursor after ';'
8915 editor.update_in(cx, |editor, window, cx| {
8916 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8917 s.select_display_ranges([
8918 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)
8919 ]);
8920 });
8921 });
8922 editor.update(cx, |editor, cx| {
8923 assert_text_with_selections(
8924 editor,
8925 indoc! {r#"
8926 let a = {
8927 key: "value",
8928 };ˇ
8929 "#},
8930 cx,
8931 );
8932 });
8933 editor.update_in(cx, |editor, window, cx| {
8934 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8935 });
8936 editor.update(cx, |editor, cx| {
8937 assert_text_with_selections(
8938 editor,
8939 indoc! {r#"
8940 «ˇlet a = {
8941 key: "value",
8942 };
8943 »"#},
8944 cx,
8945 );
8946 });
8947}
8948
8949#[gpui::test]
8950async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
8951 init_test(cx, |_| {});
8952
8953 let language = Arc::new(Language::new(
8954 LanguageConfig::default(),
8955 Some(tree_sitter_rust::LANGUAGE.into()),
8956 ));
8957
8958 let text = r#"
8959 use mod1::mod2::{mod3, mod4};
8960
8961 fn fn_1(param1: bool, param2: &str) {
8962 let var1 = "hello world";
8963 }
8964 "#
8965 .unindent();
8966
8967 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8968 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8969 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8970
8971 editor
8972 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8973 .await;
8974
8975 // Test 1: Cursor on a letter of a string word
8976 editor.update_in(cx, |editor, window, cx| {
8977 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8978 s.select_display_ranges([
8979 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
8980 ]);
8981 });
8982 });
8983 editor.update_in(cx, |editor, window, cx| {
8984 assert_text_with_selections(
8985 editor,
8986 indoc! {r#"
8987 use mod1::mod2::{mod3, mod4};
8988
8989 fn fn_1(param1: bool, param2: &str) {
8990 let var1 = "hˇello world";
8991 }
8992 "#},
8993 cx,
8994 );
8995 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8996 assert_text_with_selections(
8997 editor,
8998 indoc! {r#"
8999 use mod1::mod2::{mod3, mod4};
9000
9001 fn fn_1(param1: bool, param2: &str) {
9002 let var1 = "«ˇhello» world";
9003 }
9004 "#},
9005 cx,
9006 );
9007 });
9008
9009 // Test 2: Partial selection within a word
9010 editor.update_in(cx, |editor, window, cx| {
9011 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9012 s.select_display_ranges([
9013 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
9014 ]);
9015 });
9016 });
9017 editor.update_in(cx, |editor, window, cx| {
9018 assert_text_with_selections(
9019 editor,
9020 indoc! {r#"
9021 use mod1::mod2::{mod3, mod4};
9022
9023 fn fn_1(param1: bool, param2: &str) {
9024 let var1 = "h«elˇ»lo world";
9025 }
9026 "#},
9027 cx,
9028 );
9029 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9030 assert_text_with_selections(
9031 editor,
9032 indoc! {r#"
9033 use mod1::mod2::{mod3, mod4};
9034
9035 fn fn_1(param1: bool, param2: &str) {
9036 let var1 = "«ˇhello» world";
9037 }
9038 "#},
9039 cx,
9040 );
9041 });
9042
9043 // Test 3: Complete word already selected
9044 editor.update_in(cx, |editor, window, cx| {
9045 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9046 s.select_display_ranges([
9047 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
9048 ]);
9049 });
9050 });
9051 editor.update_in(cx, |editor, window, cx| {
9052 assert_text_with_selections(
9053 editor,
9054 indoc! {r#"
9055 use mod1::mod2::{mod3, mod4};
9056
9057 fn fn_1(param1: bool, param2: &str) {
9058 let var1 = "«helloˇ» world";
9059 }
9060 "#},
9061 cx,
9062 );
9063 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9064 assert_text_with_selections(
9065 editor,
9066 indoc! {r#"
9067 use mod1::mod2::{mod3, mod4};
9068
9069 fn fn_1(param1: bool, param2: &str) {
9070 let var1 = "«hello worldˇ»";
9071 }
9072 "#},
9073 cx,
9074 );
9075 });
9076
9077 // Test 4: Selection spanning across words
9078 editor.update_in(cx, |editor, window, cx| {
9079 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9080 s.select_display_ranges([
9081 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
9082 ]);
9083 });
9084 });
9085 editor.update_in(cx, |editor, window, cx| {
9086 assert_text_with_selections(
9087 editor,
9088 indoc! {r#"
9089 use mod1::mod2::{mod3, mod4};
9090
9091 fn fn_1(param1: bool, param2: &str) {
9092 let var1 = "hel«lo woˇ»rld";
9093 }
9094 "#},
9095 cx,
9096 );
9097 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9098 assert_text_with_selections(
9099 editor,
9100 indoc! {r#"
9101 use mod1::mod2::{mod3, mod4};
9102
9103 fn fn_1(param1: bool, param2: &str) {
9104 let var1 = "«ˇhello world»";
9105 }
9106 "#},
9107 cx,
9108 );
9109 });
9110
9111 // Test 5: Expansion beyond string
9112 editor.update_in(cx, |editor, window, cx| {
9113 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9114 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9115 assert_text_with_selections(
9116 editor,
9117 indoc! {r#"
9118 use mod1::mod2::{mod3, mod4};
9119
9120 fn fn_1(param1: bool, param2: &str) {
9121 «ˇlet var1 = "hello world";»
9122 }
9123 "#},
9124 cx,
9125 );
9126 });
9127}
9128
9129#[gpui::test]
9130async fn test_unwrap_syntax_nodes(cx: &mut gpui::TestAppContext) {
9131 init_test(cx, |_| {});
9132
9133 let mut cx = EditorTestContext::new(cx).await;
9134
9135 let language = Arc::new(Language::new(
9136 LanguageConfig::default(),
9137 Some(tree_sitter_rust::LANGUAGE.into()),
9138 ));
9139
9140 cx.update_buffer(|buffer, cx| {
9141 buffer.set_language(Some(language), cx);
9142 });
9143
9144 cx.set_state(indoc! { r#"use mod1::{mod2::{«mod3ˇ», mod4}, mod5::{mod6, «mod7ˇ»}};"# });
9145 cx.update_editor(|editor, window, cx| {
9146 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9147 });
9148
9149 cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# });
9150}
9151
9152#[gpui::test]
9153async fn test_fold_function_bodies(cx: &mut TestAppContext) {
9154 init_test(cx, |_| {});
9155
9156 let base_text = r#"
9157 impl A {
9158 // this is an uncommitted comment
9159
9160 fn b() {
9161 c();
9162 }
9163
9164 // this is another uncommitted comment
9165
9166 fn d() {
9167 // e
9168 // f
9169 }
9170 }
9171
9172 fn g() {
9173 // h
9174 }
9175 "#
9176 .unindent();
9177
9178 let text = r#"
9179 ˇimpl A {
9180
9181 fn b() {
9182 c();
9183 }
9184
9185 fn d() {
9186 // e
9187 // f
9188 }
9189 }
9190
9191 fn g() {
9192 // h
9193 }
9194 "#
9195 .unindent();
9196
9197 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9198 cx.set_state(&text);
9199 cx.set_head_text(&base_text);
9200 cx.update_editor(|editor, window, cx| {
9201 editor.expand_all_diff_hunks(&Default::default(), window, cx);
9202 });
9203
9204 cx.assert_state_with_diff(
9205 "
9206 ˇimpl A {
9207 - // this is an uncommitted comment
9208
9209 fn b() {
9210 c();
9211 }
9212
9213 - // this is another uncommitted comment
9214 -
9215 fn d() {
9216 // e
9217 // f
9218 }
9219 }
9220
9221 fn g() {
9222 // h
9223 }
9224 "
9225 .unindent(),
9226 );
9227
9228 let expected_display_text = "
9229 impl A {
9230 // this is an uncommitted comment
9231
9232 fn b() {
9233 ⋯
9234 }
9235
9236 // this is another uncommitted comment
9237
9238 fn d() {
9239 ⋯
9240 }
9241 }
9242
9243 fn g() {
9244 ⋯
9245 }
9246 "
9247 .unindent();
9248
9249 cx.update_editor(|editor, window, cx| {
9250 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
9251 assert_eq!(editor.display_text(cx), expected_display_text);
9252 });
9253}
9254
9255#[gpui::test]
9256async fn test_autoindent(cx: &mut TestAppContext) {
9257 init_test(cx, |_| {});
9258
9259 let language = Arc::new(
9260 Language::new(
9261 LanguageConfig {
9262 brackets: BracketPairConfig {
9263 pairs: vec![
9264 BracketPair {
9265 start: "{".to_string(),
9266 end: "}".to_string(),
9267 close: false,
9268 surround: false,
9269 newline: true,
9270 },
9271 BracketPair {
9272 start: "(".to_string(),
9273 end: ")".to_string(),
9274 close: false,
9275 surround: false,
9276 newline: true,
9277 },
9278 ],
9279 ..Default::default()
9280 },
9281 ..Default::default()
9282 },
9283 Some(tree_sitter_rust::LANGUAGE.into()),
9284 )
9285 .with_indents_query(
9286 r#"
9287 (_ "(" ")" @end) @indent
9288 (_ "{" "}" @end) @indent
9289 "#,
9290 )
9291 .unwrap(),
9292 );
9293
9294 let text = "fn a() {}";
9295
9296 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9297 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9298 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9299 editor
9300 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9301 .await;
9302
9303 editor.update_in(cx, |editor, window, cx| {
9304 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9305 s.select_ranges([5..5, 8..8, 9..9])
9306 });
9307 editor.newline(&Newline, window, cx);
9308 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
9309 assert_eq!(
9310 editor.selections.ranges(cx),
9311 &[
9312 Point::new(1, 4)..Point::new(1, 4),
9313 Point::new(3, 4)..Point::new(3, 4),
9314 Point::new(5, 0)..Point::new(5, 0)
9315 ]
9316 );
9317 });
9318}
9319
9320#[gpui::test]
9321async fn test_autoindent_disabled(cx: &mut TestAppContext) {
9322 init_test(cx, |settings| settings.defaults.auto_indent = Some(false));
9323
9324 let language = Arc::new(
9325 Language::new(
9326 LanguageConfig {
9327 brackets: BracketPairConfig {
9328 pairs: vec![
9329 BracketPair {
9330 start: "{".to_string(),
9331 end: "}".to_string(),
9332 close: false,
9333 surround: false,
9334 newline: true,
9335 },
9336 BracketPair {
9337 start: "(".to_string(),
9338 end: ")".to_string(),
9339 close: false,
9340 surround: false,
9341 newline: true,
9342 },
9343 ],
9344 ..Default::default()
9345 },
9346 ..Default::default()
9347 },
9348 Some(tree_sitter_rust::LANGUAGE.into()),
9349 )
9350 .with_indents_query(
9351 r#"
9352 (_ "(" ")" @end) @indent
9353 (_ "{" "}" @end) @indent
9354 "#,
9355 )
9356 .unwrap(),
9357 );
9358
9359 let text = "fn a() {}";
9360
9361 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9362 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9363 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9364 editor
9365 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9366 .await;
9367
9368 editor.update_in(cx, |editor, window, cx| {
9369 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9370 s.select_ranges([5..5, 8..8, 9..9])
9371 });
9372 editor.newline(&Newline, window, cx);
9373 assert_eq!(
9374 editor.text(cx),
9375 indoc!(
9376 "
9377 fn a(
9378
9379 ) {
9380
9381 }
9382 "
9383 )
9384 );
9385 assert_eq!(
9386 editor.selections.ranges(cx),
9387 &[
9388 Point::new(1, 0)..Point::new(1, 0),
9389 Point::new(3, 0)..Point::new(3, 0),
9390 Point::new(5, 0)..Point::new(5, 0)
9391 ]
9392 );
9393 });
9394}
9395
9396#[gpui::test]
9397async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) {
9398 init_test(cx, |settings| {
9399 settings.defaults.auto_indent = Some(true);
9400 settings.languages.0.insert(
9401 "python".into(),
9402 LanguageSettingsContent {
9403 auto_indent: Some(false),
9404 ..Default::default()
9405 },
9406 );
9407 });
9408
9409 let mut cx = EditorTestContext::new(cx).await;
9410
9411 let injected_language = Arc::new(
9412 Language::new(
9413 LanguageConfig {
9414 brackets: BracketPairConfig {
9415 pairs: vec![
9416 BracketPair {
9417 start: "{".to_string(),
9418 end: "}".to_string(),
9419 close: false,
9420 surround: false,
9421 newline: true,
9422 },
9423 BracketPair {
9424 start: "(".to_string(),
9425 end: ")".to_string(),
9426 close: true,
9427 surround: false,
9428 newline: true,
9429 },
9430 ],
9431 ..Default::default()
9432 },
9433 name: "python".into(),
9434 ..Default::default()
9435 },
9436 Some(tree_sitter_python::LANGUAGE.into()),
9437 )
9438 .with_indents_query(
9439 r#"
9440 (_ "(" ")" @end) @indent
9441 (_ "{" "}" @end) @indent
9442 "#,
9443 )
9444 .unwrap(),
9445 );
9446
9447 let language = Arc::new(
9448 Language::new(
9449 LanguageConfig {
9450 brackets: BracketPairConfig {
9451 pairs: vec![
9452 BracketPair {
9453 start: "{".to_string(),
9454 end: "}".to_string(),
9455 close: false,
9456 surround: false,
9457 newline: true,
9458 },
9459 BracketPair {
9460 start: "(".to_string(),
9461 end: ")".to_string(),
9462 close: true,
9463 surround: false,
9464 newline: true,
9465 },
9466 ],
9467 ..Default::default()
9468 },
9469 name: LanguageName::new("rust"),
9470 ..Default::default()
9471 },
9472 Some(tree_sitter_rust::LANGUAGE.into()),
9473 )
9474 .with_indents_query(
9475 r#"
9476 (_ "(" ")" @end) @indent
9477 (_ "{" "}" @end) @indent
9478 "#,
9479 )
9480 .unwrap()
9481 .with_injection_query(
9482 r#"
9483 (macro_invocation
9484 macro: (identifier) @_macro_name
9485 (token_tree) @injection.content
9486 (#set! injection.language "python"))
9487 "#,
9488 )
9489 .unwrap(),
9490 );
9491
9492 cx.language_registry().add(injected_language);
9493 cx.language_registry().add(language.clone());
9494
9495 cx.update_buffer(|buffer, cx| {
9496 buffer.set_language(Some(language), cx);
9497 });
9498
9499 cx.set_state(r#"struct A {ˇ}"#);
9500
9501 cx.update_editor(|editor, window, cx| {
9502 editor.newline(&Default::default(), window, cx);
9503 });
9504
9505 cx.assert_editor_state(indoc!(
9506 "struct A {
9507 ˇ
9508 }"
9509 ));
9510
9511 cx.set_state(r#"select_biased!(ˇ)"#);
9512
9513 cx.update_editor(|editor, window, cx| {
9514 editor.newline(&Default::default(), window, cx);
9515 editor.handle_input("def ", window, cx);
9516 editor.handle_input("(", window, cx);
9517 editor.newline(&Default::default(), window, cx);
9518 editor.handle_input("a", window, cx);
9519 });
9520
9521 cx.assert_editor_state(indoc!(
9522 "select_biased!(
9523 def (
9524 aˇ
9525 )
9526 )"
9527 ));
9528}
9529
9530#[gpui::test]
9531async fn test_autoindent_selections(cx: &mut TestAppContext) {
9532 init_test(cx, |_| {});
9533
9534 {
9535 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9536 cx.set_state(indoc! {"
9537 impl A {
9538
9539 fn b() {}
9540
9541 «fn c() {
9542
9543 }ˇ»
9544 }
9545 "});
9546
9547 cx.update_editor(|editor, window, cx| {
9548 editor.autoindent(&Default::default(), window, cx);
9549 });
9550
9551 cx.assert_editor_state(indoc! {"
9552 impl A {
9553
9554 fn b() {}
9555
9556 «fn c() {
9557
9558 }ˇ»
9559 }
9560 "});
9561 }
9562
9563 {
9564 let mut cx = EditorTestContext::new_multibuffer(
9565 cx,
9566 [indoc! { "
9567 impl A {
9568 «
9569 // a
9570 fn b(){}
9571 »
9572 «
9573 }
9574 fn c(){}
9575 »
9576 "}],
9577 );
9578
9579 let buffer = cx.update_editor(|editor, _, cx| {
9580 let buffer = editor.buffer().update(cx, |buffer, _| {
9581 buffer.all_buffers().iter().next().unwrap().clone()
9582 });
9583 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
9584 buffer
9585 });
9586
9587 cx.run_until_parked();
9588 cx.update_editor(|editor, window, cx| {
9589 editor.select_all(&Default::default(), window, cx);
9590 editor.autoindent(&Default::default(), window, cx)
9591 });
9592 cx.run_until_parked();
9593
9594 cx.update(|_, cx| {
9595 assert_eq!(
9596 buffer.read(cx).text(),
9597 indoc! { "
9598 impl A {
9599
9600 // a
9601 fn b(){}
9602
9603
9604 }
9605 fn c(){}
9606
9607 " }
9608 )
9609 });
9610 }
9611}
9612
9613#[gpui::test]
9614async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
9615 init_test(cx, |_| {});
9616
9617 let mut cx = EditorTestContext::new(cx).await;
9618
9619 let language = Arc::new(Language::new(
9620 LanguageConfig {
9621 brackets: BracketPairConfig {
9622 pairs: vec![
9623 BracketPair {
9624 start: "{".to_string(),
9625 end: "}".to_string(),
9626 close: true,
9627 surround: true,
9628 newline: true,
9629 },
9630 BracketPair {
9631 start: "(".to_string(),
9632 end: ")".to_string(),
9633 close: true,
9634 surround: true,
9635 newline: true,
9636 },
9637 BracketPair {
9638 start: "/*".to_string(),
9639 end: " */".to_string(),
9640 close: true,
9641 surround: true,
9642 newline: true,
9643 },
9644 BracketPair {
9645 start: "[".to_string(),
9646 end: "]".to_string(),
9647 close: false,
9648 surround: false,
9649 newline: true,
9650 },
9651 BracketPair {
9652 start: "\"".to_string(),
9653 end: "\"".to_string(),
9654 close: true,
9655 surround: true,
9656 newline: false,
9657 },
9658 BracketPair {
9659 start: "<".to_string(),
9660 end: ">".to_string(),
9661 close: false,
9662 surround: true,
9663 newline: true,
9664 },
9665 ],
9666 ..Default::default()
9667 },
9668 autoclose_before: "})]".to_string(),
9669 ..Default::default()
9670 },
9671 Some(tree_sitter_rust::LANGUAGE.into()),
9672 ));
9673
9674 cx.language_registry().add(language.clone());
9675 cx.update_buffer(|buffer, cx| {
9676 buffer.set_language(Some(language), cx);
9677 });
9678
9679 cx.set_state(
9680 &r#"
9681 🏀ˇ
9682 εˇ
9683 ❤️ˇ
9684 "#
9685 .unindent(),
9686 );
9687
9688 // autoclose multiple nested brackets at multiple cursors
9689 cx.update_editor(|editor, window, cx| {
9690 editor.handle_input("{", window, cx);
9691 editor.handle_input("{", window, cx);
9692 editor.handle_input("{", window, cx);
9693 });
9694 cx.assert_editor_state(
9695 &"
9696 🏀{{{ˇ}}}
9697 ε{{{ˇ}}}
9698 ❤️{{{ˇ}}}
9699 "
9700 .unindent(),
9701 );
9702
9703 // insert a different closing bracket
9704 cx.update_editor(|editor, window, cx| {
9705 editor.handle_input(")", window, cx);
9706 });
9707 cx.assert_editor_state(
9708 &"
9709 🏀{{{)ˇ}}}
9710 ε{{{)ˇ}}}
9711 ❤️{{{)ˇ}}}
9712 "
9713 .unindent(),
9714 );
9715
9716 // skip over the auto-closed brackets when typing a closing bracket
9717 cx.update_editor(|editor, window, cx| {
9718 editor.move_right(&MoveRight, window, cx);
9719 editor.handle_input("}", window, cx);
9720 editor.handle_input("}", window, cx);
9721 editor.handle_input("}", window, cx);
9722 });
9723 cx.assert_editor_state(
9724 &"
9725 🏀{{{)}}}}ˇ
9726 ε{{{)}}}}ˇ
9727 ❤️{{{)}}}}ˇ
9728 "
9729 .unindent(),
9730 );
9731
9732 // autoclose multi-character pairs
9733 cx.set_state(
9734 &"
9735 ˇ
9736 ˇ
9737 "
9738 .unindent(),
9739 );
9740 cx.update_editor(|editor, window, cx| {
9741 editor.handle_input("/", window, cx);
9742 editor.handle_input("*", window, cx);
9743 });
9744 cx.assert_editor_state(
9745 &"
9746 /*ˇ */
9747 /*ˇ */
9748 "
9749 .unindent(),
9750 );
9751
9752 // one cursor autocloses a multi-character pair, one cursor
9753 // does not autoclose.
9754 cx.set_state(
9755 &"
9756 /ˇ
9757 ˇ
9758 "
9759 .unindent(),
9760 );
9761 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
9762 cx.assert_editor_state(
9763 &"
9764 /*ˇ */
9765 *ˇ
9766 "
9767 .unindent(),
9768 );
9769
9770 // Don't autoclose if the next character isn't whitespace and isn't
9771 // listed in the language's "autoclose_before" section.
9772 cx.set_state("ˇa b");
9773 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
9774 cx.assert_editor_state("{ˇa b");
9775
9776 // Don't autoclose if `close` is false for the bracket pair
9777 cx.set_state("ˇ");
9778 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
9779 cx.assert_editor_state("[ˇ");
9780
9781 // Surround with brackets if text is selected
9782 cx.set_state("«aˇ» b");
9783 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
9784 cx.assert_editor_state("{«aˇ»} b");
9785
9786 // Autoclose when not immediately after a word character
9787 cx.set_state("a ˇ");
9788 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9789 cx.assert_editor_state("a \"ˇ\"");
9790
9791 // Autoclose pair where the start and end characters are the same
9792 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9793 cx.assert_editor_state("a \"\"ˇ");
9794
9795 // Don't autoclose when immediately after a word character
9796 cx.set_state("aˇ");
9797 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9798 cx.assert_editor_state("a\"ˇ");
9799
9800 // Do autoclose when after a non-word character
9801 cx.set_state("{ˇ");
9802 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9803 cx.assert_editor_state("{\"ˇ\"");
9804
9805 // Non identical pairs autoclose regardless of preceding character
9806 cx.set_state("aˇ");
9807 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
9808 cx.assert_editor_state("a{ˇ}");
9809
9810 // Don't autoclose pair if autoclose is disabled
9811 cx.set_state("ˇ");
9812 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
9813 cx.assert_editor_state("<ˇ");
9814
9815 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
9816 cx.set_state("«aˇ» b");
9817 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
9818 cx.assert_editor_state("<«aˇ»> b");
9819}
9820
9821#[gpui::test]
9822async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
9823 init_test(cx, |settings| {
9824 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
9825 });
9826
9827 let mut cx = EditorTestContext::new(cx).await;
9828
9829 let language = Arc::new(Language::new(
9830 LanguageConfig {
9831 brackets: BracketPairConfig {
9832 pairs: vec![
9833 BracketPair {
9834 start: "{".to_string(),
9835 end: "}".to_string(),
9836 close: true,
9837 surround: true,
9838 newline: true,
9839 },
9840 BracketPair {
9841 start: "(".to_string(),
9842 end: ")".to_string(),
9843 close: true,
9844 surround: true,
9845 newline: true,
9846 },
9847 BracketPair {
9848 start: "[".to_string(),
9849 end: "]".to_string(),
9850 close: false,
9851 surround: false,
9852 newline: true,
9853 },
9854 ],
9855 ..Default::default()
9856 },
9857 autoclose_before: "})]".to_string(),
9858 ..Default::default()
9859 },
9860 Some(tree_sitter_rust::LANGUAGE.into()),
9861 ));
9862
9863 cx.language_registry().add(language.clone());
9864 cx.update_buffer(|buffer, cx| {
9865 buffer.set_language(Some(language), cx);
9866 });
9867
9868 cx.set_state(
9869 &"
9870 ˇ
9871 ˇ
9872 ˇ
9873 "
9874 .unindent(),
9875 );
9876
9877 // ensure only matching closing brackets are skipped over
9878 cx.update_editor(|editor, window, cx| {
9879 editor.handle_input("}", window, cx);
9880 editor.move_left(&MoveLeft, window, cx);
9881 editor.handle_input(")", window, cx);
9882 editor.move_left(&MoveLeft, window, cx);
9883 });
9884 cx.assert_editor_state(
9885 &"
9886 ˇ)}
9887 ˇ)}
9888 ˇ)}
9889 "
9890 .unindent(),
9891 );
9892
9893 // skip-over closing brackets at multiple cursors
9894 cx.update_editor(|editor, window, cx| {
9895 editor.handle_input(")", window, cx);
9896 editor.handle_input("}", window, cx);
9897 });
9898 cx.assert_editor_state(
9899 &"
9900 )}ˇ
9901 )}ˇ
9902 )}ˇ
9903 "
9904 .unindent(),
9905 );
9906
9907 // ignore non-close brackets
9908 cx.update_editor(|editor, window, cx| {
9909 editor.handle_input("]", window, cx);
9910 editor.move_left(&MoveLeft, window, cx);
9911 editor.handle_input("]", window, cx);
9912 });
9913 cx.assert_editor_state(
9914 &"
9915 )}]ˇ]
9916 )}]ˇ]
9917 )}]ˇ]
9918 "
9919 .unindent(),
9920 );
9921}
9922
9923#[gpui::test]
9924async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
9925 init_test(cx, |_| {});
9926
9927 let mut cx = EditorTestContext::new(cx).await;
9928
9929 let html_language = Arc::new(
9930 Language::new(
9931 LanguageConfig {
9932 name: "HTML".into(),
9933 brackets: BracketPairConfig {
9934 pairs: vec![
9935 BracketPair {
9936 start: "<".into(),
9937 end: ">".into(),
9938 close: true,
9939 ..Default::default()
9940 },
9941 BracketPair {
9942 start: "{".into(),
9943 end: "}".into(),
9944 close: true,
9945 ..Default::default()
9946 },
9947 BracketPair {
9948 start: "(".into(),
9949 end: ")".into(),
9950 close: true,
9951 ..Default::default()
9952 },
9953 ],
9954 ..Default::default()
9955 },
9956 autoclose_before: "})]>".into(),
9957 ..Default::default()
9958 },
9959 Some(tree_sitter_html::LANGUAGE.into()),
9960 )
9961 .with_injection_query(
9962 r#"
9963 (script_element
9964 (raw_text) @injection.content
9965 (#set! injection.language "javascript"))
9966 "#,
9967 )
9968 .unwrap(),
9969 );
9970
9971 let javascript_language = Arc::new(Language::new(
9972 LanguageConfig {
9973 name: "JavaScript".into(),
9974 brackets: BracketPairConfig {
9975 pairs: vec![
9976 BracketPair {
9977 start: "/*".into(),
9978 end: " */".into(),
9979 close: true,
9980 ..Default::default()
9981 },
9982 BracketPair {
9983 start: "{".into(),
9984 end: "}".into(),
9985 close: true,
9986 ..Default::default()
9987 },
9988 BracketPair {
9989 start: "(".into(),
9990 end: ")".into(),
9991 close: true,
9992 ..Default::default()
9993 },
9994 ],
9995 ..Default::default()
9996 },
9997 autoclose_before: "})]>".into(),
9998 ..Default::default()
9999 },
10000 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
10001 ));
10002
10003 cx.language_registry().add(html_language.clone());
10004 cx.language_registry().add(javascript_language);
10005 cx.executor().run_until_parked();
10006
10007 cx.update_buffer(|buffer, cx| {
10008 buffer.set_language(Some(html_language), cx);
10009 });
10010
10011 cx.set_state(
10012 &r#"
10013 <body>ˇ
10014 <script>
10015 var x = 1;ˇ
10016 </script>
10017 </body>ˇ
10018 "#
10019 .unindent(),
10020 );
10021
10022 // Precondition: different languages are active at different locations.
10023 cx.update_editor(|editor, window, cx| {
10024 let snapshot = editor.snapshot(window, cx);
10025 let cursors = editor.selections.ranges::<usize>(cx);
10026 let languages = cursors
10027 .iter()
10028 .map(|c| snapshot.language_at(c.start).unwrap().name())
10029 .collect::<Vec<_>>();
10030 assert_eq!(
10031 languages,
10032 &["HTML".into(), "JavaScript".into(), "HTML".into()]
10033 );
10034 });
10035
10036 // Angle brackets autoclose in HTML, but not JavaScript.
10037 cx.update_editor(|editor, window, cx| {
10038 editor.handle_input("<", window, cx);
10039 editor.handle_input("a", window, cx);
10040 });
10041 cx.assert_editor_state(
10042 &r#"
10043 <body><aˇ>
10044 <script>
10045 var x = 1;<aˇ
10046 </script>
10047 </body><aˇ>
10048 "#
10049 .unindent(),
10050 );
10051
10052 // Curly braces and parens autoclose in both HTML and JavaScript.
10053 cx.update_editor(|editor, window, cx| {
10054 editor.handle_input(" b=", window, cx);
10055 editor.handle_input("{", window, cx);
10056 editor.handle_input("c", window, cx);
10057 editor.handle_input("(", window, cx);
10058 });
10059 cx.assert_editor_state(
10060 &r#"
10061 <body><a b={c(ˇ)}>
10062 <script>
10063 var x = 1;<a b={c(ˇ)}
10064 </script>
10065 </body><a b={c(ˇ)}>
10066 "#
10067 .unindent(),
10068 );
10069
10070 // Brackets that were already autoclosed are skipped.
10071 cx.update_editor(|editor, window, cx| {
10072 editor.handle_input(")", window, cx);
10073 editor.handle_input("d", window, cx);
10074 editor.handle_input("}", window, cx);
10075 });
10076 cx.assert_editor_state(
10077 &r#"
10078 <body><a b={c()d}ˇ>
10079 <script>
10080 var x = 1;<a b={c()d}ˇ
10081 </script>
10082 </body><a b={c()d}ˇ>
10083 "#
10084 .unindent(),
10085 );
10086 cx.update_editor(|editor, window, cx| {
10087 editor.handle_input(">", window, cx);
10088 });
10089 cx.assert_editor_state(
10090 &r#"
10091 <body><a b={c()d}>ˇ
10092 <script>
10093 var x = 1;<a b={c()d}>ˇ
10094 </script>
10095 </body><a b={c()d}>ˇ
10096 "#
10097 .unindent(),
10098 );
10099
10100 // Reset
10101 cx.set_state(
10102 &r#"
10103 <body>ˇ
10104 <script>
10105 var x = 1;ˇ
10106 </script>
10107 </body>ˇ
10108 "#
10109 .unindent(),
10110 );
10111
10112 cx.update_editor(|editor, window, cx| {
10113 editor.handle_input("<", window, cx);
10114 });
10115 cx.assert_editor_state(
10116 &r#"
10117 <body><ˇ>
10118 <script>
10119 var x = 1;<ˇ
10120 </script>
10121 </body><ˇ>
10122 "#
10123 .unindent(),
10124 );
10125
10126 // When backspacing, the closing angle brackets are removed.
10127 cx.update_editor(|editor, window, cx| {
10128 editor.backspace(&Backspace, window, cx);
10129 });
10130 cx.assert_editor_state(
10131 &r#"
10132 <body>ˇ
10133 <script>
10134 var x = 1;ˇ
10135 </script>
10136 </body>ˇ
10137 "#
10138 .unindent(),
10139 );
10140
10141 // Block comments autoclose in JavaScript, but not HTML.
10142 cx.update_editor(|editor, window, cx| {
10143 editor.handle_input("/", window, cx);
10144 editor.handle_input("*", window, cx);
10145 });
10146 cx.assert_editor_state(
10147 &r#"
10148 <body>/*ˇ
10149 <script>
10150 var x = 1;/*ˇ */
10151 </script>
10152 </body>/*ˇ
10153 "#
10154 .unindent(),
10155 );
10156}
10157
10158#[gpui::test]
10159async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
10160 init_test(cx, |_| {});
10161
10162 let mut cx = EditorTestContext::new(cx).await;
10163
10164 let rust_language = Arc::new(
10165 Language::new(
10166 LanguageConfig {
10167 name: "Rust".into(),
10168 brackets: serde_json::from_value(json!([
10169 { "start": "{", "end": "}", "close": true, "newline": true },
10170 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
10171 ]))
10172 .unwrap(),
10173 autoclose_before: "})]>".into(),
10174 ..Default::default()
10175 },
10176 Some(tree_sitter_rust::LANGUAGE.into()),
10177 )
10178 .with_override_query("(string_literal) @string")
10179 .unwrap(),
10180 );
10181
10182 cx.language_registry().add(rust_language.clone());
10183 cx.update_buffer(|buffer, cx| {
10184 buffer.set_language(Some(rust_language), cx);
10185 });
10186
10187 cx.set_state(
10188 &r#"
10189 let x = ˇ
10190 "#
10191 .unindent(),
10192 );
10193
10194 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
10195 cx.update_editor(|editor, window, cx| {
10196 editor.handle_input("\"", window, cx);
10197 });
10198 cx.assert_editor_state(
10199 &r#"
10200 let x = "ˇ"
10201 "#
10202 .unindent(),
10203 );
10204
10205 // Inserting another quotation mark. The cursor moves across the existing
10206 // automatically-inserted quotation mark.
10207 cx.update_editor(|editor, window, cx| {
10208 editor.handle_input("\"", window, cx);
10209 });
10210 cx.assert_editor_state(
10211 &r#"
10212 let x = ""ˇ
10213 "#
10214 .unindent(),
10215 );
10216
10217 // Reset
10218 cx.set_state(
10219 &r#"
10220 let x = ˇ
10221 "#
10222 .unindent(),
10223 );
10224
10225 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
10226 cx.update_editor(|editor, window, cx| {
10227 editor.handle_input("\"", window, cx);
10228 editor.handle_input(" ", window, cx);
10229 editor.move_left(&Default::default(), window, cx);
10230 editor.handle_input("\\", window, cx);
10231 editor.handle_input("\"", window, cx);
10232 });
10233 cx.assert_editor_state(
10234 &r#"
10235 let x = "\"ˇ "
10236 "#
10237 .unindent(),
10238 );
10239
10240 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
10241 // mark. Nothing is inserted.
10242 cx.update_editor(|editor, window, cx| {
10243 editor.move_right(&Default::default(), window, cx);
10244 editor.handle_input("\"", window, cx);
10245 });
10246 cx.assert_editor_state(
10247 &r#"
10248 let x = "\" "ˇ
10249 "#
10250 .unindent(),
10251 );
10252}
10253
10254#[gpui::test]
10255async fn test_surround_with_pair(cx: &mut TestAppContext) {
10256 init_test(cx, |_| {});
10257
10258 let language = Arc::new(Language::new(
10259 LanguageConfig {
10260 brackets: BracketPairConfig {
10261 pairs: vec![
10262 BracketPair {
10263 start: "{".to_string(),
10264 end: "}".to_string(),
10265 close: true,
10266 surround: true,
10267 newline: true,
10268 },
10269 BracketPair {
10270 start: "/* ".to_string(),
10271 end: "*/".to_string(),
10272 close: true,
10273 surround: true,
10274 ..Default::default()
10275 },
10276 ],
10277 ..Default::default()
10278 },
10279 ..Default::default()
10280 },
10281 Some(tree_sitter_rust::LANGUAGE.into()),
10282 ));
10283
10284 let text = r#"
10285 a
10286 b
10287 c
10288 "#
10289 .unindent();
10290
10291 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10292 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10293 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10294 editor
10295 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10296 .await;
10297
10298 editor.update_in(cx, |editor, window, cx| {
10299 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10300 s.select_display_ranges([
10301 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10302 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10303 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
10304 ])
10305 });
10306
10307 editor.handle_input("{", window, cx);
10308 editor.handle_input("{", window, cx);
10309 editor.handle_input("{", window, cx);
10310 assert_eq!(
10311 editor.text(cx),
10312 "
10313 {{{a}}}
10314 {{{b}}}
10315 {{{c}}}
10316 "
10317 .unindent()
10318 );
10319 assert_eq!(
10320 editor.selections.display_ranges(cx),
10321 [
10322 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
10323 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
10324 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
10325 ]
10326 );
10327
10328 editor.undo(&Undo, window, cx);
10329 editor.undo(&Undo, window, cx);
10330 editor.undo(&Undo, window, cx);
10331 assert_eq!(
10332 editor.text(cx),
10333 "
10334 a
10335 b
10336 c
10337 "
10338 .unindent()
10339 );
10340 assert_eq!(
10341 editor.selections.display_ranges(cx),
10342 [
10343 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10344 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10345 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10346 ]
10347 );
10348
10349 // Ensure inserting the first character of a multi-byte bracket pair
10350 // doesn't surround the selections with the bracket.
10351 editor.handle_input("/", window, cx);
10352 assert_eq!(
10353 editor.text(cx),
10354 "
10355 /
10356 /
10357 /
10358 "
10359 .unindent()
10360 );
10361 assert_eq!(
10362 editor.selections.display_ranges(cx),
10363 [
10364 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10365 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10366 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10367 ]
10368 );
10369
10370 editor.undo(&Undo, window, cx);
10371 assert_eq!(
10372 editor.text(cx),
10373 "
10374 a
10375 b
10376 c
10377 "
10378 .unindent()
10379 );
10380 assert_eq!(
10381 editor.selections.display_ranges(cx),
10382 [
10383 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10384 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10385 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10386 ]
10387 );
10388
10389 // Ensure inserting the last character of a multi-byte bracket pair
10390 // doesn't surround the selections with the bracket.
10391 editor.handle_input("*", window, cx);
10392 assert_eq!(
10393 editor.text(cx),
10394 "
10395 *
10396 *
10397 *
10398 "
10399 .unindent()
10400 );
10401 assert_eq!(
10402 editor.selections.display_ranges(cx),
10403 [
10404 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10405 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10406 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10407 ]
10408 );
10409 });
10410}
10411
10412#[gpui::test]
10413async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
10414 init_test(cx, |_| {});
10415
10416 let language = Arc::new(Language::new(
10417 LanguageConfig {
10418 brackets: BracketPairConfig {
10419 pairs: vec![BracketPair {
10420 start: "{".to_string(),
10421 end: "}".to_string(),
10422 close: true,
10423 surround: true,
10424 newline: true,
10425 }],
10426 ..Default::default()
10427 },
10428 autoclose_before: "}".to_string(),
10429 ..Default::default()
10430 },
10431 Some(tree_sitter_rust::LANGUAGE.into()),
10432 ));
10433
10434 let text = r#"
10435 a
10436 b
10437 c
10438 "#
10439 .unindent();
10440
10441 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10442 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10443 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10444 editor
10445 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10446 .await;
10447
10448 editor.update_in(cx, |editor, window, cx| {
10449 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10450 s.select_ranges([
10451 Point::new(0, 1)..Point::new(0, 1),
10452 Point::new(1, 1)..Point::new(1, 1),
10453 Point::new(2, 1)..Point::new(2, 1),
10454 ])
10455 });
10456
10457 editor.handle_input("{", window, cx);
10458 editor.handle_input("{", window, cx);
10459 editor.handle_input("_", window, cx);
10460 assert_eq!(
10461 editor.text(cx),
10462 "
10463 a{{_}}
10464 b{{_}}
10465 c{{_}}
10466 "
10467 .unindent()
10468 );
10469 assert_eq!(
10470 editor.selections.ranges::<Point>(cx),
10471 [
10472 Point::new(0, 4)..Point::new(0, 4),
10473 Point::new(1, 4)..Point::new(1, 4),
10474 Point::new(2, 4)..Point::new(2, 4)
10475 ]
10476 );
10477
10478 editor.backspace(&Default::default(), window, cx);
10479 editor.backspace(&Default::default(), window, cx);
10480 assert_eq!(
10481 editor.text(cx),
10482 "
10483 a{}
10484 b{}
10485 c{}
10486 "
10487 .unindent()
10488 );
10489 assert_eq!(
10490 editor.selections.ranges::<Point>(cx),
10491 [
10492 Point::new(0, 2)..Point::new(0, 2),
10493 Point::new(1, 2)..Point::new(1, 2),
10494 Point::new(2, 2)..Point::new(2, 2)
10495 ]
10496 );
10497
10498 editor.delete_to_previous_word_start(&Default::default(), window, cx);
10499 assert_eq!(
10500 editor.text(cx),
10501 "
10502 a
10503 b
10504 c
10505 "
10506 .unindent()
10507 );
10508 assert_eq!(
10509 editor.selections.ranges::<Point>(cx),
10510 [
10511 Point::new(0, 1)..Point::new(0, 1),
10512 Point::new(1, 1)..Point::new(1, 1),
10513 Point::new(2, 1)..Point::new(2, 1)
10514 ]
10515 );
10516 });
10517}
10518
10519#[gpui::test]
10520async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
10521 init_test(cx, |settings| {
10522 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10523 });
10524
10525 let mut cx = EditorTestContext::new(cx).await;
10526
10527 let language = Arc::new(Language::new(
10528 LanguageConfig {
10529 brackets: BracketPairConfig {
10530 pairs: vec![
10531 BracketPair {
10532 start: "{".to_string(),
10533 end: "}".to_string(),
10534 close: true,
10535 surround: true,
10536 newline: true,
10537 },
10538 BracketPair {
10539 start: "(".to_string(),
10540 end: ")".to_string(),
10541 close: true,
10542 surround: true,
10543 newline: true,
10544 },
10545 BracketPair {
10546 start: "[".to_string(),
10547 end: "]".to_string(),
10548 close: false,
10549 surround: true,
10550 newline: true,
10551 },
10552 ],
10553 ..Default::default()
10554 },
10555 autoclose_before: "})]".to_string(),
10556 ..Default::default()
10557 },
10558 Some(tree_sitter_rust::LANGUAGE.into()),
10559 ));
10560
10561 cx.language_registry().add(language.clone());
10562 cx.update_buffer(|buffer, cx| {
10563 buffer.set_language(Some(language), cx);
10564 });
10565
10566 cx.set_state(
10567 &"
10568 {(ˇ)}
10569 [[ˇ]]
10570 {(ˇ)}
10571 "
10572 .unindent(),
10573 );
10574
10575 cx.update_editor(|editor, window, cx| {
10576 editor.backspace(&Default::default(), window, cx);
10577 editor.backspace(&Default::default(), window, cx);
10578 });
10579
10580 cx.assert_editor_state(
10581 &"
10582 ˇ
10583 ˇ]]
10584 ˇ
10585 "
10586 .unindent(),
10587 );
10588
10589 cx.update_editor(|editor, window, cx| {
10590 editor.handle_input("{", window, cx);
10591 editor.handle_input("{", window, cx);
10592 editor.move_right(&MoveRight, window, cx);
10593 editor.move_right(&MoveRight, window, cx);
10594 editor.move_left(&MoveLeft, window, cx);
10595 editor.move_left(&MoveLeft, window, cx);
10596 editor.backspace(&Default::default(), window, cx);
10597 });
10598
10599 cx.assert_editor_state(
10600 &"
10601 {ˇ}
10602 {ˇ}]]
10603 {ˇ}
10604 "
10605 .unindent(),
10606 );
10607
10608 cx.update_editor(|editor, window, cx| {
10609 editor.backspace(&Default::default(), window, cx);
10610 });
10611
10612 cx.assert_editor_state(
10613 &"
10614 ˇ
10615 ˇ]]
10616 ˇ
10617 "
10618 .unindent(),
10619 );
10620}
10621
10622#[gpui::test]
10623async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
10624 init_test(cx, |_| {});
10625
10626 let language = Arc::new(Language::new(
10627 LanguageConfig::default(),
10628 Some(tree_sitter_rust::LANGUAGE.into()),
10629 ));
10630
10631 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
10632 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10633 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10634 editor
10635 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10636 .await;
10637
10638 editor.update_in(cx, |editor, window, cx| {
10639 editor.set_auto_replace_emoji_shortcode(true);
10640
10641 editor.handle_input("Hello ", window, cx);
10642 editor.handle_input(":wave", window, cx);
10643 assert_eq!(editor.text(cx), "Hello :wave".unindent());
10644
10645 editor.handle_input(":", window, cx);
10646 assert_eq!(editor.text(cx), "Hello 👋".unindent());
10647
10648 editor.handle_input(" :smile", window, cx);
10649 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
10650
10651 editor.handle_input(":", window, cx);
10652 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
10653
10654 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
10655 editor.handle_input(":wave", window, cx);
10656 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
10657
10658 editor.handle_input(":", window, cx);
10659 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
10660
10661 editor.handle_input(":1", window, cx);
10662 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
10663
10664 editor.handle_input(":", window, cx);
10665 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
10666
10667 // Ensure shortcode does not get replaced when it is part of a word
10668 editor.handle_input(" Test:wave", window, cx);
10669 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
10670
10671 editor.handle_input(":", window, cx);
10672 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
10673
10674 editor.set_auto_replace_emoji_shortcode(false);
10675
10676 // Ensure shortcode does not get replaced when auto replace is off
10677 editor.handle_input(" :wave", window, cx);
10678 assert_eq!(
10679 editor.text(cx),
10680 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
10681 );
10682
10683 editor.handle_input(":", window, cx);
10684 assert_eq!(
10685 editor.text(cx),
10686 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
10687 );
10688 });
10689}
10690
10691#[gpui::test]
10692async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
10693 init_test(cx, |_| {});
10694
10695 let (text, insertion_ranges) = marked_text_ranges(
10696 indoc! {"
10697 ˇ
10698 "},
10699 false,
10700 );
10701
10702 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
10703 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10704
10705 _ = editor.update_in(cx, |editor, window, cx| {
10706 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
10707
10708 editor
10709 .insert_snippet(&insertion_ranges, snippet, window, cx)
10710 .unwrap();
10711
10712 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
10713 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
10714 assert_eq!(editor.text(cx), expected_text);
10715 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
10716 }
10717
10718 assert(
10719 editor,
10720 cx,
10721 indoc! {"
10722 type «» =•
10723 "},
10724 );
10725
10726 assert!(editor.context_menu_visible(), "There should be a matches");
10727 });
10728}
10729
10730#[gpui::test]
10731async fn test_snippets(cx: &mut TestAppContext) {
10732 init_test(cx, |_| {});
10733
10734 let mut cx = EditorTestContext::new(cx).await;
10735
10736 cx.set_state(indoc! {"
10737 a.ˇ b
10738 a.ˇ b
10739 a.ˇ b
10740 "});
10741
10742 cx.update_editor(|editor, window, cx| {
10743 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
10744 let insertion_ranges = editor
10745 .selections
10746 .all(cx)
10747 .iter()
10748 .map(|s| s.range())
10749 .collect::<Vec<_>>();
10750 editor
10751 .insert_snippet(&insertion_ranges, snippet, window, cx)
10752 .unwrap();
10753 });
10754
10755 cx.assert_editor_state(indoc! {"
10756 a.f(«oneˇ», two, «threeˇ») b
10757 a.f(«oneˇ», two, «threeˇ») b
10758 a.f(«oneˇ», two, «threeˇ») b
10759 "});
10760
10761 // Can't move earlier than the first tab stop
10762 cx.update_editor(|editor, window, cx| {
10763 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
10764 });
10765 cx.assert_editor_state(indoc! {"
10766 a.f(«oneˇ», two, «threeˇ») b
10767 a.f(«oneˇ», two, «threeˇ») b
10768 a.f(«oneˇ», two, «threeˇ») b
10769 "});
10770
10771 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10772 cx.assert_editor_state(indoc! {"
10773 a.f(one, «twoˇ», three) b
10774 a.f(one, «twoˇ», three) b
10775 a.f(one, «twoˇ», three) b
10776 "});
10777
10778 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
10779 cx.assert_editor_state(indoc! {"
10780 a.f(«oneˇ», two, «threeˇ») b
10781 a.f(«oneˇ», two, «threeˇ») b
10782 a.f(«oneˇ», two, «threeˇ») b
10783 "});
10784
10785 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10786 cx.assert_editor_state(indoc! {"
10787 a.f(one, «twoˇ», three) b
10788 a.f(one, «twoˇ», three) b
10789 a.f(one, «twoˇ», three) b
10790 "});
10791 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10792 cx.assert_editor_state(indoc! {"
10793 a.f(one, two, three)ˇ b
10794 a.f(one, two, three)ˇ b
10795 a.f(one, two, three)ˇ b
10796 "});
10797
10798 // As soon as the last tab stop is reached, snippet state is gone
10799 cx.update_editor(|editor, window, cx| {
10800 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
10801 });
10802 cx.assert_editor_state(indoc! {"
10803 a.f(one, two, three)ˇ b
10804 a.f(one, two, three)ˇ b
10805 a.f(one, two, three)ˇ b
10806 "});
10807}
10808
10809#[gpui::test]
10810async fn test_snippet_indentation(cx: &mut TestAppContext) {
10811 init_test(cx, |_| {});
10812
10813 let mut cx = EditorTestContext::new(cx).await;
10814
10815 cx.update_editor(|editor, window, cx| {
10816 let snippet = Snippet::parse(indoc! {"
10817 /*
10818 * Multiline comment with leading indentation
10819 *
10820 * $1
10821 */
10822 $0"})
10823 .unwrap();
10824 let insertion_ranges = editor
10825 .selections
10826 .all(cx)
10827 .iter()
10828 .map(|s| s.range())
10829 .collect::<Vec<_>>();
10830 editor
10831 .insert_snippet(&insertion_ranges, snippet, window, cx)
10832 .unwrap();
10833 });
10834
10835 cx.assert_editor_state(indoc! {"
10836 /*
10837 * Multiline comment with leading indentation
10838 *
10839 * ˇ
10840 */
10841 "});
10842
10843 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10844 cx.assert_editor_state(indoc! {"
10845 /*
10846 * Multiline comment with leading indentation
10847 *
10848 *•
10849 */
10850 ˇ"});
10851}
10852
10853#[gpui::test]
10854async fn test_document_format_during_save(cx: &mut TestAppContext) {
10855 init_test(cx, |_| {});
10856
10857 let fs = FakeFs::new(cx.executor());
10858 fs.insert_file(path!("/file.rs"), Default::default()).await;
10859
10860 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
10861
10862 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10863 language_registry.add(rust_lang());
10864 let mut fake_servers = language_registry.register_fake_lsp(
10865 "Rust",
10866 FakeLspAdapter {
10867 capabilities: lsp::ServerCapabilities {
10868 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10869 ..Default::default()
10870 },
10871 ..Default::default()
10872 },
10873 );
10874
10875 let buffer = project
10876 .update(cx, |project, cx| {
10877 project.open_local_buffer(path!("/file.rs"), cx)
10878 })
10879 .await
10880 .unwrap();
10881
10882 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10883 let (editor, cx) = cx.add_window_view(|window, cx| {
10884 build_editor_with_project(project.clone(), buffer, window, cx)
10885 });
10886 editor.update_in(cx, |editor, window, cx| {
10887 editor.set_text("one\ntwo\nthree\n", window, cx)
10888 });
10889 assert!(cx.read(|cx| editor.is_dirty(cx)));
10890
10891 cx.executor().start_waiting();
10892 let fake_server = fake_servers.next().await.unwrap();
10893
10894 {
10895 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10896 move |params, _| async move {
10897 assert_eq!(
10898 params.text_document.uri,
10899 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
10900 );
10901 assert_eq!(params.options.tab_size, 4);
10902 Ok(Some(vec![lsp::TextEdit::new(
10903 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10904 ", ".to_string(),
10905 )]))
10906 },
10907 );
10908 let save = editor
10909 .update_in(cx, |editor, window, cx| {
10910 editor.save(
10911 SaveOptions {
10912 format: true,
10913 autosave: false,
10914 },
10915 project.clone(),
10916 window,
10917 cx,
10918 )
10919 })
10920 .unwrap();
10921 cx.executor().start_waiting();
10922 save.await;
10923
10924 assert_eq!(
10925 editor.update(cx, |editor, cx| editor.text(cx)),
10926 "one, two\nthree\n"
10927 );
10928 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10929 }
10930
10931 {
10932 editor.update_in(cx, |editor, window, cx| {
10933 editor.set_text("one\ntwo\nthree\n", window, cx)
10934 });
10935 assert!(cx.read(|cx| editor.is_dirty(cx)));
10936
10937 // Ensure we can still save even if formatting hangs.
10938 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10939 move |params, _| async move {
10940 assert_eq!(
10941 params.text_document.uri,
10942 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
10943 );
10944 futures::future::pending::<()>().await;
10945 unreachable!()
10946 },
10947 );
10948 let save = editor
10949 .update_in(cx, |editor, window, cx| {
10950 editor.save(
10951 SaveOptions {
10952 format: true,
10953 autosave: false,
10954 },
10955 project.clone(),
10956 window,
10957 cx,
10958 )
10959 })
10960 .unwrap();
10961 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
10962 cx.executor().start_waiting();
10963 save.await;
10964 assert_eq!(
10965 editor.update(cx, |editor, cx| editor.text(cx)),
10966 "one\ntwo\nthree\n"
10967 );
10968 }
10969
10970 // Set rust language override and assert overridden tabsize is sent to language server
10971 update_test_language_settings(cx, |settings| {
10972 settings.languages.0.insert(
10973 "Rust".into(),
10974 LanguageSettingsContent {
10975 tab_size: NonZeroU32::new(8),
10976 ..Default::default()
10977 },
10978 );
10979 });
10980
10981 {
10982 editor.update_in(cx, |editor, window, cx| {
10983 editor.set_text("somehting_new\n", window, cx)
10984 });
10985 assert!(cx.read(|cx| editor.is_dirty(cx)));
10986 let _formatting_request_signal = fake_server
10987 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
10988 assert_eq!(
10989 params.text_document.uri,
10990 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
10991 );
10992 assert_eq!(params.options.tab_size, 8);
10993 Ok(Some(vec![]))
10994 });
10995 let save = editor
10996 .update_in(cx, |editor, window, cx| {
10997 editor.save(
10998 SaveOptions {
10999 format: true,
11000 autosave: false,
11001 },
11002 project.clone(),
11003 window,
11004 cx,
11005 )
11006 })
11007 .unwrap();
11008 cx.executor().start_waiting();
11009 save.await;
11010 }
11011}
11012
11013#[gpui::test]
11014async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
11015 init_test(cx, |settings| {
11016 settings.defaults.ensure_final_newline_on_save = Some(false);
11017 });
11018
11019 let fs = FakeFs::new(cx.executor());
11020 fs.insert_file(path!("/file.txt"), "foo".into()).await;
11021
11022 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
11023
11024 let buffer = project
11025 .update(cx, |project, cx| {
11026 project.open_local_buffer(path!("/file.txt"), cx)
11027 })
11028 .await
11029 .unwrap();
11030
11031 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11032 let (editor, cx) = cx.add_window_view(|window, cx| {
11033 build_editor_with_project(project.clone(), buffer, window, cx)
11034 });
11035 editor.update_in(cx, |editor, window, cx| {
11036 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
11037 s.select_ranges([0..0])
11038 });
11039 });
11040 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11041
11042 editor.update_in(cx, |editor, window, cx| {
11043 editor.handle_input("\n", window, cx)
11044 });
11045 cx.run_until_parked();
11046 save(&editor, &project, cx).await;
11047 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11048
11049 editor.update_in(cx, |editor, window, cx| {
11050 editor.undo(&Default::default(), window, cx);
11051 });
11052 save(&editor, &project, cx).await;
11053 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11054
11055 editor.update_in(cx, |editor, window, cx| {
11056 editor.redo(&Default::default(), window, cx);
11057 });
11058 cx.run_until_parked();
11059 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11060
11061 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
11062 let save = editor
11063 .update_in(cx, |editor, window, cx| {
11064 editor.save(
11065 SaveOptions {
11066 format: true,
11067 autosave: false,
11068 },
11069 project.clone(),
11070 window,
11071 cx,
11072 )
11073 })
11074 .unwrap();
11075 cx.executor().start_waiting();
11076 save.await;
11077 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11078 }
11079}
11080
11081#[gpui::test]
11082async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
11083 init_test(cx, |_| {});
11084
11085 let cols = 4;
11086 let rows = 10;
11087 let sample_text_1 = sample_text(rows, cols, 'a');
11088 assert_eq!(
11089 sample_text_1,
11090 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
11091 );
11092 let sample_text_2 = sample_text(rows, cols, 'l');
11093 assert_eq!(
11094 sample_text_2,
11095 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
11096 );
11097 let sample_text_3 = sample_text(rows, cols, 'v');
11098 assert_eq!(
11099 sample_text_3,
11100 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
11101 );
11102
11103 let fs = FakeFs::new(cx.executor());
11104 fs.insert_tree(
11105 path!("/a"),
11106 json!({
11107 "main.rs": sample_text_1,
11108 "other.rs": sample_text_2,
11109 "lib.rs": sample_text_3,
11110 }),
11111 )
11112 .await;
11113
11114 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11115 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11116 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11117
11118 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11119 language_registry.add(rust_lang());
11120 let mut fake_servers = language_registry.register_fake_lsp(
11121 "Rust",
11122 FakeLspAdapter {
11123 capabilities: lsp::ServerCapabilities {
11124 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11125 ..Default::default()
11126 },
11127 ..Default::default()
11128 },
11129 );
11130
11131 let worktree = project.update(cx, |project, cx| {
11132 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
11133 assert_eq!(worktrees.len(), 1);
11134 worktrees.pop().unwrap()
11135 });
11136 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11137
11138 let buffer_1 = project
11139 .update(cx, |project, cx| {
11140 project.open_buffer((worktree_id, "main.rs"), cx)
11141 })
11142 .await
11143 .unwrap();
11144 let buffer_2 = project
11145 .update(cx, |project, cx| {
11146 project.open_buffer((worktree_id, "other.rs"), cx)
11147 })
11148 .await
11149 .unwrap();
11150 let buffer_3 = project
11151 .update(cx, |project, cx| {
11152 project.open_buffer((worktree_id, "lib.rs"), cx)
11153 })
11154 .await
11155 .unwrap();
11156
11157 let multi_buffer = cx.new(|cx| {
11158 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11159 multi_buffer.push_excerpts(
11160 buffer_1.clone(),
11161 [
11162 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11163 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11164 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11165 ],
11166 cx,
11167 );
11168 multi_buffer.push_excerpts(
11169 buffer_2.clone(),
11170 [
11171 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11172 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11173 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11174 ],
11175 cx,
11176 );
11177 multi_buffer.push_excerpts(
11178 buffer_3.clone(),
11179 [
11180 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11181 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11182 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11183 ],
11184 cx,
11185 );
11186 multi_buffer
11187 });
11188 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
11189 Editor::new(
11190 EditorMode::full(),
11191 multi_buffer,
11192 Some(project.clone()),
11193 window,
11194 cx,
11195 )
11196 });
11197
11198 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11199 editor.change_selections(
11200 SelectionEffects::scroll(Autoscroll::Next),
11201 window,
11202 cx,
11203 |s| s.select_ranges(Some(1..2)),
11204 );
11205 editor.insert("|one|two|three|", window, cx);
11206 });
11207 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11208 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11209 editor.change_selections(
11210 SelectionEffects::scroll(Autoscroll::Next),
11211 window,
11212 cx,
11213 |s| s.select_ranges(Some(60..70)),
11214 );
11215 editor.insert("|four|five|six|", window, cx);
11216 });
11217 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11218
11219 // First two buffers should be edited, but not the third one.
11220 assert_eq!(
11221 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11222 "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}",
11223 );
11224 buffer_1.update(cx, |buffer, _| {
11225 assert!(buffer.is_dirty());
11226 assert_eq!(
11227 buffer.text(),
11228 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
11229 )
11230 });
11231 buffer_2.update(cx, |buffer, _| {
11232 assert!(buffer.is_dirty());
11233 assert_eq!(
11234 buffer.text(),
11235 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
11236 )
11237 });
11238 buffer_3.update(cx, |buffer, _| {
11239 assert!(!buffer.is_dirty());
11240 assert_eq!(buffer.text(), sample_text_3,)
11241 });
11242 cx.executor().run_until_parked();
11243
11244 cx.executor().start_waiting();
11245 let save = multi_buffer_editor
11246 .update_in(cx, |editor, window, cx| {
11247 editor.save(
11248 SaveOptions {
11249 format: true,
11250 autosave: false,
11251 },
11252 project.clone(),
11253 window,
11254 cx,
11255 )
11256 })
11257 .unwrap();
11258
11259 let fake_server = fake_servers.next().await.unwrap();
11260 fake_server
11261 .server
11262 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
11263 Ok(Some(vec![lsp::TextEdit::new(
11264 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11265 format!("[{} formatted]", params.text_document.uri),
11266 )]))
11267 })
11268 .detach();
11269 save.await;
11270
11271 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
11272 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
11273 assert_eq!(
11274 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11275 uri!(
11276 "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}"
11277 ),
11278 );
11279 buffer_1.update(cx, |buffer, _| {
11280 assert!(!buffer.is_dirty());
11281 assert_eq!(
11282 buffer.text(),
11283 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
11284 )
11285 });
11286 buffer_2.update(cx, |buffer, _| {
11287 assert!(!buffer.is_dirty());
11288 assert_eq!(
11289 buffer.text(),
11290 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
11291 )
11292 });
11293 buffer_3.update(cx, |buffer, _| {
11294 assert!(!buffer.is_dirty());
11295 assert_eq!(buffer.text(), sample_text_3,)
11296 });
11297}
11298
11299#[gpui::test]
11300async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
11301 init_test(cx, |_| {});
11302
11303 let fs = FakeFs::new(cx.executor());
11304 fs.insert_tree(
11305 path!("/dir"),
11306 json!({
11307 "file1.rs": "fn main() { println!(\"hello\"); }",
11308 "file2.rs": "fn test() { println!(\"test\"); }",
11309 "file3.rs": "fn other() { println!(\"other\"); }\n",
11310 }),
11311 )
11312 .await;
11313
11314 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
11315 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11316 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11317
11318 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11319 language_registry.add(rust_lang());
11320
11321 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
11322 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11323
11324 // Open three buffers
11325 let buffer_1 = project
11326 .update(cx, |project, cx| {
11327 project.open_buffer((worktree_id, "file1.rs"), cx)
11328 })
11329 .await
11330 .unwrap();
11331 let buffer_2 = project
11332 .update(cx, |project, cx| {
11333 project.open_buffer((worktree_id, "file2.rs"), cx)
11334 })
11335 .await
11336 .unwrap();
11337 let buffer_3 = project
11338 .update(cx, |project, cx| {
11339 project.open_buffer((worktree_id, "file3.rs"), cx)
11340 })
11341 .await
11342 .unwrap();
11343
11344 // Create a multi-buffer with all three buffers
11345 let multi_buffer = cx.new(|cx| {
11346 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11347 multi_buffer.push_excerpts(
11348 buffer_1.clone(),
11349 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11350 cx,
11351 );
11352 multi_buffer.push_excerpts(
11353 buffer_2.clone(),
11354 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11355 cx,
11356 );
11357 multi_buffer.push_excerpts(
11358 buffer_3.clone(),
11359 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11360 cx,
11361 );
11362 multi_buffer
11363 });
11364
11365 let editor = cx.new_window_entity(|window, cx| {
11366 Editor::new(
11367 EditorMode::full(),
11368 multi_buffer,
11369 Some(project.clone()),
11370 window,
11371 cx,
11372 )
11373 });
11374
11375 // Edit only the first buffer
11376 editor.update_in(cx, |editor, window, cx| {
11377 editor.change_selections(
11378 SelectionEffects::scroll(Autoscroll::Next),
11379 window,
11380 cx,
11381 |s| s.select_ranges(Some(10..10)),
11382 );
11383 editor.insert("// edited", window, cx);
11384 });
11385
11386 // Verify that only buffer 1 is dirty
11387 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
11388 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11389 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11390
11391 // Get write counts after file creation (files were created with initial content)
11392 // We expect each file to have been written once during creation
11393 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
11394 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
11395 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
11396
11397 // Perform autosave
11398 let save_task = editor.update_in(cx, |editor, window, cx| {
11399 editor.save(
11400 SaveOptions {
11401 format: true,
11402 autosave: true,
11403 },
11404 project.clone(),
11405 window,
11406 cx,
11407 )
11408 });
11409 save_task.await.unwrap();
11410
11411 // Only the dirty buffer should have been saved
11412 assert_eq!(
11413 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11414 1,
11415 "Buffer 1 was dirty, so it should have been written once during autosave"
11416 );
11417 assert_eq!(
11418 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11419 0,
11420 "Buffer 2 was clean, so it should not have been written during autosave"
11421 );
11422 assert_eq!(
11423 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11424 0,
11425 "Buffer 3 was clean, so it should not have been written during autosave"
11426 );
11427
11428 // Verify buffer states after autosave
11429 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11430 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11431 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11432
11433 // Now perform a manual save (format = true)
11434 let save_task = editor.update_in(cx, |editor, window, cx| {
11435 editor.save(
11436 SaveOptions {
11437 format: true,
11438 autosave: false,
11439 },
11440 project.clone(),
11441 window,
11442 cx,
11443 )
11444 });
11445 save_task.await.unwrap();
11446
11447 // During manual save, clean buffers don't get written to disk
11448 // They just get did_save called for language server notifications
11449 assert_eq!(
11450 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11451 1,
11452 "Buffer 1 should only have been written once total (during autosave, not manual save)"
11453 );
11454 assert_eq!(
11455 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11456 0,
11457 "Buffer 2 should not have been written at all"
11458 );
11459 assert_eq!(
11460 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11461 0,
11462 "Buffer 3 should not have been written at all"
11463 );
11464}
11465
11466async fn setup_range_format_test(
11467 cx: &mut TestAppContext,
11468) -> (
11469 Entity<Project>,
11470 Entity<Editor>,
11471 &mut gpui::VisualTestContext,
11472 lsp::FakeLanguageServer,
11473) {
11474 init_test(cx, |_| {});
11475
11476 let fs = FakeFs::new(cx.executor());
11477 fs.insert_file(path!("/file.rs"), Default::default()).await;
11478
11479 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11480
11481 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11482 language_registry.add(rust_lang());
11483 let mut fake_servers = language_registry.register_fake_lsp(
11484 "Rust",
11485 FakeLspAdapter {
11486 capabilities: lsp::ServerCapabilities {
11487 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
11488 ..lsp::ServerCapabilities::default()
11489 },
11490 ..FakeLspAdapter::default()
11491 },
11492 );
11493
11494 let buffer = project
11495 .update(cx, |project, cx| {
11496 project.open_local_buffer(path!("/file.rs"), cx)
11497 })
11498 .await
11499 .unwrap();
11500
11501 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11502 let (editor, cx) = cx.add_window_view(|window, cx| {
11503 build_editor_with_project(project.clone(), buffer, window, cx)
11504 });
11505
11506 cx.executor().start_waiting();
11507 let fake_server = fake_servers.next().await.unwrap();
11508
11509 (project, editor, cx, fake_server)
11510}
11511
11512#[gpui::test]
11513async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
11514 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11515
11516 editor.update_in(cx, |editor, window, cx| {
11517 editor.set_text("one\ntwo\nthree\n", window, cx)
11518 });
11519 assert!(cx.read(|cx| editor.is_dirty(cx)));
11520
11521 let save = editor
11522 .update_in(cx, |editor, window, cx| {
11523 editor.save(
11524 SaveOptions {
11525 format: true,
11526 autosave: false,
11527 },
11528 project.clone(),
11529 window,
11530 cx,
11531 )
11532 })
11533 .unwrap();
11534 fake_server
11535 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
11536 assert_eq!(
11537 params.text_document.uri,
11538 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11539 );
11540 assert_eq!(params.options.tab_size, 4);
11541 Ok(Some(vec![lsp::TextEdit::new(
11542 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11543 ", ".to_string(),
11544 )]))
11545 })
11546 .next()
11547 .await;
11548 cx.executor().start_waiting();
11549 save.await;
11550 assert_eq!(
11551 editor.update(cx, |editor, cx| editor.text(cx)),
11552 "one, two\nthree\n"
11553 );
11554 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11555}
11556
11557#[gpui::test]
11558async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
11559 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11560
11561 editor.update_in(cx, |editor, window, cx| {
11562 editor.set_text("one\ntwo\nthree\n", window, cx)
11563 });
11564 assert!(cx.read(|cx| editor.is_dirty(cx)));
11565
11566 // Test that save still works when formatting hangs
11567 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
11568 move |params, _| async move {
11569 assert_eq!(
11570 params.text_document.uri,
11571 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11572 );
11573 futures::future::pending::<()>().await;
11574 unreachable!()
11575 },
11576 );
11577 let save = editor
11578 .update_in(cx, |editor, window, cx| {
11579 editor.save(
11580 SaveOptions {
11581 format: true,
11582 autosave: false,
11583 },
11584 project.clone(),
11585 window,
11586 cx,
11587 )
11588 })
11589 .unwrap();
11590 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11591 cx.executor().start_waiting();
11592 save.await;
11593 assert_eq!(
11594 editor.update(cx, |editor, cx| editor.text(cx)),
11595 "one\ntwo\nthree\n"
11596 );
11597 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11598}
11599
11600#[gpui::test]
11601async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
11602 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11603
11604 // Buffer starts clean, no formatting should be requested
11605 let save = editor
11606 .update_in(cx, |editor, window, cx| {
11607 editor.save(
11608 SaveOptions {
11609 format: false,
11610 autosave: false,
11611 },
11612 project.clone(),
11613 window,
11614 cx,
11615 )
11616 })
11617 .unwrap();
11618 let _pending_format_request = fake_server
11619 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
11620 panic!("Should not be invoked");
11621 })
11622 .next();
11623 cx.executor().start_waiting();
11624 save.await;
11625 cx.run_until_parked();
11626}
11627
11628#[gpui::test]
11629async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
11630 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11631
11632 // Set Rust language override and assert overridden tabsize is sent to language server
11633 update_test_language_settings(cx, |settings| {
11634 settings.languages.0.insert(
11635 "Rust".into(),
11636 LanguageSettingsContent {
11637 tab_size: NonZeroU32::new(8),
11638 ..Default::default()
11639 },
11640 );
11641 });
11642
11643 editor.update_in(cx, |editor, window, cx| {
11644 editor.set_text("something_new\n", window, cx)
11645 });
11646 assert!(cx.read(|cx| editor.is_dirty(cx)));
11647 let save = editor
11648 .update_in(cx, |editor, window, cx| {
11649 editor.save(
11650 SaveOptions {
11651 format: true,
11652 autosave: false,
11653 },
11654 project.clone(),
11655 window,
11656 cx,
11657 )
11658 })
11659 .unwrap();
11660 fake_server
11661 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
11662 assert_eq!(
11663 params.text_document.uri,
11664 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11665 );
11666 assert_eq!(params.options.tab_size, 8);
11667 Ok(Some(Vec::new()))
11668 })
11669 .next()
11670 .await;
11671 save.await;
11672}
11673
11674#[gpui::test]
11675async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
11676 init_test(cx, |settings| {
11677 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
11678 Formatter::LanguageServer { name: None },
11679 )))
11680 });
11681
11682 let fs = FakeFs::new(cx.executor());
11683 fs.insert_file(path!("/file.rs"), Default::default()).await;
11684
11685 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11686
11687 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11688 language_registry.add(Arc::new(Language::new(
11689 LanguageConfig {
11690 name: "Rust".into(),
11691 matcher: LanguageMatcher {
11692 path_suffixes: vec!["rs".to_string()],
11693 ..Default::default()
11694 },
11695 ..LanguageConfig::default()
11696 },
11697 Some(tree_sitter_rust::LANGUAGE.into()),
11698 )));
11699 update_test_language_settings(cx, |settings| {
11700 // Enable Prettier formatting for the same buffer, and ensure
11701 // LSP is called instead of Prettier.
11702 settings.defaults.prettier = Some(PrettierSettings {
11703 allowed: true,
11704 ..PrettierSettings::default()
11705 });
11706 });
11707 let mut fake_servers = language_registry.register_fake_lsp(
11708 "Rust",
11709 FakeLspAdapter {
11710 capabilities: lsp::ServerCapabilities {
11711 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11712 ..Default::default()
11713 },
11714 ..Default::default()
11715 },
11716 );
11717
11718 let buffer = project
11719 .update(cx, |project, cx| {
11720 project.open_local_buffer(path!("/file.rs"), cx)
11721 })
11722 .await
11723 .unwrap();
11724
11725 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11726 let (editor, cx) = cx.add_window_view(|window, cx| {
11727 build_editor_with_project(project.clone(), buffer, window, cx)
11728 });
11729 editor.update_in(cx, |editor, window, cx| {
11730 editor.set_text("one\ntwo\nthree\n", window, cx)
11731 });
11732
11733 cx.executor().start_waiting();
11734 let fake_server = fake_servers.next().await.unwrap();
11735
11736 let format = editor
11737 .update_in(cx, |editor, window, cx| {
11738 editor.perform_format(
11739 project.clone(),
11740 FormatTrigger::Manual,
11741 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
11742 window,
11743 cx,
11744 )
11745 })
11746 .unwrap();
11747 fake_server
11748 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
11749 assert_eq!(
11750 params.text_document.uri,
11751 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11752 );
11753 assert_eq!(params.options.tab_size, 4);
11754 Ok(Some(vec![lsp::TextEdit::new(
11755 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11756 ", ".to_string(),
11757 )]))
11758 })
11759 .next()
11760 .await;
11761 cx.executor().start_waiting();
11762 format.await;
11763 assert_eq!(
11764 editor.update(cx, |editor, cx| editor.text(cx)),
11765 "one, two\nthree\n"
11766 );
11767
11768 editor.update_in(cx, |editor, window, cx| {
11769 editor.set_text("one\ntwo\nthree\n", window, cx)
11770 });
11771 // Ensure we don't lock if formatting hangs.
11772 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11773 move |params, _| async move {
11774 assert_eq!(
11775 params.text_document.uri,
11776 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11777 );
11778 futures::future::pending::<()>().await;
11779 unreachable!()
11780 },
11781 );
11782 let format = editor
11783 .update_in(cx, |editor, window, cx| {
11784 editor.perform_format(
11785 project,
11786 FormatTrigger::Manual,
11787 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
11788 window,
11789 cx,
11790 )
11791 })
11792 .unwrap();
11793 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11794 cx.executor().start_waiting();
11795 format.await;
11796 assert_eq!(
11797 editor.update(cx, |editor, cx| editor.text(cx)),
11798 "one\ntwo\nthree\n"
11799 );
11800}
11801
11802#[gpui::test]
11803async fn test_multiple_formatters(cx: &mut TestAppContext) {
11804 init_test(cx, |settings| {
11805 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
11806 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
11807 Formatter::LanguageServer { name: None },
11808 Formatter::CodeActions(
11809 [
11810 ("code-action-1".into(), true),
11811 ("code-action-2".into(), true),
11812 ]
11813 .into_iter()
11814 .collect(),
11815 ),
11816 ])))
11817 });
11818
11819 let fs = FakeFs::new(cx.executor());
11820 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
11821 .await;
11822
11823 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11824 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11825 language_registry.add(rust_lang());
11826
11827 let mut fake_servers = language_registry.register_fake_lsp(
11828 "Rust",
11829 FakeLspAdapter {
11830 capabilities: lsp::ServerCapabilities {
11831 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11832 execute_command_provider: Some(lsp::ExecuteCommandOptions {
11833 commands: vec!["the-command-for-code-action-1".into()],
11834 ..Default::default()
11835 }),
11836 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
11837 ..Default::default()
11838 },
11839 ..Default::default()
11840 },
11841 );
11842
11843 let buffer = project
11844 .update(cx, |project, cx| {
11845 project.open_local_buffer(path!("/file.rs"), cx)
11846 })
11847 .await
11848 .unwrap();
11849
11850 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11851 let (editor, cx) = cx.add_window_view(|window, cx| {
11852 build_editor_with_project(project.clone(), buffer, window, cx)
11853 });
11854
11855 cx.executor().start_waiting();
11856
11857 let fake_server = fake_servers.next().await.unwrap();
11858 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11859 move |_params, _| async move {
11860 Ok(Some(vec![lsp::TextEdit::new(
11861 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
11862 "applied-formatting\n".to_string(),
11863 )]))
11864 },
11865 );
11866 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
11867 move |params, _| async move {
11868 assert_eq!(
11869 params.context.only,
11870 Some(vec!["code-action-1".into(), "code-action-2".into()])
11871 );
11872 let uri = lsp::Uri::from_file_path(path!("/file.rs")).unwrap();
11873 Ok(Some(vec![
11874 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
11875 kind: Some("code-action-1".into()),
11876 edit: Some(lsp::WorkspaceEdit::new(
11877 [(
11878 uri.clone(),
11879 vec![lsp::TextEdit::new(
11880 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
11881 "applied-code-action-1-edit\n".to_string(),
11882 )],
11883 )]
11884 .into_iter()
11885 .collect(),
11886 )),
11887 command: Some(lsp::Command {
11888 command: "the-command-for-code-action-1".into(),
11889 ..Default::default()
11890 }),
11891 ..Default::default()
11892 }),
11893 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
11894 kind: Some("code-action-2".into()),
11895 edit: Some(lsp::WorkspaceEdit::new(
11896 [(
11897 uri,
11898 vec![lsp::TextEdit::new(
11899 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
11900 "applied-code-action-2-edit\n".to_string(),
11901 )],
11902 )]
11903 .into_iter()
11904 .collect(),
11905 )),
11906 ..Default::default()
11907 }),
11908 ]))
11909 },
11910 );
11911
11912 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
11913 move |params, _| async move { Ok(params) }
11914 });
11915
11916 let command_lock = Arc::new(futures::lock::Mutex::new(()));
11917 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
11918 let fake = fake_server.clone();
11919 let lock = command_lock.clone();
11920 move |params, _| {
11921 assert_eq!(params.command, "the-command-for-code-action-1");
11922 let fake = fake.clone();
11923 let lock = lock.clone();
11924 async move {
11925 lock.lock().await;
11926 fake.server
11927 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
11928 label: None,
11929 edit: lsp::WorkspaceEdit {
11930 changes: Some(
11931 [(
11932 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
11933 vec![lsp::TextEdit {
11934 range: lsp::Range::new(
11935 lsp::Position::new(0, 0),
11936 lsp::Position::new(0, 0),
11937 ),
11938 new_text: "applied-code-action-1-command\n".into(),
11939 }],
11940 )]
11941 .into_iter()
11942 .collect(),
11943 ),
11944 ..Default::default()
11945 },
11946 })
11947 .await
11948 .into_response()
11949 .unwrap();
11950 Ok(Some(json!(null)))
11951 }
11952 }
11953 });
11954
11955 cx.executor().start_waiting();
11956 editor
11957 .update_in(cx, |editor, window, cx| {
11958 editor.perform_format(
11959 project.clone(),
11960 FormatTrigger::Manual,
11961 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
11962 window,
11963 cx,
11964 )
11965 })
11966 .unwrap()
11967 .await;
11968 editor.update(cx, |editor, cx| {
11969 assert_eq!(
11970 editor.text(cx),
11971 r#"
11972 applied-code-action-2-edit
11973 applied-code-action-1-command
11974 applied-code-action-1-edit
11975 applied-formatting
11976 one
11977 two
11978 three
11979 "#
11980 .unindent()
11981 );
11982 });
11983
11984 editor.update_in(cx, |editor, window, cx| {
11985 editor.undo(&Default::default(), window, cx);
11986 assert_eq!(editor.text(cx), "one \ntwo \nthree");
11987 });
11988
11989 // Perform a manual edit while waiting for an LSP command
11990 // that's being run as part of a formatting code action.
11991 let lock_guard = command_lock.lock().await;
11992 let format = editor
11993 .update_in(cx, |editor, window, cx| {
11994 editor.perform_format(
11995 project.clone(),
11996 FormatTrigger::Manual,
11997 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
11998 window,
11999 cx,
12000 )
12001 })
12002 .unwrap();
12003 cx.run_until_parked();
12004 editor.update(cx, |editor, cx| {
12005 assert_eq!(
12006 editor.text(cx),
12007 r#"
12008 applied-code-action-1-edit
12009 applied-formatting
12010 one
12011 two
12012 three
12013 "#
12014 .unindent()
12015 );
12016
12017 editor.buffer.update(cx, |buffer, cx| {
12018 let ix = buffer.len(cx);
12019 buffer.edit([(ix..ix, "edited\n")], None, cx);
12020 });
12021 });
12022
12023 // Allow the LSP command to proceed. Because the buffer was edited,
12024 // the second code action will not be run.
12025 drop(lock_guard);
12026 format.await;
12027 editor.update_in(cx, |editor, window, cx| {
12028 assert_eq!(
12029 editor.text(cx),
12030 r#"
12031 applied-code-action-1-command
12032 applied-code-action-1-edit
12033 applied-formatting
12034 one
12035 two
12036 three
12037 edited
12038 "#
12039 .unindent()
12040 );
12041
12042 // The manual edit is undone first, because it is the last thing the user did
12043 // (even though the command completed afterwards).
12044 editor.undo(&Default::default(), window, cx);
12045 assert_eq!(
12046 editor.text(cx),
12047 r#"
12048 applied-code-action-1-command
12049 applied-code-action-1-edit
12050 applied-formatting
12051 one
12052 two
12053 three
12054 "#
12055 .unindent()
12056 );
12057
12058 // All the formatting (including the command, which completed after the manual edit)
12059 // is undone together.
12060 editor.undo(&Default::default(), window, cx);
12061 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12062 });
12063}
12064
12065#[gpui::test]
12066async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
12067 init_test(cx, |settings| {
12068 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
12069 Formatter::LanguageServer { name: None },
12070 ])))
12071 });
12072
12073 let fs = FakeFs::new(cx.executor());
12074 fs.insert_file(path!("/file.ts"), Default::default()).await;
12075
12076 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12077
12078 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12079 language_registry.add(Arc::new(Language::new(
12080 LanguageConfig {
12081 name: "TypeScript".into(),
12082 matcher: LanguageMatcher {
12083 path_suffixes: vec!["ts".to_string()],
12084 ..Default::default()
12085 },
12086 ..LanguageConfig::default()
12087 },
12088 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12089 )));
12090 update_test_language_settings(cx, |settings| {
12091 settings.defaults.prettier = Some(PrettierSettings {
12092 allowed: true,
12093 ..PrettierSettings::default()
12094 });
12095 });
12096 let mut fake_servers = language_registry.register_fake_lsp(
12097 "TypeScript",
12098 FakeLspAdapter {
12099 capabilities: lsp::ServerCapabilities {
12100 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12101 ..Default::default()
12102 },
12103 ..Default::default()
12104 },
12105 );
12106
12107 let buffer = project
12108 .update(cx, |project, cx| {
12109 project.open_local_buffer(path!("/file.ts"), cx)
12110 })
12111 .await
12112 .unwrap();
12113
12114 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12115 let (editor, cx) = cx.add_window_view(|window, cx| {
12116 build_editor_with_project(project.clone(), buffer, window, cx)
12117 });
12118 editor.update_in(cx, |editor, window, cx| {
12119 editor.set_text(
12120 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12121 window,
12122 cx,
12123 )
12124 });
12125
12126 cx.executor().start_waiting();
12127 let fake_server = fake_servers.next().await.unwrap();
12128
12129 let format = editor
12130 .update_in(cx, |editor, window, cx| {
12131 editor.perform_code_action_kind(
12132 project.clone(),
12133 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12134 window,
12135 cx,
12136 )
12137 })
12138 .unwrap();
12139 fake_server
12140 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
12141 assert_eq!(
12142 params.text_document.uri,
12143 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12144 );
12145 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12146 lsp::CodeAction {
12147 title: "Organize Imports".to_string(),
12148 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
12149 edit: Some(lsp::WorkspaceEdit {
12150 changes: Some(
12151 [(
12152 params.text_document.uri.clone(),
12153 vec![lsp::TextEdit::new(
12154 lsp::Range::new(
12155 lsp::Position::new(1, 0),
12156 lsp::Position::new(2, 0),
12157 ),
12158 "".to_string(),
12159 )],
12160 )]
12161 .into_iter()
12162 .collect(),
12163 ),
12164 ..Default::default()
12165 }),
12166 ..Default::default()
12167 },
12168 )]))
12169 })
12170 .next()
12171 .await;
12172 cx.executor().start_waiting();
12173 format.await;
12174 assert_eq!(
12175 editor.update(cx, |editor, cx| editor.text(cx)),
12176 "import { a } from 'module';\n\nconst x = a;\n"
12177 );
12178
12179 editor.update_in(cx, |editor, window, cx| {
12180 editor.set_text(
12181 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12182 window,
12183 cx,
12184 )
12185 });
12186 // Ensure we don't lock if code action hangs.
12187 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12188 move |params, _| async move {
12189 assert_eq!(
12190 params.text_document.uri,
12191 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12192 );
12193 futures::future::pending::<()>().await;
12194 unreachable!()
12195 },
12196 );
12197 let format = editor
12198 .update_in(cx, |editor, window, cx| {
12199 editor.perform_code_action_kind(
12200 project,
12201 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12202 window,
12203 cx,
12204 )
12205 })
12206 .unwrap();
12207 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
12208 cx.executor().start_waiting();
12209 format.await;
12210 assert_eq!(
12211 editor.update(cx, |editor, cx| editor.text(cx)),
12212 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
12213 );
12214}
12215
12216#[gpui::test]
12217async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
12218 init_test(cx, |_| {});
12219
12220 let mut cx = EditorLspTestContext::new_rust(
12221 lsp::ServerCapabilities {
12222 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12223 ..Default::default()
12224 },
12225 cx,
12226 )
12227 .await;
12228
12229 cx.set_state(indoc! {"
12230 one.twoˇ
12231 "});
12232
12233 // The format request takes a long time. When it completes, it inserts
12234 // a newline and an indent before the `.`
12235 cx.lsp
12236 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
12237 let executor = cx.background_executor().clone();
12238 async move {
12239 executor.timer(Duration::from_millis(100)).await;
12240 Ok(Some(vec![lsp::TextEdit {
12241 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
12242 new_text: "\n ".into(),
12243 }]))
12244 }
12245 });
12246
12247 // Submit a format request.
12248 let format_1 = cx
12249 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12250 .unwrap();
12251 cx.executor().run_until_parked();
12252
12253 // Submit a second format request.
12254 let format_2 = cx
12255 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12256 .unwrap();
12257 cx.executor().run_until_parked();
12258
12259 // Wait for both format requests to complete
12260 cx.executor().advance_clock(Duration::from_millis(200));
12261 cx.executor().start_waiting();
12262 format_1.await.unwrap();
12263 cx.executor().start_waiting();
12264 format_2.await.unwrap();
12265
12266 // The formatting edits only happens once.
12267 cx.assert_editor_state(indoc! {"
12268 one
12269 .twoˇ
12270 "});
12271}
12272
12273#[gpui::test]
12274async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
12275 init_test(cx, |settings| {
12276 settings.defaults.formatter = Some(SelectedFormatter::Auto)
12277 });
12278
12279 let mut cx = EditorLspTestContext::new_rust(
12280 lsp::ServerCapabilities {
12281 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12282 ..Default::default()
12283 },
12284 cx,
12285 )
12286 .await;
12287
12288 // Set up a buffer white some trailing whitespace and no trailing newline.
12289 cx.set_state(
12290 &[
12291 "one ", //
12292 "twoˇ", //
12293 "three ", //
12294 "four", //
12295 ]
12296 .join("\n"),
12297 );
12298
12299 // Submit a format request.
12300 let format = cx
12301 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12302 .unwrap();
12303
12304 // Record which buffer changes have been sent to the language server
12305 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
12306 cx.lsp
12307 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
12308 let buffer_changes = buffer_changes.clone();
12309 move |params, _| {
12310 buffer_changes.lock().extend(
12311 params
12312 .content_changes
12313 .into_iter()
12314 .map(|e| (e.range.unwrap(), e.text)),
12315 );
12316 }
12317 });
12318
12319 // Handle formatting requests to the language server.
12320 cx.lsp
12321 .set_request_handler::<lsp::request::Formatting, _, _>({
12322 let buffer_changes = buffer_changes.clone();
12323 move |_, _| {
12324 // When formatting is requested, trailing whitespace has already been stripped,
12325 // and the trailing newline has already been added.
12326 assert_eq!(
12327 &buffer_changes.lock()[1..],
12328 &[
12329 (
12330 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
12331 "".into()
12332 ),
12333 (
12334 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
12335 "".into()
12336 ),
12337 (
12338 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
12339 "\n".into()
12340 ),
12341 ]
12342 );
12343
12344 // Insert blank lines between each line of the buffer.
12345 async move {
12346 Ok(Some(vec![
12347 lsp::TextEdit {
12348 range: lsp::Range::new(
12349 lsp::Position::new(1, 0),
12350 lsp::Position::new(1, 0),
12351 ),
12352 new_text: "\n".into(),
12353 },
12354 lsp::TextEdit {
12355 range: lsp::Range::new(
12356 lsp::Position::new(2, 0),
12357 lsp::Position::new(2, 0),
12358 ),
12359 new_text: "\n".into(),
12360 },
12361 ]))
12362 }
12363 }
12364 });
12365
12366 // After formatting the buffer, the trailing whitespace is stripped,
12367 // a newline is appended, and the edits provided by the language server
12368 // have been applied.
12369 format.await.unwrap();
12370 cx.assert_editor_state(
12371 &[
12372 "one", //
12373 "", //
12374 "twoˇ", //
12375 "", //
12376 "three", //
12377 "four", //
12378 "", //
12379 ]
12380 .join("\n"),
12381 );
12382
12383 // Undoing the formatting undoes the trailing whitespace removal, the
12384 // trailing newline, and the LSP edits.
12385 cx.update_buffer(|buffer, cx| buffer.undo(cx));
12386 cx.assert_editor_state(
12387 &[
12388 "one ", //
12389 "twoˇ", //
12390 "three ", //
12391 "four", //
12392 ]
12393 .join("\n"),
12394 );
12395}
12396
12397#[gpui::test]
12398async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
12399 cx: &mut TestAppContext,
12400) {
12401 init_test(cx, |_| {});
12402
12403 cx.update(|cx| {
12404 cx.update_global::<SettingsStore, _>(|settings, cx| {
12405 settings.update_user_settings::<EditorSettings>(cx, |settings| {
12406 settings.auto_signature_help = Some(true);
12407 });
12408 });
12409 });
12410
12411 let mut cx = EditorLspTestContext::new_rust(
12412 lsp::ServerCapabilities {
12413 signature_help_provider: Some(lsp::SignatureHelpOptions {
12414 ..Default::default()
12415 }),
12416 ..Default::default()
12417 },
12418 cx,
12419 )
12420 .await;
12421
12422 let language = Language::new(
12423 LanguageConfig {
12424 name: "Rust".into(),
12425 brackets: BracketPairConfig {
12426 pairs: vec![
12427 BracketPair {
12428 start: "{".to_string(),
12429 end: "}".to_string(),
12430 close: true,
12431 surround: true,
12432 newline: true,
12433 },
12434 BracketPair {
12435 start: "(".to_string(),
12436 end: ")".to_string(),
12437 close: true,
12438 surround: true,
12439 newline: true,
12440 },
12441 BracketPair {
12442 start: "/*".to_string(),
12443 end: " */".to_string(),
12444 close: true,
12445 surround: true,
12446 newline: true,
12447 },
12448 BracketPair {
12449 start: "[".to_string(),
12450 end: "]".to_string(),
12451 close: false,
12452 surround: false,
12453 newline: true,
12454 },
12455 BracketPair {
12456 start: "\"".to_string(),
12457 end: "\"".to_string(),
12458 close: true,
12459 surround: true,
12460 newline: false,
12461 },
12462 BracketPair {
12463 start: "<".to_string(),
12464 end: ">".to_string(),
12465 close: false,
12466 surround: true,
12467 newline: true,
12468 },
12469 ],
12470 ..Default::default()
12471 },
12472 autoclose_before: "})]".to_string(),
12473 ..Default::default()
12474 },
12475 Some(tree_sitter_rust::LANGUAGE.into()),
12476 );
12477 let language = Arc::new(language);
12478
12479 cx.language_registry().add(language.clone());
12480 cx.update_buffer(|buffer, cx| {
12481 buffer.set_language(Some(language), cx);
12482 });
12483
12484 cx.set_state(
12485 &r#"
12486 fn main() {
12487 sampleˇ
12488 }
12489 "#
12490 .unindent(),
12491 );
12492
12493 cx.update_editor(|editor, window, cx| {
12494 editor.handle_input("(", window, cx);
12495 });
12496 cx.assert_editor_state(
12497 &"
12498 fn main() {
12499 sample(ˇ)
12500 }
12501 "
12502 .unindent(),
12503 );
12504
12505 let mocked_response = lsp::SignatureHelp {
12506 signatures: vec![lsp::SignatureInformation {
12507 label: "fn sample(param1: u8, param2: u8)".to_string(),
12508 documentation: None,
12509 parameters: Some(vec![
12510 lsp::ParameterInformation {
12511 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12512 documentation: None,
12513 },
12514 lsp::ParameterInformation {
12515 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12516 documentation: None,
12517 },
12518 ]),
12519 active_parameter: None,
12520 }],
12521 active_signature: Some(0),
12522 active_parameter: Some(0),
12523 };
12524 handle_signature_help_request(&mut cx, mocked_response).await;
12525
12526 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12527 .await;
12528
12529 cx.editor(|editor, _, _| {
12530 let signature_help_state = editor.signature_help_state.popover().cloned();
12531 let signature = signature_help_state.unwrap();
12532 assert_eq!(
12533 signature.signatures[signature.current_signature].label,
12534 "fn sample(param1: u8, param2: u8)"
12535 );
12536 });
12537}
12538
12539#[gpui::test]
12540async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
12541 init_test(cx, |_| {});
12542
12543 cx.update(|cx| {
12544 cx.update_global::<SettingsStore, _>(|settings, cx| {
12545 settings.update_user_settings::<EditorSettings>(cx, |settings| {
12546 settings.auto_signature_help = Some(false);
12547 settings.show_signature_help_after_edits = Some(false);
12548 });
12549 });
12550 });
12551
12552 let mut cx = EditorLspTestContext::new_rust(
12553 lsp::ServerCapabilities {
12554 signature_help_provider: Some(lsp::SignatureHelpOptions {
12555 ..Default::default()
12556 }),
12557 ..Default::default()
12558 },
12559 cx,
12560 )
12561 .await;
12562
12563 let language = Language::new(
12564 LanguageConfig {
12565 name: "Rust".into(),
12566 brackets: BracketPairConfig {
12567 pairs: vec![
12568 BracketPair {
12569 start: "{".to_string(),
12570 end: "}".to_string(),
12571 close: true,
12572 surround: true,
12573 newline: true,
12574 },
12575 BracketPair {
12576 start: "(".to_string(),
12577 end: ")".to_string(),
12578 close: true,
12579 surround: true,
12580 newline: true,
12581 },
12582 BracketPair {
12583 start: "/*".to_string(),
12584 end: " */".to_string(),
12585 close: true,
12586 surround: true,
12587 newline: true,
12588 },
12589 BracketPair {
12590 start: "[".to_string(),
12591 end: "]".to_string(),
12592 close: false,
12593 surround: false,
12594 newline: true,
12595 },
12596 BracketPair {
12597 start: "\"".to_string(),
12598 end: "\"".to_string(),
12599 close: true,
12600 surround: true,
12601 newline: false,
12602 },
12603 BracketPair {
12604 start: "<".to_string(),
12605 end: ">".to_string(),
12606 close: false,
12607 surround: true,
12608 newline: true,
12609 },
12610 ],
12611 ..Default::default()
12612 },
12613 autoclose_before: "})]".to_string(),
12614 ..Default::default()
12615 },
12616 Some(tree_sitter_rust::LANGUAGE.into()),
12617 );
12618 let language = Arc::new(language);
12619
12620 cx.language_registry().add(language.clone());
12621 cx.update_buffer(|buffer, cx| {
12622 buffer.set_language(Some(language), cx);
12623 });
12624
12625 // Ensure that signature_help is not called when no signature help is enabled.
12626 cx.set_state(
12627 &r#"
12628 fn main() {
12629 sampleˇ
12630 }
12631 "#
12632 .unindent(),
12633 );
12634 cx.update_editor(|editor, window, cx| {
12635 editor.handle_input("(", window, cx);
12636 });
12637 cx.assert_editor_state(
12638 &"
12639 fn main() {
12640 sample(ˇ)
12641 }
12642 "
12643 .unindent(),
12644 );
12645 cx.editor(|editor, _, _| {
12646 assert!(editor.signature_help_state.task().is_none());
12647 });
12648
12649 let mocked_response = lsp::SignatureHelp {
12650 signatures: vec![lsp::SignatureInformation {
12651 label: "fn sample(param1: u8, param2: u8)".to_string(),
12652 documentation: None,
12653 parameters: Some(vec![
12654 lsp::ParameterInformation {
12655 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12656 documentation: None,
12657 },
12658 lsp::ParameterInformation {
12659 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12660 documentation: None,
12661 },
12662 ]),
12663 active_parameter: None,
12664 }],
12665 active_signature: Some(0),
12666 active_parameter: Some(0),
12667 };
12668
12669 // Ensure that signature_help is called when enabled afte edits
12670 cx.update(|_, cx| {
12671 cx.update_global::<SettingsStore, _>(|settings, cx| {
12672 settings.update_user_settings::<EditorSettings>(cx, |settings| {
12673 settings.auto_signature_help = Some(false);
12674 settings.show_signature_help_after_edits = Some(true);
12675 });
12676 });
12677 });
12678 cx.set_state(
12679 &r#"
12680 fn main() {
12681 sampleˇ
12682 }
12683 "#
12684 .unindent(),
12685 );
12686 cx.update_editor(|editor, window, cx| {
12687 editor.handle_input("(", window, cx);
12688 });
12689 cx.assert_editor_state(
12690 &"
12691 fn main() {
12692 sample(ˇ)
12693 }
12694 "
12695 .unindent(),
12696 );
12697 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12698 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12699 .await;
12700 cx.update_editor(|editor, _, _| {
12701 let signature_help_state = editor.signature_help_state.popover().cloned();
12702 assert!(signature_help_state.is_some());
12703 let signature = signature_help_state.unwrap();
12704 assert_eq!(
12705 signature.signatures[signature.current_signature].label,
12706 "fn sample(param1: u8, param2: u8)"
12707 );
12708 editor.signature_help_state = SignatureHelpState::default();
12709 });
12710
12711 // Ensure that signature_help is called when auto signature help override is enabled
12712 cx.update(|_, cx| {
12713 cx.update_global::<SettingsStore, _>(|settings, cx| {
12714 settings.update_user_settings::<EditorSettings>(cx, |settings| {
12715 settings.auto_signature_help = Some(true);
12716 settings.show_signature_help_after_edits = Some(false);
12717 });
12718 });
12719 });
12720 cx.set_state(
12721 &r#"
12722 fn main() {
12723 sampleˇ
12724 }
12725 "#
12726 .unindent(),
12727 );
12728 cx.update_editor(|editor, window, cx| {
12729 editor.handle_input("(", window, cx);
12730 });
12731 cx.assert_editor_state(
12732 &"
12733 fn main() {
12734 sample(ˇ)
12735 }
12736 "
12737 .unindent(),
12738 );
12739 handle_signature_help_request(&mut cx, mocked_response).await;
12740 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12741 .await;
12742 cx.editor(|editor, _, _| {
12743 let signature_help_state = editor.signature_help_state.popover().cloned();
12744 assert!(signature_help_state.is_some());
12745 let signature = signature_help_state.unwrap();
12746 assert_eq!(
12747 signature.signatures[signature.current_signature].label,
12748 "fn sample(param1: u8, param2: u8)"
12749 );
12750 });
12751}
12752
12753#[gpui::test]
12754async fn test_signature_help(cx: &mut TestAppContext) {
12755 init_test(cx, |_| {});
12756 cx.update(|cx| {
12757 cx.update_global::<SettingsStore, _>(|settings, cx| {
12758 settings.update_user_settings::<EditorSettings>(cx, |settings| {
12759 settings.auto_signature_help = Some(true);
12760 });
12761 });
12762 });
12763
12764 let mut cx = EditorLspTestContext::new_rust(
12765 lsp::ServerCapabilities {
12766 signature_help_provider: Some(lsp::SignatureHelpOptions {
12767 ..Default::default()
12768 }),
12769 ..Default::default()
12770 },
12771 cx,
12772 )
12773 .await;
12774
12775 // A test that directly calls `show_signature_help`
12776 cx.update_editor(|editor, window, cx| {
12777 editor.show_signature_help(&ShowSignatureHelp, window, cx);
12778 });
12779
12780 let mocked_response = lsp::SignatureHelp {
12781 signatures: vec![lsp::SignatureInformation {
12782 label: "fn sample(param1: u8, param2: u8)".to_string(),
12783 documentation: None,
12784 parameters: Some(vec![
12785 lsp::ParameterInformation {
12786 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12787 documentation: None,
12788 },
12789 lsp::ParameterInformation {
12790 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12791 documentation: None,
12792 },
12793 ]),
12794 active_parameter: None,
12795 }],
12796 active_signature: Some(0),
12797 active_parameter: Some(0),
12798 };
12799 handle_signature_help_request(&mut cx, mocked_response).await;
12800
12801 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12802 .await;
12803
12804 cx.editor(|editor, _, _| {
12805 let signature_help_state = editor.signature_help_state.popover().cloned();
12806 assert!(signature_help_state.is_some());
12807 let signature = signature_help_state.unwrap();
12808 assert_eq!(
12809 signature.signatures[signature.current_signature].label,
12810 "fn sample(param1: u8, param2: u8)"
12811 );
12812 });
12813
12814 // When exiting outside from inside the brackets, `signature_help` is closed.
12815 cx.set_state(indoc! {"
12816 fn main() {
12817 sample(ˇ);
12818 }
12819
12820 fn sample(param1: u8, param2: u8) {}
12821 "});
12822
12823 cx.update_editor(|editor, window, cx| {
12824 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12825 s.select_ranges([0..0])
12826 });
12827 });
12828
12829 let mocked_response = lsp::SignatureHelp {
12830 signatures: Vec::new(),
12831 active_signature: None,
12832 active_parameter: None,
12833 };
12834 handle_signature_help_request(&mut cx, mocked_response).await;
12835
12836 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
12837 .await;
12838
12839 cx.editor(|editor, _, _| {
12840 assert!(!editor.signature_help_state.is_shown());
12841 });
12842
12843 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
12844 cx.set_state(indoc! {"
12845 fn main() {
12846 sample(ˇ);
12847 }
12848
12849 fn sample(param1: u8, param2: u8) {}
12850 "});
12851
12852 let mocked_response = lsp::SignatureHelp {
12853 signatures: vec![lsp::SignatureInformation {
12854 label: "fn sample(param1: u8, param2: u8)".to_string(),
12855 documentation: None,
12856 parameters: Some(vec![
12857 lsp::ParameterInformation {
12858 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12859 documentation: None,
12860 },
12861 lsp::ParameterInformation {
12862 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12863 documentation: None,
12864 },
12865 ]),
12866 active_parameter: None,
12867 }],
12868 active_signature: Some(0),
12869 active_parameter: Some(0),
12870 };
12871 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12872 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12873 .await;
12874 cx.editor(|editor, _, _| {
12875 assert!(editor.signature_help_state.is_shown());
12876 });
12877
12878 // Restore the popover with more parameter input
12879 cx.set_state(indoc! {"
12880 fn main() {
12881 sample(param1, param2ˇ);
12882 }
12883
12884 fn sample(param1: u8, param2: u8) {}
12885 "});
12886
12887 let mocked_response = lsp::SignatureHelp {
12888 signatures: vec![lsp::SignatureInformation {
12889 label: "fn sample(param1: u8, param2: u8)".to_string(),
12890 documentation: None,
12891 parameters: Some(vec![
12892 lsp::ParameterInformation {
12893 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12894 documentation: None,
12895 },
12896 lsp::ParameterInformation {
12897 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12898 documentation: None,
12899 },
12900 ]),
12901 active_parameter: None,
12902 }],
12903 active_signature: Some(0),
12904 active_parameter: Some(1),
12905 };
12906 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12907 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12908 .await;
12909
12910 // When selecting a range, the popover is gone.
12911 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
12912 cx.update_editor(|editor, window, cx| {
12913 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12914 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
12915 })
12916 });
12917 cx.assert_editor_state(indoc! {"
12918 fn main() {
12919 sample(param1, «ˇparam2»);
12920 }
12921
12922 fn sample(param1: u8, param2: u8) {}
12923 "});
12924 cx.editor(|editor, _, _| {
12925 assert!(!editor.signature_help_state.is_shown());
12926 });
12927
12928 // When unselecting again, the popover is back if within the brackets.
12929 cx.update_editor(|editor, window, cx| {
12930 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12931 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
12932 })
12933 });
12934 cx.assert_editor_state(indoc! {"
12935 fn main() {
12936 sample(param1, ˇparam2);
12937 }
12938
12939 fn sample(param1: u8, param2: u8) {}
12940 "});
12941 handle_signature_help_request(&mut cx, mocked_response).await;
12942 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12943 .await;
12944 cx.editor(|editor, _, _| {
12945 assert!(editor.signature_help_state.is_shown());
12946 });
12947
12948 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
12949 cx.update_editor(|editor, window, cx| {
12950 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12951 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
12952 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
12953 })
12954 });
12955 cx.assert_editor_state(indoc! {"
12956 fn main() {
12957 sample(param1, ˇparam2);
12958 }
12959
12960 fn sample(param1: u8, param2: u8) {}
12961 "});
12962
12963 let mocked_response = lsp::SignatureHelp {
12964 signatures: vec![lsp::SignatureInformation {
12965 label: "fn sample(param1: u8, param2: u8)".to_string(),
12966 documentation: None,
12967 parameters: Some(vec![
12968 lsp::ParameterInformation {
12969 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12970 documentation: None,
12971 },
12972 lsp::ParameterInformation {
12973 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12974 documentation: None,
12975 },
12976 ]),
12977 active_parameter: None,
12978 }],
12979 active_signature: Some(0),
12980 active_parameter: Some(1),
12981 };
12982 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12983 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12984 .await;
12985 cx.update_editor(|editor, _, cx| {
12986 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
12987 });
12988 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
12989 .await;
12990 cx.update_editor(|editor, window, cx| {
12991 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12992 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
12993 })
12994 });
12995 cx.assert_editor_state(indoc! {"
12996 fn main() {
12997 sample(param1, «ˇparam2»);
12998 }
12999
13000 fn sample(param1: u8, param2: u8) {}
13001 "});
13002 cx.update_editor(|editor, window, cx| {
13003 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13004 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13005 })
13006 });
13007 cx.assert_editor_state(indoc! {"
13008 fn main() {
13009 sample(param1, ˇparam2);
13010 }
13011
13012 fn sample(param1: u8, param2: u8) {}
13013 "});
13014 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
13015 .await;
13016}
13017
13018#[gpui::test]
13019async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
13020 init_test(cx, |_| {});
13021
13022 let mut cx = EditorLspTestContext::new_rust(
13023 lsp::ServerCapabilities {
13024 signature_help_provider: Some(lsp::SignatureHelpOptions {
13025 ..Default::default()
13026 }),
13027 ..Default::default()
13028 },
13029 cx,
13030 )
13031 .await;
13032
13033 cx.set_state(indoc! {"
13034 fn main() {
13035 overloadedˇ
13036 }
13037 "});
13038
13039 cx.update_editor(|editor, window, cx| {
13040 editor.handle_input("(", window, cx);
13041 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13042 });
13043
13044 // Mock response with 3 signatures
13045 let mocked_response = lsp::SignatureHelp {
13046 signatures: vec![
13047 lsp::SignatureInformation {
13048 label: "fn overloaded(x: i32)".to_string(),
13049 documentation: None,
13050 parameters: Some(vec![lsp::ParameterInformation {
13051 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13052 documentation: None,
13053 }]),
13054 active_parameter: None,
13055 },
13056 lsp::SignatureInformation {
13057 label: "fn overloaded(x: i32, y: i32)".to_string(),
13058 documentation: None,
13059 parameters: Some(vec![
13060 lsp::ParameterInformation {
13061 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13062 documentation: None,
13063 },
13064 lsp::ParameterInformation {
13065 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13066 documentation: None,
13067 },
13068 ]),
13069 active_parameter: None,
13070 },
13071 lsp::SignatureInformation {
13072 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
13073 documentation: None,
13074 parameters: Some(vec![
13075 lsp::ParameterInformation {
13076 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13077 documentation: None,
13078 },
13079 lsp::ParameterInformation {
13080 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13081 documentation: None,
13082 },
13083 lsp::ParameterInformation {
13084 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
13085 documentation: None,
13086 },
13087 ]),
13088 active_parameter: None,
13089 },
13090 ],
13091 active_signature: Some(1),
13092 active_parameter: Some(0),
13093 };
13094 handle_signature_help_request(&mut cx, mocked_response).await;
13095
13096 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13097 .await;
13098
13099 // Verify we have multiple signatures and the right one is selected
13100 cx.editor(|editor, _, _| {
13101 let popover = editor.signature_help_state.popover().cloned().unwrap();
13102 assert_eq!(popover.signatures.len(), 3);
13103 // active_signature was 1, so that should be the current
13104 assert_eq!(popover.current_signature, 1);
13105 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
13106 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
13107 assert_eq!(
13108 popover.signatures[2].label,
13109 "fn overloaded(x: i32, y: i32, z: i32)"
13110 );
13111 });
13112
13113 // Test navigation functionality
13114 cx.update_editor(|editor, window, cx| {
13115 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13116 });
13117
13118 cx.editor(|editor, _, _| {
13119 let popover = editor.signature_help_state.popover().cloned().unwrap();
13120 assert_eq!(popover.current_signature, 2);
13121 });
13122
13123 // Test wrap around
13124 cx.update_editor(|editor, window, cx| {
13125 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13126 });
13127
13128 cx.editor(|editor, _, _| {
13129 let popover = editor.signature_help_state.popover().cloned().unwrap();
13130 assert_eq!(popover.current_signature, 0);
13131 });
13132
13133 // Test previous navigation
13134 cx.update_editor(|editor, window, cx| {
13135 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
13136 });
13137
13138 cx.editor(|editor, _, _| {
13139 let popover = editor.signature_help_state.popover().cloned().unwrap();
13140 assert_eq!(popover.current_signature, 2);
13141 });
13142}
13143
13144#[gpui::test]
13145async fn test_completion_mode(cx: &mut TestAppContext) {
13146 init_test(cx, |_| {});
13147 let mut cx = EditorLspTestContext::new_rust(
13148 lsp::ServerCapabilities {
13149 completion_provider: Some(lsp::CompletionOptions {
13150 resolve_provider: Some(true),
13151 ..Default::default()
13152 }),
13153 ..Default::default()
13154 },
13155 cx,
13156 )
13157 .await;
13158
13159 struct Run {
13160 run_description: &'static str,
13161 initial_state: String,
13162 buffer_marked_text: String,
13163 completion_label: &'static str,
13164 completion_text: &'static str,
13165 expected_with_insert_mode: String,
13166 expected_with_replace_mode: String,
13167 expected_with_replace_subsequence_mode: String,
13168 expected_with_replace_suffix_mode: String,
13169 }
13170
13171 let runs = [
13172 Run {
13173 run_description: "Start of word matches completion text",
13174 initial_state: "before ediˇ after".into(),
13175 buffer_marked_text: "before <edi|> after".into(),
13176 completion_label: "editor",
13177 completion_text: "editor",
13178 expected_with_insert_mode: "before editorˇ after".into(),
13179 expected_with_replace_mode: "before editorˇ after".into(),
13180 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13181 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13182 },
13183 Run {
13184 run_description: "Accept same text at the middle of the word",
13185 initial_state: "before ediˇtor after".into(),
13186 buffer_marked_text: "before <edi|tor> after".into(),
13187 completion_label: "editor",
13188 completion_text: "editor",
13189 expected_with_insert_mode: "before editorˇtor after".into(),
13190 expected_with_replace_mode: "before editorˇ after".into(),
13191 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13192 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13193 },
13194 Run {
13195 run_description: "End of word matches completion text -- cursor at end",
13196 initial_state: "before torˇ after".into(),
13197 buffer_marked_text: "before <tor|> after".into(),
13198 completion_label: "editor",
13199 completion_text: "editor",
13200 expected_with_insert_mode: "before editorˇ after".into(),
13201 expected_with_replace_mode: "before editorˇ after".into(),
13202 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13203 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13204 },
13205 Run {
13206 run_description: "End of word matches completion text -- cursor at start",
13207 initial_state: "before ˇtor after".into(),
13208 buffer_marked_text: "before <|tor> after".into(),
13209 completion_label: "editor",
13210 completion_text: "editor",
13211 expected_with_insert_mode: "before editorˇtor after".into(),
13212 expected_with_replace_mode: "before editorˇ after".into(),
13213 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13214 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13215 },
13216 Run {
13217 run_description: "Prepend text containing whitespace",
13218 initial_state: "pˇfield: bool".into(),
13219 buffer_marked_text: "<p|field>: bool".into(),
13220 completion_label: "pub ",
13221 completion_text: "pub ",
13222 expected_with_insert_mode: "pub ˇfield: bool".into(),
13223 expected_with_replace_mode: "pub ˇ: bool".into(),
13224 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
13225 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
13226 },
13227 Run {
13228 run_description: "Add element to start of list",
13229 initial_state: "[element_ˇelement_2]".into(),
13230 buffer_marked_text: "[<element_|element_2>]".into(),
13231 completion_label: "element_1",
13232 completion_text: "element_1",
13233 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
13234 expected_with_replace_mode: "[element_1ˇ]".into(),
13235 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
13236 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
13237 },
13238 Run {
13239 run_description: "Add element to start of list -- first and second elements are equal",
13240 initial_state: "[elˇelement]".into(),
13241 buffer_marked_text: "[<el|element>]".into(),
13242 completion_label: "element",
13243 completion_text: "element",
13244 expected_with_insert_mode: "[elementˇelement]".into(),
13245 expected_with_replace_mode: "[elementˇ]".into(),
13246 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
13247 expected_with_replace_suffix_mode: "[elementˇ]".into(),
13248 },
13249 Run {
13250 run_description: "Ends with matching suffix",
13251 initial_state: "SubˇError".into(),
13252 buffer_marked_text: "<Sub|Error>".into(),
13253 completion_label: "SubscriptionError",
13254 completion_text: "SubscriptionError",
13255 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
13256 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13257 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13258 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
13259 },
13260 Run {
13261 run_description: "Suffix is a subsequence -- contiguous",
13262 initial_state: "SubˇErr".into(),
13263 buffer_marked_text: "<Sub|Err>".into(),
13264 completion_label: "SubscriptionError",
13265 completion_text: "SubscriptionError",
13266 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
13267 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13268 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13269 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
13270 },
13271 Run {
13272 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
13273 initial_state: "Suˇscrirr".into(),
13274 buffer_marked_text: "<Su|scrirr>".into(),
13275 completion_label: "SubscriptionError",
13276 completion_text: "SubscriptionError",
13277 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
13278 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13279 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13280 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
13281 },
13282 Run {
13283 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
13284 initial_state: "foo(indˇix)".into(),
13285 buffer_marked_text: "foo(<ind|ix>)".into(),
13286 completion_label: "node_index",
13287 completion_text: "node_index",
13288 expected_with_insert_mode: "foo(node_indexˇix)".into(),
13289 expected_with_replace_mode: "foo(node_indexˇ)".into(),
13290 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
13291 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
13292 },
13293 Run {
13294 run_description: "Replace range ends before cursor - should extend to cursor",
13295 initial_state: "before editˇo after".into(),
13296 buffer_marked_text: "before <{ed}>it|o after".into(),
13297 completion_label: "editor",
13298 completion_text: "editor",
13299 expected_with_insert_mode: "before editorˇo after".into(),
13300 expected_with_replace_mode: "before editorˇo after".into(),
13301 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
13302 expected_with_replace_suffix_mode: "before editorˇo after".into(),
13303 },
13304 Run {
13305 run_description: "Uses label for suffix matching",
13306 initial_state: "before ediˇtor after".into(),
13307 buffer_marked_text: "before <edi|tor> after".into(),
13308 completion_label: "editor",
13309 completion_text: "editor()",
13310 expected_with_insert_mode: "before editor()ˇtor after".into(),
13311 expected_with_replace_mode: "before editor()ˇ after".into(),
13312 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
13313 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
13314 },
13315 Run {
13316 run_description: "Case insensitive subsequence and suffix matching",
13317 initial_state: "before EDiˇtoR after".into(),
13318 buffer_marked_text: "before <EDi|toR> after".into(),
13319 completion_label: "editor",
13320 completion_text: "editor",
13321 expected_with_insert_mode: "before editorˇtoR after".into(),
13322 expected_with_replace_mode: "before editorˇ after".into(),
13323 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13324 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13325 },
13326 ];
13327
13328 for run in runs {
13329 let run_variations = [
13330 (LspInsertMode::Insert, run.expected_with_insert_mode),
13331 (LspInsertMode::Replace, run.expected_with_replace_mode),
13332 (
13333 LspInsertMode::ReplaceSubsequence,
13334 run.expected_with_replace_subsequence_mode,
13335 ),
13336 (
13337 LspInsertMode::ReplaceSuffix,
13338 run.expected_with_replace_suffix_mode,
13339 ),
13340 ];
13341
13342 for (lsp_insert_mode, expected_text) in run_variations {
13343 eprintln!(
13344 "run = {:?}, mode = {lsp_insert_mode:.?}",
13345 run.run_description,
13346 );
13347
13348 update_test_language_settings(&mut cx, |settings| {
13349 settings.defaults.completions = Some(CompletionSettings {
13350 lsp_insert_mode,
13351 words: WordsCompletionMode::Disabled,
13352 words_min_length: 0,
13353 lsp: true,
13354 lsp_fetch_timeout_ms: 0,
13355 });
13356 });
13357
13358 cx.set_state(&run.initial_state);
13359 cx.update_editor(|editor, window, cx| {
13360 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13361 });
13362
13363 let counter = Arc::new(AtomicUsize::new(0));
13364 handle_completion_request_with_insert_and_replace(
13365 &mut cx,
13366 &run.buffer_marked_text,
13367 vec![(run.completion_label, run.completion_text)],
13368 counter.clone(),
13369 )
13370 .await;
13371 cx.condition(|editor, _| editor.context_menu_visible())
13372 .await;
13373 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13374
13375 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13376 editor
13377 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13378 .unwrap()
13379 });
13380 cx.assert_editor_state(&expected_text);
13381 handle_resolve_completion_request(&mut cx, None).await;
13382 apply_additional_edits.await.unwrap();
13383 }
13384 }
13385}
13386
13387#[gpui::test]
13388async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
13389 init_test(cx, |_| {});
13390 let mut cx = EditorLspTestContext::new_rust(
13391 lsp::ServerCapabilities {
13392 completion_provider: Some(lsp::CompletionOptions {
13393 resolve_provider: Some(true),
13394 ..Default::default()
13395 }),
13396 ..Default::default()
13397 },
13398 cx,
13399 )
13400 .await;
13401
13402 let initial_state = "SubˇError";
13403 let buffer_marked_text = "<Sub|Error>";
13404 let completion_text = "SubscriptionError";
13405 let expected_with_insert_mode = "SubscriptionErrorˇError";
13406 let expected_with_replace_mode = "SubscriptionErrorˇ";
13407
13408 update_test_language_settings(&mut cx, |settings| {
13409 settings.defaults.completions = Some(CompletionSettings {
13410 words: WordsCompletionMode::Disabled,
13411 words_min_length: 0,
13412 // set the opposite here to ensure that the action is overriding the default behavior
13413 lsp_insert_mode: LspInsertMode::Insert,
13414 lsp: true,
13415 lsp_fetch_timeout_ms: 0,
13416 });
13417 });
13418
13419 cx.set_state(initial_state);
13420 cx.update_editor(|editor, window, cx| {
13421 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13422 });
13423
13424 let counter = Arc::new(AtomicUsize::new(0));
13425 handle_completion_request_with_insert_and_replace(
13426 &mut cx,
13427 buffer_marked_text,
13428 vec![(completion_text, completion_text)],
13429 counter.clone(),
13430 )
13431 .await;
13432 cx.condition(|editor, _| editor.context_menu_visible())
13433 .await;
13434 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13435
13436 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13437 editor
13438 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13439 .unwrap()
13440 });
13441 cx.assert_editor_state(expected_with_replace_mode);
13442 handle_resolve_completion_request(&mut cx, None).await;
13443 apply_additional_edits.await.unwrap();
13444
13445 update_test_language_settings(&mut cx, |settings| {
13446 settings.defaults.completions = Some(CompletionSettings {
13447 words: WordsCompletionMode::Disabled,
13448 words_min_length: 0,
13449 // set the opposite here to ensure that the action is overriding the default behavior
13450 lsp_insert_mode: LspInsertMode::Replace,
13451 lsp: true,
13452 lsp_fetch_timeout_ms: 0,
13453 });
13454 });
13455
13456 cx.set_state(initial_state);
13457 cx.update_editor(|editor, window, cx| {
13458 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13459 });
13460 handle_completion_request_with_insert_and_replace(
13461 &mut cx,
13462 buffer_marked_text,
13463 vec![(completion_text, completion_text)],
13464 counter.clone(),
13465 )
13466 .await;
13467 cx.condition(|editor, _| editor.context_menu_visible())
13468 .await;
13469 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
13470
13471 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13472 editor
13473 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
13474 .unwrap()
13475 });
13476 cx.assert_editor_state(expected_with_insert_mode);
13477 handle_resolve_completion_request(&mut cx, None).await;
13478 apply_additional_edits.await.unwrap();
13479}
13480
13481#[gpui::test]
13482async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
13483 init_test(cx, |_| {});
13484 let mut cx = EditorLspTestContext::new_rust(
13485 lsp::ServerCapabilities {
13486 completion_provider: Some(lsp::CompletionOptions {
13487 resolve_provider: Some(true),
13488 ..Default::default()
13489 }),
13490 ..Default::default()
13491 },
13492 cx,
13493 )
13494 .await;
13495
13496 // scenario: surrounding text matches completion text
13497 let completion_text = "to_offset";
13498 let initial_state = indoc! {"
13499 1. buf.to_offˇsuffix
13500 2. buf.to_offˇsuf
13501 3. buf.to_offˇfix
13502 4. buf.to_offˇ
13503 5. into_offˇensive
13504 6. ˇsuffix
13505 7. let ˇ //
13506 8. aaˇzz
13507 9. buf.to_off«zzzzzˇ»suffix
13508 10. buf.«ˇzzzzz»suffix
13509 11. to_off«ˇzzzzz»
13510
13511 buf.to_offˇsuffix // newest cursor
13512 "};
13513 let completion_marked_buffer = indoc! {"
13514 1. buf.to_offsuffix
13515 2. buf.to_offsuf
13516 3. buf.to_offfix
13517 4. buf.to_off
13518 5. into_offensive
13519 6. suffix
13520 7. let //
13521 8. aazz
13522 9. buf.to_offzzzzzsuffix
13523 10. buf.zzzzzsuffix
13524 11. to_offzzzzz
13525
13526 buf.<to_off|suffix> // newest cursor
13527 "};
13528 let expected = indoc! {"
13529 1. buf.to_offsetˇ
13530 2. buf.to_offsetˇsuf
13531 3. buf.to_offsetˇfix
13532 4. buf.to_offsetˇ
13533 5. into_offsetˇensive
13534 6. to_offsetˇsuffix
13535 7. let to_offsetˇ //
13536 8. aato_offsetˇzz
13537 9. buf.to_offsetˇ
13538 10. buf.to_offsetˇsuffix
13539 11. to_offsetˇ
13540
13541 buf.to_offsetˇ // newest cursor
13542 "};
13543 cx.set_state(initial_state);
13544 cx.update_editor(|editor, window, cx| {
13545 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13546 });
13547 handle_completion_request_with_insert_and_replace(
13548 &mut cx,
13549 completion_marked_buffer,
13550 vec![(completion_text, completion_text)],
13551 Arc::new(AtomicUsize::new(0)),
13552 )
13553 .await;
13554 cx.condition(|editor, _| editor.context_menu_visible())
13555 .await;
13556 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13557 editor
13558 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13559 .unwrap()
13560 });
13561 cx.assert_editor_state(expected);
13562 handle_resolve_completion_request(&mut cx, None).await;
13563 apply_additional_edits.await.unwrap();
13564
13565 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
13566 let completion_text = "foo_and_bar";
13567 let initial_state = indoc! {"
13568 1. ooanbˇ
13569 2. zooanbˇ
13570 3. ooanbˇz
13571 4. zooanbˇz
13572 5. ooanˇ
13573 6. oanbˇ
13574
13575 ooanbˇ
13576 "};
13577 let completion_marked_buffer = indoc! {"
13578 1. ooanb
13579 2. zooanb
13580 3. ooanbz
13581 4. zooanbz
13582 5. ooan
13583 6. oanb
13584
13585 <ooanb|>
13586 "};
13587 let expected = indoc! {"
13588 1. foo_and_barˇ
13589 2. zfoo_and_barˇ
13590 3. foo_and_barˇz
13591 4. zfoo_and_barˇz
13592 5. ooanfoo_and_barˇ
13593 6. oanbfoo_and_barˇ
13594
13595 foo_and_barˇ
13596 "};
13597 cx.set_state(initial_state);
13598 cx.update_editor(|editor, window, cx| {
13599 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13600 });
13601 handle_completion_request_with_insert_and_replace(
13602 &mut cx,
13603 completion_marked_buffer,
13604 vec![(completion_text, completion_text)],
13605 Arc::new(AtomicUsize::new(0)),
13606 )
13607 .await;
13608 cx.condition(|editor, _| editor.context_menu_visible())
13609 .await;
13610 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13611 editor
13612 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13613 .unwrap()
13614 });
13615 cx.assert_editor_state(expected);
13616 handle_resolve_completion_request(&mut cx, None).await;
13617 apply_additional_edits.await.unwrap();
13618
13619 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
13620 // (expects the same as if it was inserted at the end)
13621 let completion_text = "foo_and_bar";
13622 let initial_state = indoc! {"
13623 1. ooˇanb
13624 2. zooˇanb
13625 3. ooˇanbz
13626 4. zooˇanbz
13627
13628 ooˇanb
13629 "};
13630 let completion_marked_buffer = indoc! {"
13631 1. ooanb
13632 2. zooanb
13633 3. ooanbz
13634 4. zooanbz
13635
13636 <oo|anb>
13637 "};
13638 let expected = indoc! {"
13639 1. foo_and_barˇ
13640 2. zfoo_and_barˇ
13641 3. foo_and_barˇz
13642 4. zfoo_and_barˇz
13643
13644 foo_and_barˇ
13645 "};
13646 cx.set_state(initial_state);
13647 cx.update_editor(|editor, window, cx| {
13648 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13649 });
13650 handle_completion_request_with_insert_and_replace(
13651 &mut cx,
13652 completion_marked_buffer,
13653 vec![(completion_text, completion_text)],
13654 Arc::new(AtomicUsize::new(0)),
13655 )
13656 .await;
13657 cx.condition(|editor, _| editor.context_menu_visible())
13658 .await;
13659 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13660 editor
13661 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13662 .unwrap()
13663 });
13664 cx.assert_editor_state(expected);
13665 handle_resolve_completion_request(&mut cx, None).await;
13666 apply_additional_edits.await.unwrap();
13667}
13668
13669// This used to crash
13670#[gpui::test]
13671async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
13672 init_test(cx, |_| {});
13673
13674 let buffer_text = indoc! {"
13675 fn main() {
13676 10.satu;
13677
13678 //
13679 // separate cursors so they open in different excerpts (manually reproducible)
13680 //
13681
13682 10.satu20;
13683 }
13684 "};
13685 let multibuffer_text_with_selections = indoc! {"
13686 fn main() {
13687 10.satuˇ;
13688
13689 //
13690
13691 //
13692
13693 10.satuˇ20;
13694 }
13695 "};
13696 let expected_multibuffer = indoc! {"
13697 fn main() {
13698 10.saturating_sub()ˇ;
13699
13700 //
13701
13702 //
13703
13704 10.saturating_sub()ˇ;
13705 }
13706 "};
13707
13708 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
13709 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
13710
13711 let fs = FakeFs::new(cx.executor());
13712 fs.insert_tree(
13713 path!("/a"),
13714 json!({
13715 "main.rs": buffer_text,
13716 }),
13717 )
13718 .await;
13719
13720 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13721 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13722 language_registry.add(rust_lang());
13723 let mut fake_servers = language_registry.register_fake_lsp(
13724 "Rust",
13725 FakeLspAdapter {
13726 capabilities: lsp::ServerCapabilities {
13727 completion_provider: Some(lsp::CompletionOptions {
13728 resolve_provider: None,
13729 ..lsp::CompletionOptions::default()
13730 }),
13731 ..lsp::ServerCapabilities::default()
13732 },
13733 ..FakeLspAdapter::default()
13734 },
13735 );
13736 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13737 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13738 let buffer = project
13739 .update(cx, |project, cx| {
13740 project.open_local_buffer(path!("/a/main.rs"), cx)
13741 })
13742 .await
13743 .unwrap();
13744
13745 let multi_buffer = cx.new(|cx| {
13746 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
13747 multi_buffer.push_excerpts(
13748 buffer.clone(),
13749 [ExcerptRange::new(0..first_excerpt_end)],
13750 cx,
13751 );
13752 multi_buffer.push_excerpts(
13753 buffer.clone(),
13754 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
13755 cx,
13756 );
13757 multi_buffer
13758 });
13759
13760 let editor = workspace
13761 .update(cx, |_, window, cx| {
13762 cx.new(|cx| {
13763 Editor::new(
13764 EditorMode::Full {
13765 scale_ui_elements_with_buffer_font_size: false,
13766 show_active_line_background: false,
13767 sized_by_content: false,
13768 },
13769 multi_buffer.clone(),
13770 Some(project.clone()),
13771 window,
13772 cx,
13773 )
13774 })
13775 })
13776 .unwrap();
13777
13778 let pane = workspace
13779 .update(cx, |workspace, _, _| workspace.active_pane().clone())
13780 .unwrap();
13781 pane.update_in(cx, |pane, window, cx| {
13782 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
13783 });
13784
13785 let fake_server = fake_servers.next().await.unwrap();
13786
13787 editor.update_in(cx, |editor, window, cx| {
13788 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13789 s.select_ranges([
13790 Point::new(1, 11)..Point::new(1, 11),
13791 Point::new(7, 11)..Point::new(7, 11),
13792 ])
13793 });
13794
13795 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
13796 });
13797
13798 editor.update_in(cx, |editor, window, cx| {
13799 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13800 });
13801
13802 fake_server
13803 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13804 let completion_item = lsp::CompletionItem {
13805 label: "saturating_sub()".into(),
13806 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
13807 lsp::InsertReplaceEdit {
13808 new_text: "saturating_sub()".to_owned(),
13809 insert: lsp::Range::new(
13810 lsp::Position::new(7, 7),
13811 lsp::Position::new(7, 11),
13812 ),
13813 replace: lsp::Range::new(
13814 lsp::Position::new(7, 7),
13815 lsp::Position::new(7, 13),
13816 ),
13817 },
13818 )),
13819 ..lsp::CompletionItem::default()
13820 };
13821
13822 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
13823 })
13824 .next()
13825 .await
13826 .unwrap();
13827
13828 cx.condition(&editor, |editor, _| editor.context_menu_visible())
13829 .await;
13830
13831 editor
13832 .update_in(cx, |editor, window, cx| {
13833 editor
13834 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13835 .unwrap()
13836 })
13837 .await
13838 .unwrap();
13839
13840 editor.update(cx, |editor, cx| {
13841 assert_text_with_selections(editor, expected_multibuffer, cx);
13842 })
13843}
13844
13845#[gpui::test]
13846async fn test_completion(cx: &mut TestAppContext) {
13847 init_test(cx, |_| {});
13848
13849 let mut cx = EditorLspTestContext::new_rust(
13850 lsp::ServerCapabilities {
13851 completion_provider: Some(lsp::CompletionOptions {
13852 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
13853 resolve_provider: Some(true),
13854 ..Default::default()
13855 }),
13856 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13857 ..Default::default()
13858 },
13859 cx,
13860 )
13861 .await;
13862 let counter = Arc::new(AtomicUsize::new(0));
13863
13864 cx.set_state(indoc! {"
13865 oneˇ
13866 two
13867 three
13868 "});
13869 cx.simulate_keystroke(".");
13870 handle_completion_request(
13871 indoc! {"
13872 one.|<>
13873 two
13874 three
13875 "},
13876 vec!["first_completion", "second_completion"],
13877 true,
13878 counter.clone(),
13879 &mut cx,
13880 )
13881 .await;
13882 cx.condition(|editor, _| editor.context_menu_visible())
13883 .await;
13884 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13885
13886 let _handler = handle_signature_help_request(
13887 &mut cx,
13888 lsp::SignatureHelp {
13889 signatures: vec![lsp::SignatureInformation {
13890 label: "test signature".to_string(),
13891 documentation: None,
13892 parameters: Some(vec![lsp::ParameterInformation {
13893 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
13894 documentation: None,
13895 }]),
13896 active_parameter: None,
13897 }],
13898 active_signature: None,
13899 active_parameter: None,
13900 },
13901 );
13902 cx.update_editor(|editor, window, cx| {
13903 assert!(
13904 !editor.signature_help_state.is_shown(),
13905 "No signature help was called for"
13906 );
13907 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13908 });
13909 cx.run_until_parked();
13910 cx.update_editor(|editor, _, _| {
13911 assert!(
13912 !editor.signature_help_state.is_shown(),
13913 "No signature help should be shown when completions menu is open"
13914 );
13915 });
13916
13917 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13918 editor.context_menu_next(&Default::default(), window, cx);
13919 editor
13920 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13921 .unwrap()
13922 });
13923 cx.assert_editor_state(indoc! {"
13924 one.second_completionˇ
13925 two
13926 three
13927 "});
13928
13929 handle_resolve_completion_request(
13930 &mut cx,
13931 Some(vec![
13932 (
13933 //This overlaps with the primary completion edit which is
13934 //misbehavior from the LSP spec, test that we filter it out
13935 indoc! {"
13936 one.second_ˇcompletion
13937 two
13938 threeˇ
13939 "},
13940 "overlapping additional edit",
13941 ),
13942 (
13943 indoc! {"
13944 one.second_completion
13945 two
13946 threeˇ
13947 "},
13948 "\nadditional edit",
13949 ),
13950 ]),
13951 )
13952 .await;
13953 apply_additional_edits.await.unwrap();
13954 cx.assert_editor_state(indoc! {"
13955 one.second_completionˇ
13956 two
13957 three
13958 additional edit
13959 "});
13960
13961 cx.set_state(indoc! {"
13962 one.second_completion
13963 twoˇ
13964 threeˇ
13965 additional edit
13966 "});
13967 cx.simulate_keystroke(" ");
13968 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
13969 cx.simulate_keystroke("s");
13970 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
13971
13972 cx.assert_editor_state(indoc! {"
13973 one.second_completion
13974 two sˇ
13975 three sˇ
13976 additional edit
13977 "});
13978 handle_completion_request(
13979 indoc! {"
13980 one.second_completion
13981 two s
13982 three <s|>
13983 additional edit
13984 "},
13985 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
13986 true,
13987 counter.clone(),
13988 &mut cx,
13989 )
13990 .await;
13991 cx.condition(|editor, _| editor.context_menu_visible())
13992 .await;
13993 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
13994
13995 cx.simulate_keystroke("i");
13996
13997 handle_completion_request(
13998 indoc! {"
13999 one.second_completion
14000 two si
14001 three <si|>
14002 additional edit
14003 "},
14004 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14005 true,
14006 counter.clone(),
14007 &mut cx,
14008 )
14009 .await;
14010 cx.condition(|editor, _| editor.context_menu_visible())
14011 .await;
14012 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14013
14014 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14015 editor
14016 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14017 .unwrap()
14018 });
14019 cx.assert_editor_state(indoc! {"
14020 one.second_completion
14021 two sixth_completionˇ
14022 three sixth_completionˇ
14023 additional edit
14024 "});
14025
14026 apply_additional_edits.await.unwrap();
14027
14028 update_test_language_settings(&mut cx, |settings| {
14029 settings.defaults.show_completions_on_input = Some(false);
14030 });
14031 cx.set_state("editorˇ");
14032 cx.simulate_keystroke(".");
14033 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14034 cx.simulate_keystrokes("c l o");
14035 cx.assert_editor_state("editor.cloˇ");
14036 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14037 cx.update_editor(|editor, window, cx| {
14038 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14039 });
14040 handle_completion_request(
14041 "editor.<clo|>",
14042 vec!["close", "clobber"],
14043 true,
14044 counter.clone(),
14045 &mut cx,
14046 )
14047 .await;
14048 cx.condition(|editor, _| editor.context_menu_visible())
14049 .await;
14050 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
14051
14052 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14053 editor
14054 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14055 .unwrap()
14056 });
14057 cx.assert_editor_state("editor.clobberˇ");
14058 handle_resolve_completion_request(&mut cx, None).await;
14059 apply_additional_edits.await.unwrap();
14060}
14061
14062#[gpui::test]
14063async fn test_completion_reuse(cx: &mut TestAppContext) {
14064 init_test(cx, |_| {});
14065
14066 let mut cx = EditorLspTestContext::new_rust(
14067 lsp::ServerCapabilities {
14068 completion_provider: Some(lsp::CompletionOptions {
14069 trigger_characters: Some(vec![".".to_string()]),
14070 ..Default::default()
14071 }),
14072 ..Default::default()
14073 },
14074 cx,
14075 )
14076 .await;
14077
14078 let counter = Arc::new(AtomicUsize::new(0));
14079 cx.set_state("objˇ");
14080 cx.simulate_keystroke(".");
14081
14082 // Initial completion request returns complete results
14083 let is_incomplete = false;
14084 handle_completion_request(
14085 "obj.|<>",
14086 vec!["a", "ab", "abc"],
14087 is_incomplete,
14088 counter.clone(),
14089 &mut cx,
14090 )
14091 .await;
14092 cx.run_until_parked();
14093 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14094 cx.assert_editor_state("obj.ˇ");
14095 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14096
14097 // Type "a" - filters existing completions
14098 cx.simulate_keystroke("a");
14099 cx.run_until_parked();
14100 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14101 cx.assert_editor_state("obj.aˇ");
14102 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14103
14104 // Type "b" - filters existing completions
14105 cx.simulate_keystroke("b");
14106 cx.run_until_parked();
14107 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14108 cx.assert_editor_state("obj.abˇ");
14109 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14110
14111 // Type "c" - filters existing completions
14112 cx.simulate_keystroke("c");
14113 cx.run_until_parked();
14114 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14115 cx.assert_editor_state("obj.abcˇ");
14116 check_displayed_completions(vec!["abc"], &mut cx);
14117
14118 // Backspace to delete "c" - filters existing completions
14119 cx.update_editor(|editor, window, cx| {
14120 editor.backspace(&Backspace, window, cx);
14121 });
14122 cx.run_until_parked();
14123 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14124 cx.assert_editor_state("obj.abˇ");
14125 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14126
14127 // Moving cursor to the left dismisses menu.
14128 cx.update_editor(|editor, window, cx| {
14129 editor.move_left(&MoveLeft, window, cx);
14130 });
14131 cx.run_until_parked();
14132 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14133 cx.assert_editor_state("obj.aˇb");
14134 cx.update_editor(|editor, _, _| {
14135 assert_eq!(editor.context_menu_visible(), false);
14136 });
14137
14138 // Type "b" - new request
14139 cx.simulate_keystroke("b");
14140 let is_incomplete = false;
14141 handle_completion_request(
14142 "obj.<ab|>a",
14143 vec!["ab", "abc"],
14144 is_incomplete,
14145 counter.clone(),
14146 &mut cx,
14147 )
14148 .await;
14149 cx.run_until_parked();
14150 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14151 cx.assert_editor_state("obj.abˇb");
14152 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14153
14154 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
14155 cx.update_editor(|editor, window, cx| {
14156 editor.backspace(&Backspace, window, cx);
14157 });
14158 let is_incomplete = false;
14159 handle_completion_request(
14160 "obj.<a|>b",
14161 vec!["a", "ab", "abc"],
14162 is_incomplete,
14163 counter.clone(),
14164 &mut cx,
14165 )
14166 .await;
14167 cx.run_until_parked();
14168 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14169 cx.assert_editor_state("obj.aˇb");
14170 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14171
14172 // Backspace to delete "a" - dismisses menu.
14173 cx.update_editor(|editor, window, cx| {
14174 editor.backspace(&Backspace, window, cx);
14175 });
14176 cx.run_until_parked();
14177 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14178 cx.assert_editor_state("obj.ˇb");
14179 cx.update_editor(|editor, _, _| {
14180 assert_eq!(editor.context_menu_visible(), false);
14181 });
14182}
14183
14184#[gpui::test]
14185async fn test_word_completion(cx: &mut TestAppContext) {
14186 let lsp_fetch_timeout_ms = 10;
14187 init_test(cx, |language_settings| {
14188 language_settings.defaults.completions = Some(CompletionSettings {
14189 words: WordsCompletionMode::Fallback,
14190 words_min_length: 0,
14191 lsp: true,
14192 lsp_fetch_timeout_ms: 10,
14193 lsp_insert_mode: LspInsertMode::Insert,
14194 });
14195 });
14196
14197 let mut cx = EditorLspTestContext::new_rust(
14198 lsp::ServerCapabilities {
14199 completion_provider: Some(lsp::CompletionOptions {
14200 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14201 ..lsp::CompletionOptions::default()
14202 }),
14203 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14204 ..lsp::ServerCapabilities::default()
14205 },
14206 cx,
14207 )
14208 .await;
14209
14210 let throttle_completions = Arc::new(AtomicBool::new(false));
14211
14212 let lsp_throttle_completions = throttle_completions.clone();
14213 let _completion_requests_handler =
14214 cx.lsp
14215 .server
14216 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
14217 let lsp_throttle_completions = lsp_throttle_completions.clone();
14218 let cx = cx.clone();
14219 async move {
14220 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
14221 cx.background_executor()
14222 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
14223 .await;
14224 }
14225 Ok(Some(lsp::CompletionResponse::Array(vec![
14226 lsp::CompletionItem {
14227 label: "first".into(),
14228 ..lsp::CompletionItem::default()
14229 },
14230 lsp::CompletionItem {
14231 label: "last".into(),
14232 ..lsp::CompletionItem::default()
14233 },
14234 ])))
14235 }
14236 });
14237
14238 cx.set_state(indoc! {"
14239 oneˇ
14240 two
14241 three
14242 "});
14243 cx.simulate_keystroke(".");
14244 cx.executor().run_until_parked();
14245 cx.condition(|editor, _| editor.context_menu_visible())
14246 .await;
14247 cx.update_editor(|editor, window, cx| {
14248 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14249 {
14250 assert_eq!(
14251 completion_menu_entries(menu),
14252 &["first", "last"],
14253 "When LSP server is fast to reply, no fallback word completions are used"
14254 );
14255 } else {
14256 panic!("expected completion menu to be open");
14257 }
14258 editor.cancel(&Cancel, window, cx);
14259 });
14260 cx.executor().run_until_parked();
14261 cx.condition(|editor, _| !editor.context_menu_visible())
14262 .await;
14263
14264 throttle_completions.store(true, atomic::Ordering::Release);
14265 cx.simulate_keystroke(".");
14266 cx.executor()
14267 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
14268 cx.executor().run_until_parked();
14269 cx.condition(|editor, _| editor.context_menu_visible())
14270 .await;
14271 cx.update_editor(|editor, _, _| {
14272 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14273 {
14274 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
14275 "When LSP server is slow, document words can be shown instead, if configured accordingly");
14276 } else {
14277 panic!("expected completion menu to be open");
14278 }
14279 });
14280}
14281
14282#[gpui::test]
14283async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
14284 init_test(cx, |language_settings| {
14285 language_settings.defaults.completions = Some(CompletionSettings {
14286 words: WordsCompletionMode::Enabled,
14287 words_min_length: 0,
14288 lsp: true,
14289 lsp_fetch_timeout_ms: 0,
14290 lsp_insert_mode: LspInsertMode::Insert,
14291 });
14292 });
14293
14294 let mut cx = EditorLspTestContext::new_rust(
14295 lsp::ServerCapabilities {
14296 completion_provider: Some(lsp::CompletionOptions {
14297 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14298 ..lsp::CompletionOptions::default()
14299 }),
14300 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14301 ..lsp::ServerCapabilities::default()
14302 },
14303 cx,
14304 )
14305 .await;
14306
14307 let _completion_requests_handler =
14308 cx.lsp
14309 .server
14310 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14311 Ok(Some(lsp::CompletionResponse::Array(vec![
14312 lsp::CompletionItem {
14313 label: "first".into(),
14314 ..lsp::CompletionItem::default()
14315 },
14316 lsp::CompletionItem {
14317 label: "last".into(),
14318 ..lsp::CompletionItem::default()
14319 },
14320 ])))
14321 });
14322
14323 cx.set_state(indoc! {"ˇ
14324 first
14325 last
14326 second
14327 "});
14328 cx.simulate_keystroke(".");
14329 cx.executor().run_until_parked();
14330 cx.condition(|editor, _| editor.context_menu_visible())
14331 .await;
14332 cx.update_editor(|editor, _, _| {
14333 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14334 {
14335 assert_eq!(
14336 completion_menu_entries(menu),
14337 &["first", "last", "second"],
14338 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
14339 );
14340 } else {
14341 panic!("expected completion menu to be open");
14342 }
14343 });
14344}
14345
14346#[gpui::test]
14347async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
14348 init_test(cx, |language_settings| {
14349 language_settings.defaults.completions = Some(CompletionSettings {
14350 words: WordsCompletionMode::Disabled,
14351 words_min_length: 0,
14352 lsp: true,
14353 lsp_fetch_timeout_ms: 0,
14354 lsp_insert_mode: LspInsertMode::Insert,
14355 });
14356 });
14357
14358 let mut cx = EditorLspTestContext::new_rust(
14359 lsp::ServerCapabilities {
14360 completion_provider: Some(lsp::CompletionOptions {
14361 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14362 ..lsp::CompletionOptions::default()
14363 }),
14364 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14365 ..lsp::ServerCapabilities::default()
14366 },
14367 cx,
14368 )
14369 .await;
14370
14371 let _completion_requests_handler =
14372 cx.lsp
14373 .server
14374 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14375 panic!("LSP completions should not be queried when dealing with word completions")
14376 });
14377
14378 cx.set_state(indoc! {"ˇ
14379 first
14380 last
14381 second
14382 "});
14383 cx.update_editor(|editor, window, cx| {
14384 editor.show_word_completions(&ShowWordCompletions, window, cx);
14385 });
14386 cx.executor().run_until_parked();
14387 cx.condition(|editor, _| editor.context_menu_visible())
14388 .await;
14389 cx.update_editor(|editor, _, _| {
14390 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14391 {
14392 assert_eq!(
14393 completion_menu_entries(menu),
14394 &["first", "last", "second"],
14395 "`ShowWordCompletions` action should show word completions"
14396 );
14397 } else {
14398 panic!("expected completion menu to be open");
14399 }
14400 });
14401
14402 cx.simulate_keystroke("l");
14403 cx.executor().run_until_parked();
14404 cx.condition(|editor, _| editor.context_menu_visible())
14405 .await;
14406 cx.update_editor(|editor, _, _| {
14407 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14408 {
14409 assert_eq!(
14410 completion_menu_entries(menu),
14411 &["last"],
14412 "After showing word completions, further editing should filter them and not query the LSP"
14413 );
14414 } else {
14415 panic!("expected completion menu to be open");
14416 }
14417 });
14418}
14419
14420#[gpui::test]
14421async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
14422 init_test(cx, |language_settings| {
14423 language_settings.defaults.completions = Some(CompletionSettings {
14424 words: WordsCompletionMode::Fallback,
14425 words_min_length: 0,
14426 lsp: false,
14427 lsp_fetch_timeout_ms: 0,
14428 lsp_insert_mode: LspInsertMode::Insert,
14429 });
14430 });
14431
14432 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14433
14434 cx.set_state(indoc! {"ˇ
14435 0_usize
14436 let
14437 33
14438 4.5f32
14439 "});
14440 cx.update_editor(|editor, window, cx| {
14441 editor.show_completions(&ShowCompletions::default(), window, cx);
14442 });
14443 cx.executor().run_until_parked();
14444 cx.condition(|editor, _| editor.context_menu_visible())
14445 .await;
14446 cx.update_editor(|editor, window, cx| {
14447 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14448 {
14449 assert_eq!(
14450 completion_menu_entries(menu),
14451 &["let"],
14452 "With no digits in the completion query, no digits should be in the word completions"
14453 );
14454 } else {
14455 panic!("expected completion menu to be open");
14456 }
14457 editor.cancel(&Cancel, window, cx);
14458 });
14459
14460 cx.set_state(indoc! {"3ˇ
14461 0_usize
14462 let
14463 3
14464 33.35f32
14465 "});
14466 cx.update_editor(|editor, window, cx| {
14467 editor.show_completions(&ShowCompletions::default(), window, cx);
14468 });
14469 cx.executor().run_until_parked();
14470 cx.condition(|editor, _| editor.context_menu_visible())
14471 .await;
14472 cx.update_editor(|editor, _, _| {
14473 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14474 {
14475 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
14476 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
14477 } else {
14478 panic!("expected completion menu to be open");
14479 }
14480 });
14481}
14482
14483#[gpui::test]
14484async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
14485 init_test(cx, |language_settings| {
14486 language_settings.defaults.completions = Some(CompletionSettings {
14487 words: WordsCompletionMode::Enabled,
14488 words_min_length: 3,
14489 lsp: true,
14490 lsp_fetch_timeout_ms: 0,
14491 lsp_insert_mode: LspInsertMode::Insert,
14492 });
14493 });
14494
14495 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14496 cx.set_state(indoc! {"ˇ
14497 wow
14498 wowen
14499 wowser
14500 "});
14501 cx.simulate_keystroke("w");
14502 cx.executor().run_until_parked();
14503 cx.update_editor(|editor, _, _| {
14504 if editor.context_menu.borrow_mut().is_some() {
14505 panic!(
14506 "expected completion menu to be hidden, as words completion threshold is not met"
14507 );
14508 }
14509 });
14510
14511 cx.update_editor(|editor, window, cx| {
14512 editor.show_word_completions(&ShowWordCompletions, window, cx);
14513 });
14514 cx.executor().run_until_parked();
14515 cx.update_editor(|editor, window, cx| {
14516 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14517 {
14518 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");
14519 } else {
14520 panic!("expected completion menu to be open after the word completions are called with an action");
14521 }
14522
14523 editor.cancel(&Cancel, window, cx);
14524 });
14525 cx.update_editor(|editor, _, _| {
14526 if editor.context_menu.borrow_mut().is_some() {
14527 panic!("expected completion menu to be hidden after canceling");
14528 }
14529 });
14530
14531 cx.simulate_keystroke("o");
14532 cx.executor().run_until_parked();
14533 cx.update_editor(|editor, _, _| {
14534 if editor.context_menu.borrow_mut().is_some() {
14535 panic!(
14536 "expected completion menu to be hidden, as words completion threshold is not met still"
14537 );
14538 }
14539 });
14540
14541 cx.simulate_keystroke("w");
14542 cx.executor().run_until_parked();
14543 cx.update_editor(|editor, _, _| {
14544 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14545 {
14546 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
14547 } else {
14548 panic!("expected completion menu to be open after the word completions threshold is met");
14549 }
14550 });
14551}
14552
14553#[gpui::test]
14554async fn test_word_completions_disabled(cx: &mut TestAppContext) {
14555 init_test(cx, |language_settings| {
14556 language_settings.defaults.completions = Some(CompletionSettings {
14557 words: WordsCompletionMode::Enabled,
14558 words_min_length: 0,
14559 lsp: true,
14560 lsp_fetch_timeout_ms: 0,
14561 lsp_insert_mode: LspInsertMode::Insert,
14562 });
14563 });
14564
14565 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14566 cx.update_editor(|editor, _, _| {
14567 editor.disable_word_completions();
14568 });
14569 cx.set_state(indoc! {"ˇ
14570 wow
14571 wowen
14572 wowser
14573 "});
14574 cx.simulate_keystroke("w");
14575 cx.executor().run_until_parked();
14576 cx.update_editor(|editor, _, _| {
14577 if editor.context_menu.borrow_mut().is_some() {
14578 panic!(
14579 "expected completion menu to be hidden, as words completion are disabled for this editor"
14580 );
14581 }
14582 });
14583
14584 cx.update_editor(|editor, window, cx| {
14585 editor.show_word_completions(&ShowWordCompletions, window, cx);
14586 });
14587 cx.executor().run_until_parked();
14588 cx.update_editor(|editor, _, _| {
14589 if editor.context_menu.borrow_mut().is_some() {
14590 panic!(
14591 "expected completion menu to be hidden even if called for explicitly, as words completion are disabled for this editor"
14592 );
14593 }
14594 });
14595}
14596
14597fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
14598 let position = || lsp::Position {
14599 line: params.text_document_position.position.line,
14600 character: params.text_document_position.position.character,
14601 };
14602 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14603 range: lsp::Range {
14604 start: position(),
14605 end: position(),
14606 },
14607 new_text: text.to_string(),
14608 }))
14609}
14610
14611#[gpui::test]
14612async fn test_multiline_completion(cx: &mut TestAppContext) {
14613 init_test(cx, |_| {});
14614
14615 let fs = FakeFs::new(cx.executor());
14616 fs.insert_tree(
14617 path!("/a"),
14618 json!({
14619 "main.ts": "a",
14620 }),
14621 )
14622 .await;
14623
14624 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14625 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14626 let typescript_language = Arc::new(Language::new(
14627 LanguageConfig {
14628 name: "TypeScript".into(),
14629 matcher: LanguageMatcher {
14630 path_suffixes: vec!["ts".to_string()],
14631 ..LanguageMatcher::default()
14632 },
14633 line_comments: vec!["// ".into()],
14634 ..LanguageConfig::default()
14635 },
14636 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
14637 ));
14638 language_registry.add(typescript_language.clone());
14639 let mut fake_servers = language_registry.register_fake_lsp(
14640 "TypeScript",
14641 FakeLspAdapter {
14642 capabilities: lsp::ServerCapabilities {
14643 completion_provider: Some(lsp::CompletionOptions {
14644 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14645 ..lsp::CompletionOptions::default()
14646 }),
14647 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14648 ..lsp::ServerCapabilities::default()
14649 },
14650 // Emulate vtsls label generation
14651 label_for_completion: Some(Box::new(|item, _| {
14652 let text = if let Some(description) = item
14653 .label_details
14654 .as_ref()
14655 .and_then(|label_details| label_details.description.as_ref())
14656 {
14657 format!("{} {}", item.label, description)
14658 } else if let Some(detail) = &item.detail {
14659 format!("{} {}", item.label, detail)
14660 } else {
14661 item.label.clone()
14662 };
14663 let len = text.len();
14664 Some(language::CodeLabel {
14665 text,
14666 runs: Vec::new(),
14667 filter_range: 0..len,
14668 })
14669 })),
14670 ..FakeLspAdapter::default()
14671 },
14672 );
14673 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14674 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14675 let worktree_id = workspace
14676 .update(cx, |workspace, _window, cx| {
14677 workspace.project().update(cx, |project, cx| {
14678 project.worktrees(cx).next().unwrap().read(cx).id()
14679 })
14680 })
14681 .unwrap();
14682 let _buffer = project
14683 .update(cx, |project, cx| {
14684 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
14685 })
14686 .await
14687 .unwrap();
14688 let editor = workspace
14689 .update(cx, |workspace, window, cx| {
14690 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
14691 })
14692 .unwrap()
14693 .await
14694 .unwrap()
14695 .downcast::<Editor>()
14696 .unwrap();
14697 let fake_server = fake_servers.next().await.unwrap();
14698
14699 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
14700 let multiline_label_2 = "a\nb\nc\n";
14701 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
14702 let multiline_description = "d\ne\nf\n";
14703 let multiline_detail_2 = "g\nh\ni\n";
14704
14705 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
14706 move |params, _| async move {
14707 Ok(Some(lsp::CompletionResponse::Array(vec![
14708 lsp::CompletionItem {
14709 label: multiline_label.to_string(),
14710 text_edit: gen_text_edit(¶ms, "new_text_1"),
14711 ..lsp::CompletionItem::default()
14712 },
14713 lsp::CompletionItem {
14714 label: "single line label 1".to_string(),
14715 detail: Some(multiline_detail.to_string()),
14716 text_edit: gen_text_edit(¶ms, "new_text_2"),
14717 ..lsp::CompletionItem::default()
14718 },
14719 lsp::CompletionItem {
14720 label: "single line label 2".to_string(),
14721 label_details: Some(lsp::CompletionItemLabelDetails {
14722 description: Some(multiline_description.to_string()),
14723 detail: None,
14724 }),
14725 text_edit: gen_text_edit(¶ms, "new_text_2"),
14726 ..lsp::CompletionItem::default()
14727 },
14728 lsp::CompletionItem {
14729 label: multiline_label_2.to_string(),
14730 detail: Some(multiline_detail_2.to_string()),
14731 text_edit: gen_text_edit(¶ms, "new_text_3"),
14732 ..lsp::CompletionItem::default()
14733 },
14734 lsp::CompletionItem {
14735 label: "Label with many spaces and \t but without newlines".to_string(),
14736 detail: Some(
14737 "Details with many spaces and \t but without newlines".to_string(),
14738 ),
14739 text_edit: gen_text_edit(¶ms, "new_text_4"),
14740 ..lsp::CompletionItem::default()
14741 },
14742 ])))
14743 },
14744 );
14745
14746 editor.update_in(cx, |editor, window, cx| {
14747 cx.focus_self(window);
14748 editor.move_to_end(&MoveToEnd, window, cx);
14749 editor.handle_input(".", window, cx);
14750 });
14751 cx.run_until_parked();
14752 completion_handle.next().await.unwrap();
14753
14754 editor.update(cx, |editor, _| {
14755 assert!(editor.context_menu_visible());
14756 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14757 {
14758 let completion_labels = menu
14759 .completions
14760 .borrow()
14761 .iter()
14762 .map(|c| c.label.text.clone())
14763 .collect::<Vec<_>>();
14764 assert_eq!(
14765 completion_labels,
14766 &[
14767 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
14768 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
14769 "single line label 2 d e f ",
14770 "a b c g h i ",
14771 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
14772 ],
14773 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
14774 );
14775
14776 for completion in menu
14777 .completions
14778 .borrow()
14779 .iter() {
14780 assert_eq!(
14781 completion.label.filter_range,
14782 0..completion.label.text.len(),
14783 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
14784 );
14785 }
14786 } else {
14787 panic!("expected completion menu to be open");
14788 }
14789 });
14790}
14791
14792#[gpui::test]
14793async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
14794 init_test(cx, |_| {});
14795 let mut cx = EditorLspTestContext::new_rust(
14796 lsp::ServerCapabilities {
14797 completion_provider: Some(lsp::CompletionOptions {
14798 trigger_characters: Some(vec![".".to_string()]),
14799 ..Default::default()
14800 }),
14801 ..Default::default()
14802 },
14803 cx,
14804 )
14805 .await;
14806 cx.lsp
14807 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14808 Ok(Some(lsp::CompletionResponse::Array(vec![
14809 lsp::CompletionItem {
14810 label: "first".into(),
14811 ..Default::default()
14812 },
14813 lsp::CompletionItem {
14814 label: "last".into(),
14815 ..Default::default()
14816 },
14817 ])))
14818 });
14819 cx.set_state("variableˇ");
14820 cx.simulate_keystroke(".");
14821 cx.executor().run_until_parked();
14822
14823 cx.update_editor(|editor, _, _| {
14824 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14825 {
14826 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
14827 } else {
14828 panic!("expected completion menu to be open");
14829 }
14830 });
14831
14832 cx.update_editor(|editor, window, cx| {
14833 editor.move_page_down(&MovePageDown::default(), window, cx);
14834 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14835 {
14836 assert!(
14837 menu.selected_item == 1,
14838 "expected PageDown to select the last item from the context menu"
14839 );
14840 } else {
14841 panic!("expected completion menu to stay open after PageDown");
14842 }
14843 });
14844
14845 cx.update_editor(|editor, window, cx| {
14846 editor.move_page_up(&MovePageUp::default(), window, cx);
14847 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14848 {
14849 assert!(
14850 menu.selected_item == 0,
14851 "expected PageUp to select the first item from the context menu"
14852 );
14853 } else {
14854 panic!("expected completion menu to stay open after PageUp");
14855 }
14856 });
14857}
14858
14859#[gpui::test]
14860async fn test_as_is_completions(cx: &mut TestAppContext) {
14861 init_test(cx, |_| {});
14862 let mut cx = EditorLspTestContext::new_rust(
14863 lsp::ServerCapabilities {
14864 completion_provider: Some(lsp::CompletionOptions {
14865 ..Default::default()
14866 }),
14867 ..Default::default()
14868 },
14869 cx,
14870 )
14871 .await;
14872 cx.lsp
14873 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14874 Ok(Some(lsp::CompletionResponse::Array(vec![
14875 lsp::CompletionItem {
14876 label: "unsafe".into(),
14877 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14878 range: lsp::Range {
14879 start: lsp::Position {
14880 line: 1,
14881 character: 2,
14882 },
14883 end: lsp::Position {
14884 line: 1,
14885 character: 3,
14886 },
14887 },
14888 new_text: "unsafe".to_string(),
14889 })),
14890 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
14891 ..Default::default()
14892 },
14893 ])))
14894 });
14895 cx.set_state("fn a() {}\n nˇ");
14896 cx.executor().run_until_parked();
14897 cx.update_editor(|editor, window, cx| {
14898 editor.show_completions(
14899 &ShowCompletions {
14900 trigger: Some("\n".into()),
14901 },
14902 window,
14903 cx,
14904 );
14905 });
14906 cx.executor().run_until_parked();
14907
14908 cx.update_editor(|editor, window, cx| {
14909 editor.confirm_completion(&Default::default(), window, cx)
14910 });
14911 cx.executor().run_until_parked();
14912 cx.assert_editor_state("fn a() {}\n unsafeˇ");
14913}
14914
14915#[gpui::test]
14916async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
14917 init_test(cx, |_| {});
14918 let language =
14919 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
14920 let mut cx = EditorLspTestContext::new(
14921 language,
14922 lsp::ServerCapabilities {
14923 completion_provider: Some(lsp::CompletionOptions {
14924 ..lsp::CompletionOptions::default()
14925 }),
14926 ..lsp::ServerCapabilities::default()
14927 },
14928 cx,
14929 )
14930 .await;
14931
14932 cx.set_state(
14933 "#ifndef BAR_H
14934#define BAR_H
14935
14936#include <stdbool.h>
14937
14938int fn_branch(bool do_branch1, bool do_branch2);
14939
14940#endif // BAR_H
14941ˇ",
14942 );
14943 cx.executor().run_until_parked();
14944 cx.update_editor(|editor, window, cx| {
14945 editor.handle_input("#", window, cx);
14946 });
14947 cx.executor().run_until_parked();
14948 cx.update_editor(|editor, window, cx| {
14949 editor.handle_input("i", window, cx);
14950 });
14951 cx.executor().run_until_parked();
14952 cx.update_editor(|editor, window, cx| {
14953 editor.handle_input("n", window, cx);
14954 });
14955 cx.executor().run_until_parked();
14956 cx.assert_editor_state(
14957 "#ifndef BAR_H
14958#define BAR_H
14959
14960#include <stdbool.h>
14961
14962int fn_branch(bool do_branch1, bool do_branch2);
14963
14964#endif // BAR_H
14965#inˇ",
14966 );
14967
14968 cx.lsp
14969 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14970 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
14971 is_incomplete: false,
14972 item_defaults: None,
14973 items: vec![lsp::CompletionItem {
14974 kind: Some(lsp::CompletionItemKind::SNIPPET),
14975 label_details: Some(lsp::CompletionItemLabelDetails {
14976 detail: Some("header".to_string()),
14977 description: None,
14978 }),
14979 label: " include".to_string(),
14980 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14981 range: lsp::Range {
14982 start: lsp::Position {
14983 line: 8,
14984 character: 1,
14985 },
14986 end: lsp::Position {
14987 line: 8,
14988 character: 1,
14989 },
14990 },
14991 new_text: "include \"$0\"".to_string(),
14992 })),
14993 sort_text: Some("40b67681include".to_string()),
14994 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
14995 filter_text: Some("include".to_string()),
14996 insert_text: Some("include \"$0\"".to_string()),
14997 ..lsp::CompletionItem::default()
14998 }],
14999 })))
15000 });
15001 cx.update_editor(|editor, window, cx| {
15002 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15003 });
15004 cx.executor().run_until_parked();
15005 cx.update_editor(|editor, window, cx| {
15006 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15007 });
15008 cx.executor().run_until_parked();
15009 cx.assert_editor_state(
15010 "#ifndef BAR_H
15011#define BAR_H
15012
15013#include <stdbool.h>
15014
15015int fn_branch(bool do_branch1, bool do_branch2);
15016
15017#endif // BAR_H
15018#include \"ˇ\"",
15019 );
15020
15021 cx.lsp
15022 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15023 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15024 is_incomplete: true,
15025 item_defaults: None,
15026 items: vec![lsp::CompletionItem {
15027 kind: Some(lsp::CompletionItemKind::FILE),
15028 label: "AGL/".to_string(),
15029 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15030 range: lsp::Range {
15031 start: lsp::Position {
15032 line: 8,
15033 character: 10,
15034 },
15035 end: lsp::Position {
15036 line: 8,
15037 character: 11,
15038 },
15039 },
15040 new_text: "AGL/".to_string(),
15041 })),
15042 sort_text: Some("40b67681AGL/".to_string()),
15043 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15044 filter_text: Some("AGL/".to_string()),
15045 insert_text: Some("AGL/".to_string()),
15046 ..lsp::CompletionItem::default()
15047 }],
15048 })))
15049 });
15050 cx.update_editor(|editor, window, cx| {
15051 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15052 });
15053 cx.executor().run_until_parked();
15054 cx.update_editor(|editor, window, cx| {
15055 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15056 });
15057 cx.executor().run_until_parked();
15058 cx.assert_editor_state(
15059 r##"#ifndef BAR_H
15060#define BAR_H
15061
15062#include <stdbool.h>
15063
15064int fn_branch(bool do_branch1, bool do_branch2);
15065
15066#endif // BAR_H
15067#include "AGL/ˇ"##,
15068 );
15069
15070 cx.update_editor(|editor, window, cx| {
15071 editor.handle_input("\"", window, cx);
15072 });
15073 cx.executor().run_until_parked();
15074 cx.assert_editor_state(
15075 r##"#ifndef BAR_H
15076#define BAR_H
15077
15078#include <stdbool.h>
15079
15080int fn_branch(bool do_branch1, bool do_branch2);
15081
15082#endif // BAR_H
15083#include "AGL/"ˇ"##,
15084 );
15085}
15086
15087#[gpui::test]
15088async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
15089 init_test(cx, |_| {});
15090
15091 let mut cx = EditorLspTestContext::new_rust(
15092 lsp::ServerCapabilities {
15093 completion_provider: Some(lsp::CompletionOptions {
15094 trigger_characters: Some(vec![".".to_string()]),
15095 resolve_provider: Some(true),
15096 ..Default::default()
15097 }),
15098 ..Default::default()
15099 },
15100 cx,
15101 )
15102 .await;
15103
15104 cx.set_state("fn main() { let a = 2ˇ; }");
15105 cx.simulate_keystroke(".");
15106 let completion_item = lsp::CompletionItem {
15107 label: "Some".into(),
15108 kind: Some(lsp::CompletionItemKind::SNIPPET),
15109 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15110 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15111 kind: lsp::MarkupKind::Markdown,
15112 value: "```rust\nSome(2)\n```".to_string(),
15113 })),
15114 deprecated: Some(false),
15115 sort_text: Some("Some".to_string()),
15116 filter_text: Some("Some".to_string()),
15117 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15118 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15119 range: lsp::Range {
15120 start: lsp::Position {
15121 line: 0,
15122 character: 22,
15123 },
15124 end: lsp::Position {
15125 line: 0,
15126 character: 22,
15127 },
15128 },
15129 new_text: "Some(2)".to_string(),
15130 })),
15131 additional_text_edits: Some(vec![lsp::TextEdit {
15132 range: lsp::Range {
15133 start: lsp::Position {
15134 line: 0,
15135 character: 20,
15136 },
15137 end: lsp::Position {
15138 line: 0,
15139 character: 22,
15140 },
15141 },
15142 new_text: "".to_string(),
15143 }]),
15144 ..Default::default()
15145 };
15146
15147 let closure_completion_item = completion_item.clone();
15148 let counter = Arc::new(AtomicUsize::new(0));
15149 let counter_clone = counter.clone();
15150 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15151 let task_completion_item = closure_completion_item.clone();
15152 counter_clone.fetch_add(1, atomic::Ordering::Release);
15153 async move {
15154 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15155 is_incomplete: true,
15156 item_defaults: None,
15157 items: vec![task_completion_item],
15158 })))
15159 }
15160 });
15161
15162 cx.condition(|editor, _| editor.context_menu_visible())
15163 .await;
15164 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
15165 assert!(request.next().await.is_some());
15166 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15167
15168 cx.simulate_keystrokes("S o m");
15169 cx.condition(|editor, _| editor.context_menu_visible())
15170 .await;
15171 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
15172 assert!(request.next().await.is_some());
15173 assert!(request.next().await.is_some());
15174 assert!(request.next().await.is_some());
15175 request.close();
15176 assert!(request.next().await.is_none());
15177 assert_eq!(
15178 counter.load(atomic::Ordering::Acquire),
15179 4,
15180 "With the completions menu open, only one LSP request should happen per input"
15181 );
15182}
15183
15184#[gpui::test]
15185async fn test_toggle_comment(cx: &mut TestAppContext) {
15186 init_test(cx, |_| {});
15187 let mut cx = EditorTestContext::new(cx).await;
15188 let language = Arc::new(Language::new(
15189 LanguageConfig {
15190 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15191 ..Default::default()
15192 },
15193 Some(tree_sitter_rust::LANGUAGE.into()),
15194 ));
15195 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15196
15197 // If multiple selections intersect a line, the line is only toggled once.
15198 cx.set_state(indoc! {"
15199 fn a() {
15200 «//b();
15201 ˇ»// «c();
15202 //ˇ» d();
15203 }
15204 "});
15205
15206 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15207
15208 cx.assert_editor_state(indoc! {"
15209 fn a() {
15210 «b();
15211 c();
15212 ˇ» d();
15213 }
15214 "});
15215
15216 // The comment prefix is inserted at the same column for every line in a
15217 // selection.
15218 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15219
15220 cx.assert_editor_state(indoc! {"
15221 fn a() {
15222 // «b();
15223 // c();
15224 ˇ»// d();
15225 }
15226 "});
15227
15228 // If a selection ends at the beginning of a line, that line is not toggled.
15229 cx.set_selections_state(indoc! {"
15230 fn a() {
15231 // b();
15232 «// c();
15233 ˇ» // d();
15234 }
15235 "});
15236
15237 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15238
15239 cx.assert_editor_state(indoc! {"
15240 fn a() {
15241 // b();
15242 «c();
15243 ˇ» // d();
15244 }
15245 "});
15246
15247 // If a selection span a single line and is empty, the line is toggled.
15248 cx.set_state(indoc! {"
15249 fn a() {
15250 a();
15251 b();
15252 ˇ
15253 }
15254 "});
15255
15256 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15257
15258 cx.assert_editor_state(indoc! {"
15259 fn a() {
15260 a();
15261 b();
15262 //•ˇ
15263 }
15264 "});
15265
15266 // If a selection span multiple lines, empty lines are not toggled.
15267 cx.set_state(indoc! {"
15268 fn a() {
15269 «a();
15270
15271 c();ˇ»
15272 }
15273 "});
15274
15275 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15276
15277 cx.assert_editor_state(indoc! {"
15278 fn a() {
15279 // «a();
15280
15281 // c();ˇ»
15282 }
15283 "});
15284
15285 // If a selection includes multiple comment prefixes, all lines are uncommented.
15286 cx.set_state(indoc! {"
15287 fn a() {
15288 «// a();
15289 /// b();
15290 //! c();ˇ»
15291 }
15292 "});
15293
15294 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15295
15296 cx.assert_editor_state(indoc! {"
15297 fn a() {
15298 «a();
15299 b();
15300 c();ˇ»
15301 }
15302 "});
15303}
15304
15305#[gpui::test]
15306async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
15307 init_test(cx, |_| {});
15308 let mut cx = EditorTestContext::new(cx).await;
15309 let language = Arc::new(Language::new(
15310 LanguageConfig {
15311 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15312 ..Default::default()
15313 },
15314 Some(tree_sitter_rust::LANGUAGE.into()),
15315 ));
15316 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15317
15318 let toggle_comments = &ToggleComments {
15319 advance_downwards: false,
15320 ignore_indent: true,
15321 };
15322
15323 // If multiple selections intersect a line, the line is only toggled once.
15324 cx.set_state(indoc! {"
15325 fn a() {
15326 // «b();
15327 // c();
15328 // ˇ» d();
15329 }
15330 "});
15331
15332 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15333
15334 cx.assert_editor_state(indoc! {"
15335 fn a() {
15336 «b();
15337 c();
15338 ˇ» d();
15339 }
15340 "});
15341
15342 // The comment prefix is inserted at the beginning of each line
15343 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15344
15345 cx.assert_editor_state(indoc! {"
15346 fn a() {
15347 // «b();
15348 // c();
15349 // ˇ» d();
15350 }
15351 "});
15352
15353 // If a selection ends at the beginning of a line, that line is not toggled.
15354 cx.set_selections_state(indoc! {"
15355 fn a() {
15356 // b();
15357 // «c();
15358 ˇ»// d();
15359 }
15360 "});
15361
15362 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15363
15364 cx.assert_editor_state(indoc! {"
15365 fn a() {
15366 // b();
15367 «c();
15368 ˇ»// d();
15369 }
15370 "});
15371
15372 // If a selection span a single line and is empty, the line is toggled.
15373 cx.set_state(indoc! {"
15374 fn a() {
15375 a();
15376 b();
15377 ˇ
15378 }
15379 "});
15380
15381 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15382
15383 cx.assert_editor_state(indoc! {"
15384 fn a() {
15385 a();
15386 b();
15387 //ˇ
15388 }
15389 "});
15390
15391 // If a selection span multiple lines, empty lines are not toggled.
15392 cx.set_state(indoc! {"
15393 fn a() {
15394 «a();
15395
15396 c();ˇ»
15397 }
15398 "});
15399
15400 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15401
15402 cx.assert_editor_state(indoc! {"
15403 fn a() {
15404 // «a();
15405
15406 // c();ˇ»
15407 }
15408 "});
15409
15410 // If a selection includes multiple comment prefixes, all lines are uncommented.
15411 cx.set_state(indoc! {"
15412 fn a() {
15413 // «a();
15414 /// b();
15415 //! c();ˇ»
15416 }
15417 "});
15418
15419 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15420
15421 cx.assert_editor_state(indoc! {"
15422 fn a() {
15423 «a();
15424 b();
15425 c();ˇ»
15426 }
15427 "});
15428}
15429
15430#[gpui::test]
15431async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
15432 init_test(cx, |_| {});
15433
15434 let language = Arc::new(Language::new(
15435 LanguageConfig {
15436 line_comments: vec!["// ".into()],
15437 ..Default::default()
15438 },
15439 Some(tree_sitter_rust::LANGUAGE.into()),
15440 ));
15441
15442 let mut cx = EditorTestContext::new(cx).await;
15443
15444 cx.language_registry().add(language.clone());
15445 cx.update_buffer(|buffer, cx| {
15446 buffer.set_language(Some(language), cx);
15447 });
15448
15449 let toggle_comments = &ToggleComments {
15450 advance_downwards: true,
15451 ignore_indent: false,
15452 };
15453
15454 // Single cursor on one line -> advance
15455 // Cursor moves horizontally 3 characters as well on non-blank line
15456 cx.set_state(indoc!(
15457 "fn a() {
15458 ˇdog();
15459 cat();
15460 }"
15461 ));
15462 cx.update_editor(|editor, window, cx| {
15463 editor.toggle_comments(toggle_comments, window, cx);
15464 });
15465 cx.assert_editor_state(indoc!(
15466 "fn a() {
15467 // dog();
15468 catˇ();
15469 }"
15470 ));
15471
15472 // Single selection on one line -> don't advance
15473 cx.set_state(indoc!(
15474 "fn a() {
15475 «dog()ˇ»;
15476 cat();
15477 }"
15478 ));
15479 cx.update_editor(|editor, window, cx| {
15480 editor.toggle_comments(toggle_comments, window, cx);
15481 });
15482 cx.assert_editor_state(indoc!(
15483 "fn a() {
15484 // «dog()ˇ»;
15485 cat();
15486 }"
15487 ));
15488
15489 // Multiple cursors on one line -> advance
15490 cx.set_state(indoc!(
15491 "fn a() {
15492 ˇdˇog();
15493 cat();
15494 }"
15495 ));
15496 cx.update_editor(|editor, window, cx| {
15497 editor.toggle_comments(toggle_comments, window, cx);
15498 });
15499 cx.assert_editor_state(indoc!(
15500 "fn a() {
15501 // dog();
15502 catˇ(ˇ);
15503 }"
15504 ));
15505
15506 // Multiple cursors on one line, with selection -> don't advance
15507 cx.set_state(indoc!(
15508 "fn a() {
15509 ˇdˇog«()ˇ»;
15510 cat();
15511 }"
15512 ));
15513 cx.update_editor(|editor, window, cx| {
15514 editor.toggle_comments(toggle_comments, window, cx);
15515 });
15516 cx.assert_editor_state(indoc!(
15517 "fn a() {
15518 // ˇdˇog«()ˇ»;
15519 cat();
15520 }"
15521 ));
15522
15523 // Single cursor on one line -> advance
15524 // Cursor moves to column 0 on blank line
15525 cx.set_state(indoc!(
15526 "fn a() {
15527 ˇdog();
15528
15529 cat();
15530 }"
15531 ));
15532 cx.update_editor(|editor, window, cx| {
15533 editor.toggle_comments(toggle_comments, window, cx);
15534 });
15535 cx.assert_editor_state(indoc!(
15536 "fn a() {
15537 // dog();
15538 ˇ
15539 cat();
15540 }"
15541 ));
15542
15543 // Single cursor on one line -> advance
15544 // Cursor starts and ends at column 0
15545 cx.set_state(indoc!(
15546 "fn a() {
15547 ˇ dog();
15548 cat();
15549 }"
15550 ));
15551 cx.update_editor(|editor, window, cx| {
15552 editor.toggle_comments(toggle_comments, window, cx);
15553 });
15554 cx.assert_editor_state(indoc!(
15555 "fn a() {
15556 // dog();
15557 ˇ cat();
15558 }"
15559 ));
15560}
15561
15562#[gpui::test]
15563async fn test_toggle_block_comment(cx: &mut TestAppContext) {
15564 init_test(cx, |_| {});
15565
15566 let mut cx = EditorTestContext::new(cx).await;
15567
15568 let html_language = Arc::new(
15569 Language::new(
15570 LanguageConfig {
15571 name: "HTML".into(),
15572 block_comment: Some(BlockCommentConfig {
15573 start: "<!-- ".into(),
15574 prefix: "".into(),
15575 end: " -->".into(),
15576 tab_size: 0,
15577 }),
15578 ..Default::default()
15579 },
15580 Some(tree_sitter_html::LANGUAGE.into()),
15581 )
15582 .with_injection_query(
15583 r#"
15584 (script_element
15585 (raw_text) @injection.content
15586 (#set! injection.language "javascript"))
15587 "#,
15588 )
15589 .unwrap(),
15590 );
15591
15592 let javascript_language = Arc::new(Language::new(
15593 LanguageConfig {
15594 name: "JavaScript".into(),
15595 line_comments: vec!["// ".into()],
15596 ..Default::default()
15597 },
15598 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
15599 ));
15600
15601 cx.language_registry().add(html_language.clone());
15602 cx.language_registry().add(javascript_language);
15603 cx.update_buffer(|buffer, cx| {
15604 buffer.set_language(Some(html_language), cx);
15605 });
15606
15607 // Toggle comments for empty selections
15608 cx.set_state(
15609 &r#"
15610 <p>A</p>ˇ
15611 <p>B</p>ˇ
15612 <p>C</p>ˇ
15613 "#
15614 .unindent(),
15615 );
15616 cx.update_editor(|editor, window, cx| {
15617 editor.toggle_comments(&ToggleComments::default(), window, cx)
15618 });
15619 cx.assert_editor_state(
15620 &r#"
15621 <!-- <p>A</p>ˇ -->
15622 <!-- <p>B</p>ˇ -->
15623 <!-- <p>C</p>ˇ -->
15624 "#
15625 .unindent(),
15626 );
15627 cx.update_editor(|editor, window, cx| {
15628 editor.toggle_comments(&ToggleComments::default(), window, cx)
15629 });
15630 cx.assert_editor_state(
15631 &r#"
15632 <p>A</p>ˇ
15633 <p>B</p>ˇ
15634 <p>C</p>ˇ
15635 "#
15636 .unindent(),
15637 );
15638
15639 // Toggle comments for mixture of empty and non-empty selections, where
15640 // multiple selections occupy a given line.
15641 cx.set_state(
15642 &r#"
15643 <p>A«</p>
15644 <p>ˇ»B</p>ˇ
15645 <p>C«</p>
15646 <p>ˇ»D</p>ˇ
15647 "#
15648 .unindent(),
15649 );
15650
15651 cx.update_editor(|editor, window, cx| {
15652 editor.toggle_comments(&ToggleComments::default(), window, cx)
15653 });
15654 cx.assert_editor_state(
15655 &r#"
15656 <!-- <p>A«</p>
15657 <p>ˇ»B</p>ˇ -->
15658 <!-- <p>C«</p>
15659 <p>ˇ»D</p>ˇ -->
15660 "#
15661 .unindent(),
15662 );
15663 cx.update_editor(|editor, window, cx| {
15664 editor.toggle_comments(&ToggleComments::default(), window, cx)
15665 });
15666 cx.assert_editor_state(
15667 &r#"
15668 <p>A«</p>
15669 <p>ˇ»B</p>ˇ
15670 <p>C«</p>
15671 <p>ˇ»D</p>ˇ
15672 "#
15673 .unindent(),
15674 );
15675
15676 // Toggle comments when different languages are active for different
15677 // selections.
15678 cx.set_state(
15679 &r#"
15680 ˇ<script>
15681 ˇvar x = new Y();
15682 ˇ</script>
15683 "#
15684 .unindent(),
15685 );
15686 cx.executor().run_until_parked();
15687 cx.update_editor(|editor, window, cx| {
15688 editor.toggle_comments(&ToggleComments::default(), window, cx)
15689 });
15690 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
15691 // Uncommenting and commenting from this position brings in even more wrong artifacts.
15692 cx.assert_editor_state(
15693 &r#"
15694 <!-- ˇ<script> -->
15695 // ˇvar x = new Y();
15696 <!-- ˇ</script> -->
15697 "#
15698 .unindent(),
15699 );
15700}
15701
15702#[gpui::test]
15703fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
15704 init_test(cx, |_| {});
15705
15706 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15707 let multibuffer = cx.new(|cx| {
15708 let mut multibuffer = MultiBuffer::new(ReadWrite);
15709 multibuffer.push_excerpts(
15710 buffer.clone(),
15711 [
15712 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
15713 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
15714 ],
15715 cx,
15716 );
15717 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
15718 multibuffer
15719 });
15720
15721 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
15722 editor.update_in(cx, |editor, window, cx| {
15723 assert_eq!(editor.text(cx), "aaaa\nbbbb");
15724 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15725 s.select_ranges([
15726 Point::new(0, 0)..Point::new(0, 0),
15727 Point::new(1, 0)..Point::new(1, 0),
15728 ])
15729 });
15730
15731 editor.handle_input("X", window, cx);
15732 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
15733 assert_eq!(
15734 editor.selections.ranges(cx),
15735 [
15736 Point::new(0, 1)..Point::new(0, 1),
15737 Point::new(1, 1)..Point::new(1, 1),
15738 ]
15739 );
15740
15741 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
15742 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15743 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
15744 });
15745 editor.backspace(&Default::default(), window, cx);
15746 assert_eq!(editor.text(cx), "Xa\nbbb");
15747 assert_eq!(
15748 editor.selections.ranges(cx),
15749 [Point::new(1, 0)..Point::new(1, 0)]
15750 );
15751
15752 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15753 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
15754 });
15755 editor.backspace(&Default::default(), window, cx);
15756 assert_eq!(editor.text(cx), "X\nbb");
15757 assert_eq!(
15758 editor.selections.ranges(cx),
15759 [Point::new(0, 1)..Point::new(0, 1)]
15760 );
15761 });
15762}
15763
15764#[gpui::test]
15765fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
15766 init_test(cx, |_| {});
15767
15768 let markers = vec![('[', ']').into(), ('(', ')').into()];
15769 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
15770 indoc! {"
15771 [aaaa
15772 (bbbb]
15773 cccc)",
15774 },
15775 markers.clone(),
15776 );
15777 let excerpt_ranges = markers.into_iter().map(|marker| {
15778 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
15779 ExcerptRange::new(context)
15780 });
15781 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
15782 let multibuffer = cx.new(|cx| {
15783 let mut multibuffer = MultiBuffer::new(ReadWrite);
15784 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
15785 multibuffer
15786 });
15787
15788 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
15789 editor.update_in(cx, |editor, window, cx| {
15790 let (expected_text, selection_ranges) = marked_text_ranges(
15791 indoc! {"
15792 aaaa
15793 bˇbbb
15794 bˇbbˇb
15795 cccc"
15796 },
15797 true,
15798 );
15799 assert_eq!(editor.text(cx), expected_text);
15800 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15801 s.select_ranges(selection_ranges)
15802 });
15803
15804 editor.handle_input("X", window, cx);
15805
15806 let (expected_text, expected_selections) = marked_text_ranges(
15807 indoc! {"
15808 aaaa
15809 bXˇbbXb
15810 bXˇbbXˇb
15811 cccc"
15812 },
15813 false,
15814 );
15815 assert_eq!(editor.text(cx), expected_text);
15816 assert_eq!(editor.selections.ranges(cx), expected_selections);
15817
15818 editor.newline(&Newline, window, cx);
15819 let (expected_text, expected_selections) = marked_text_ranges(
15820 indoc! {"
15821 aaaa
15822 bX
15823 ˇbbX
15824 b
15825 bX
15826 ˇbbX
15827 ˇb
15828 cccc"
15829 },
15830 false,
15831 );
15832 assert_eq!(editor.text(cx), expected_text);
15833 assert_eq!(editor.selections.ranges(cx), expected_selections);
15834 });
15835}
15836
15837#[gpui::test]
15838fn test_refresh_selections(cx: &mut TestAppContext) {
15839 init_test(cx, |_| {});
15840
15841 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15842 let mut excerpt1_id = None;
15843 let multibuffer = cx.new(|cx| {
15844 let mut multibuffer = MultiBuffer::new(ReadWrite);
15845 excerpt1_id = multibuffer
15846 .push_excerpts(
15847 buffer.clone(),
15848 [
15849 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
15850 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
15851 ],
15852 cx,
15853 )
15854 .into_iter()
15855 .next();
15856 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
15857 multibuffer
15858 });
15859
15860 let editor = cx.add_window(|window, cx| {
15861 let mut editor = build_editor(multibuffer.clone(), window, cx);
15862 let snapshot = editor.snapshot(window, cx);
15863 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15864 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
15865 });
15866 editor.begin_selection(
15867 Point::new(2, 1).to_display_point(&snapshot),
15868 true,
15869 1,
15870 window,
15871 cx,
15872 );
15873 assert_eq!(
15874 editor.selections.ranges(cx),
15875 [
15876 Point::new(1, 3)..Point::new(1, 3),
15877 Point::new(2, 1)..Point::new(2, 1),
15878 ]
15879 );
15880 editor
15881 });
15882
15883 // Refreshing selections is a no-op when excerpts haven't changed.
15884 _ = editor.update(cx, |editor, window, cx| {
15885 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
15886 assert_eq!(
15887 editor.selections.ranges(cx),
15888 [
15889 Point::new(1, 3)..Point::new(1, 3),
15890 Point::new(2, 1)..Point::new(2, 1),
15891 ]
15892 );
15893 });
15894
15895 multibuffer.update(cx, |multibuffer, cx| {
15896 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
15897 });
15898 _ = editor.update(cx, |editor, window, cx| {
15899 // Removing an excerpt causes the first selection to become degenerate.
15900 assert_eq!(
15901 editor.selections.ranges(cx),
15902 [
15903 Point::new(0, 0)..Point::new(0, 0),
15904 Point::new(0, 1)..Point::new(0, 1)
15905 ]
15906 );
15907
15908 // Refreshing selections will relocate the first selection to the original buffer
15909 // location.
15910 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
15911 assert_eq!(
15912 editor.selections.ranges(cx),
15913 [
15914 Point::new(0, 1)..Point::new(0, 1),
15915 Point::new(0, 3)..Point::new(0, 3)
15916 ]
15917 );
15918 assert!(editor.selections.pending_anchor().is_some());
15919 });
15920}
15921
15922#[gpui::test]
15923fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
15924 init_test(cx, |_| {});
15925
15926 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15927 let mut excerpt1_id = None;
15928 let multibuffer = cx.new(|cx| {
15929 let mut multibuffer = MultiBuffer::new(ReadWrite);
15930 excerpt1_id = multibuffer
15931 .push_excerpts(
15932 buffer.clone(),
15933 [
15934 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
15935 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
15936 ],
15937 cx,
15938 )
15939 .into_iter()
15940 .next();
15941 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
15942 multibuffer
15943 });
15944
15945 let editor = cx.add_window(|window, cx| {
15946 let mut editor = build_editor(multibuffer.clone(), window, cx);
15947 let snapshot = editor.snapshot(window, cx);
15948 editor.begin_selection(
15949 Point::new(1, 3).to_display_point(&snapshot),
15950 false,
15951 1,
15952 window,
15953 cx,
15954 );
15955 assert_eq!(
15956 editor.selections.ranges(cx),
15957 [Point::new(1, 3)..Point::new(1, 3)]
15958 );
15959 editor
15960 });
15961
15962 multibuffer.update(cx, |multibuffer, cx| {
15963 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
15964 });
15965 _ = editor.update(cx, |editor, window, cx| {
15966 assert_eq!(
15967 editor.selections.ranges(cx),
15968 [Point::new(0, 0)..Point::new(0, 0)]
15969 );
15970
15971 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
15972 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
15973 assert_eq!(
15974 editor.selections.ranges(cx),
15975 [Point::new(0, 3)..Point::new(0, 3)]
15976 );
15977 assert!(editor.selections.pending_anchor().is_some());
15978 });
15979}
15980
15981#[gpui::test]
15982async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
15983 init_test(cx, |_| {});
15984
15985 let language = Arc::new(
15986 Language::new(
15987 LanguageConfig {
15988 brackets: BracketPairConfig {
15989 pairs: vec![
15990 BracketPair {
15991 start: "{".to_string(),
15992 end: "}".to_string(),
15993 close: true,
15994 surround: true,
15995 newline: true,
15996 },
15997 BracketPair {
15998 start: "/* ".to_string(),
15999 end: " */".to_string(),
16000 close: true,
16001 surround: true,
16002 newline: true,
16003 },
16004 ],
16005 ..Default::default()
16006 },
16007 ..Default::default()
16008 },
16009 Some(tree_sitter_rust::LANGUAGE.into()),
16010 )
16011 .with_indents_query("")
16012 .unwrap(),
16013 );
16014
16015 let text = concat!(
16016 "{ }\n", //
16017 " x\n", //
16018 " /* */\n", //
16019 "x\n", //
16020 "{{} }\n", //
16021 );
16022
16023 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
16024 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16025 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16026 editor
16027 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
16028 .await;
16029
16030 editor.update_in(cx, |editor, window, cx| {
16031 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16032 s.select_display_ranges([
16033 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
16034 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
16035 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
16036 ])
16037 });
16038 editor.newline(&Newline, window, cx);
16039
16040 assert_eq!(
16041 editor.buffer().read(cx).read(cx).text(),
16042 concat!(
16043 "{ \n", // Suppress rustfmt
16044 "\n", //
16045 "}\n", //
16046 " x\n", //
16047 " /* \n", //
16048 " \n", //
16049 " */\n", //
16050 "x\n", //
16051 "{{} \n", //
16052 "}\n", //
16053 )
16054 );
16055 });
16056}
16057
16058#[gpui::test]
16059fn test_highlighted_ranges(cx: &mut TestAppContext) {
16060 init_test(cx, |_| {});
16061
16062 let editor = cx.add_window(|window, cx| {
16063 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
16064 build_editor(buffer, window, cx)
16065 });
16066
16067 _ = editor.update(cx, |editor, window, cx| {
16068 struct Type1;
16069 struct Type2;
16070
16071 let buffer = editor.buffer.read(cx).snapshot(cx);
16072
16073 let anchor_range =
16074 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
16075
16076 editor.highlight_background::<Type1>(
16077 &[
16078 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
16079 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
16080 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
16081 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
16082 ],
16083 |_| Hsla::red(),
16084 cx,
16085 );
16086 editor.highlight_background::<Type2>(
16087 &[
16088 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
16089 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
16090 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
16091 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
16092 ],
16093 |_| Hsla::green(),
16094 cx,
16095 );
16096
16097 let snapshot = editor.snapshot(window, cx);
16098 let highlighted_ranges = editor.sorted_background_highlights_in_range(
16099 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
16100 &snapshot,
16101 cx.theme(),
16102 );
16103 assert_eq!(
16104 highlighted_ranges,
16105 &[
16106 (
16107 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
16108 Hsla::green(),
16109 ),
16110 (
16111 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
16112 Hsla::red(),
16113 ),
16114 (
16115 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
16116 Hsla::green(),
16117 ),
16118 (
16119 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16120 Hsla::red(),
16121 ),
16122 ]
16123 );
16124 assert_eq!(
16125 editor.sorted_background_highlights_in_range(
16126 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
16127 &snapshot,
16128 cx.theme(),
16129 ),
16130 &[(
16131 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16132 Hsla::red(),
16133 )]
16134 );
16135 });
16136}
16137
16138#[gpui::test]
16139async fn test_following(cx: &mut TestAppContext) {
16140 init_test(cx, |_| {});
16141
16142 let fs = FakeFs::new(cx.executor());
16143 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16144
16145 let buffer = project.update(cx, |project, cx| {
16146 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, false, cx);
16147 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
16148 });
16149 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
16150 let follower = cx.update(|cx| {
16151 cx.open_window(
16152 WindowOptions {
16153 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
16154 gpui::Point::new(px(0.), px(0.)),
16155 gpui::Point::new(px(10.), px(80.)),
16156 ))),
16157 ..Default::default()
16158 },
16159 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
16160 )
16161 .unwrap()
16162 });
16163
16164 let is_still_following = Rc::new(RefCell::new(true));
16165 let follower_edit_event_count = Rc::new(RefCell::new(0));
16166 let pending_update = Rc::new(RefCell::new(None));
16167 let leader_entity = leader.root(cx).unwrap();
16168 let follower_entity = follower.root(cx).unwrap();
16169 _ = follower.update(cx, {
16170 let update = pending_update.clone();
16171 let is_still_following = is_still_following.clone();
16172 let follower_edit_event_count = follower_edit_event_count.clone();
16173 |_, window, cx| {
16174 cx.subscribe_in(
16175 &leader_entity,
16176 window,
16177 move |_, leader, event, window, cx| {
16178 leader.read(cx).add_event_to_update_proto(
16179 event,
16180 &mut update.borrow_mut(),
16181 window,
16182 cx,
16183 );
16184 },
16185 )
16186 .detach();
16187
16188 cx.subscribe_in(
16189 &follower_entity,
16190 window,
16191 move |_, _, event: &EditorEvent, _window, _cx| {
16192 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
16193 *is_still_following.borrow_mut() = false;
16194 }
16195
16196 if let EditorEvent::BufferEdited = event {
16197 *follower_edit_event_count.borrow_mut() += 1;
16198 }
16199 },
16200 )
16201 .detach();
16202 }
16203 });
16204
16205 // Update the selections only
16206 _ = leader.update(cx, |leader, window, cx| {
16207 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16208 s.select_ranges([1..1])
16209 });
16210 });
16211 follower
16212 .update(cx, |follower, window, cx| {
16213 follower.apply_update_proto(
16214 &project,
16215 pending_update.borrow_mut().take().unwrap(),
16216 window,
16217 cx,
16218 )
16219 })
16220 .unwrap()
16221 .await
16222 .unwrap();
16223 _ = follower.update(cx, |follower, _, cx| {
16224 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
16225 });
16226 assert!(*is_still_following.borrow());
16227 assert_eq!(*follower_edit_event_count.borrow(), 0);
16228
16229 // Update the scroll position only
16230 _ = leader.update(cx, |leader, window, cx| {
16231 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16232 });
16233 follower
16234 .update(cx, |follower, window, cx| {
16235 follower.apply_update_proto(
16236 &project,
16237 pending_update.borrow_mut().take().unwrap(),
16238 window,
16239 cx,
16240 )
16241 })
16242 .unwrap()
16243 .await
16244 .unwrap();
16245 assert_eq!(
16246 follower
16247 .update(cx, |follower, _, cx| follower.scroll_position(cx))
16248 .unwrap(),
16249 gpui::Point::new(1.5, 3.5)
16250 );
16251 assert!(*is_still_following.borrow());
16252 assert_eq!(*follower_edit_event_count.borrow(), 0);
16253
16254 // Update the selections and scroll position. The follower's scroll position is updated
16255 // via autoscroll, not via the leader's exact scroll position.
16256 _ = leader.update(cx, |leader, window, cx| {
16257 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16258 s.select_ranges([0..0])
16259 });
16260 leader.request_autoscroll(Autoscroll::newest(), cx);
16261 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16262 });
16263 follower
16264 .update(cx, |follower, window, cx| {
16265 follower.apply_update_proto(
16266 &project,
16267 pending_update.borrow_mut().take().unwrap(),
16268 window,
16269 cx,
16270 )
16271 })
16272 .unwrap()
16273 .await
16274 .unwrap();
16275 _ = follower.update(cx, |follower, _, cx| {
16276 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
16277 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
16278 });
16279 assert!(*is_still_following.borrow());
16280
16281 // Creating a pending selection that precedes another selection
16282 _ = leader.update(cx, |leader, window, cx| {
16283 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16284 s.select_ranges([1..1])
16285 });
16286 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
16287 });
16288 follower
16289 .update(cx, |follower, window, cx| {
16290 follower.apply_update_proto(
16291 &project,
16292 pending_update.borrow_mut().take().unwrap(),
16293 window,
16294 cx,
16295 )
16296 })
16297 .unwrap()
16298 .await
16299 .unwrap();
16300 _ = follower.update(cx, |follower, _, cx| {
16301 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
16302 });
16303 assert!(*is_still_following.borrow());
16304
16305 // Extend the pending selection so that it surrounds another selection
16306 _ = leader.update(cx, |leader, window, cx| {
16307 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
16308 });
16309 follower
16310 .update(cx, |follower, window, cx| {
16311 follower.apply_update_proto(
16312 &project,
16313 pending_update.borrow_mut().take().unwrap(),
16314 window,
16315 cx,
16316 )
16317 })
16318 .unwrap()
16319 .await
16320 .unwrap();
16321 _ = follower.update(cx, |follower, _, cx| {
16322 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
16323 });
16324
16325 // Scrolling locally breaks the follow
16326 _ = follower.update(cx, |follower, window, cx| {
16327 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
16328 follower.set_scroll_anchor(
16329 ScrollAnchor {
16330 anchor: top_anchor,
16331 offset: gpui::Point::new(0.0, 0.5),
16332 },
16333 window,
16334 cx,
16335 );
16336 });
16337 assert!(!(*is_still_following.borrow()));
16338}
16339
16340#[gpui::test]
16341async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
16342 init_test(cx, |_| {});
16343
16344 let fs = FakeFs::new(cx.executor());
16345 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16346 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16347 let pane = workspace
16348 .update(cx, |workspace, _, _| workspace.active_pane().clone())
16349 .unwrap();
16350
16351 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16352
16353 let leader = pane.update_in(cx, |_, window, cx| {
16354 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
16355 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
16356 });
16357
16358 // Start following the editor when it has no excerpts.
16359 let mut state_message =
16360 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16361 let workspace_entity = workspace.root(cx).unwrap();
16362 let follower_1 = cx
16363 .update_window(*workspace.deref(), |_, window, cx| {
16364 Editor::from_state_proto(
16365 workspace_entity,
16366 ViewId {
16367 creator: CollaboratorId::PeerId(PeerId::default()),
16368 id: 0,
16369 },
16370 &mut state_message,
16371 window,
16372 cx,
16373 )
16374 })
16375 .unwrap()
16376 .unwrap()
16377 .await
16378 .unwrap();
16379
16380 let update_message = Rc::new(RefCell::new(None));
16381 follower_1.update_in(cx, {
16382 let update = update_message.clone();
16383 |_, window, cx| {
16384 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
16385 leader.read(cx).add_event_to_update_proto(
16386 event,
16387 &mut update.borrow_mut(),
16388 window,
16389 cx,
16390 );
16391 })
16392 .detach();
16393 }
16394 });
16395
16396 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
16397 (
16398 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, false, cx),
16399 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, false, cx),
16400 )
16401 });
16402
16403 // Insert some excerpts.
16404 leader.update(cx, |leader, cx| {
16405 leader.buffer.update(cx, |multibuffer, cx| {
16406 multibuffer.set_excerpts_for_path(
16407 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
16408 buffer_1.clone(),
16409 vec![
16410 Point::row_range(0..3),
16411 Point::row_range(1..6),
16412 Point::row_range(12..15),
16413 ],
16414 0,
16415 cx,
16416 );
16417 multibuffer.set_excerpts_for_path(
16418 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
16419 buffer_2.clone(),
16420 vec![Point::row_range(0..6), Point::row_range(8..12)],
16421 0,
16422 cx,
16423 );
16424 });
16425 });
16426
16427 // Apply the update of adding the excerpts.
16428 follower_1
16429 .update_in(cx, |follower, window, cx| {
16430 follower.apply_update_proto(
16431 &project,
16432 update_message.borrow().clone().unwrap(),
16433 window,
16434 cx,
16435 )
16436 })
16437 .await
16438 .unwrap();
16439 assert_eq!(
16440 follower_1.update(cx, |editor, cx| editor.text(cx)),
16441 leader.update(cx, |editor, cx| editor.text(cx))
16442 );
16443 update_message.borrow_mut().take();
16444
16445 // Start following separately after it already has excerpts.
16446 let mut state_message =
16447 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16448 let workspace_entity = workspace.root(cx).unwrap();
16449 let follower_2 = cx
16450 .update_window(*workspace.deref(), |_, window, cx| {
16451 Editor::from_state_proto(
16452 workspace_entity,
16453 ViewId {
16454 creator: CollaboratorId::PeerId(PeerId::default()),
16455 id: 0,
16456 },
16457 &mut state_message,
16458 window,
16459 cx,
16460 )
16461 })
16462 .unwrap()
16463 .unwrap()
16464 .await
16465 .unwrap();
16466 assert_eq!(
16467 follower_2.update(cx, |editor, cx| editor.text(cx)),
16468 leader.update(cx, |editor, cx| editor.text(cx))
16469 );
16470
16471 // Remove some excerpts.
16472 leader.update(cx, |leader, cx| {
16473 leader.buffer.update(cx, |multibuffer, cx| {
16474 let excerpt_ids = multibuffer.excerpt_ids();
16475 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
16476 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
16477 });
16478 });
16479
16480 // Apply the update of removing the excerpts.
16481 follower_1
16482 .update_in(cx, |follower, window, cx| {
16483 follower.apply_update_proto(
16484 &project,
16485 update_message.borrow().clone().unwrap(),
16486 window,
16487 cx,
16488 )
16489 })
16490 .await
16491 .unwrap();
16492 follower_2
16493 .update_in(cx, |follower, window, cx| {
16494 follower.apply_update_proto(
16495 &project,
16496 update_message.borrow().clone().unwrap(),
16497 window,
16498 cx,
16499 )
16500 })
16501 .await
16502 .unwrap();
16503 update_message.borrow_mut().take();
16504 assert_eq!(
16505 follower_1.update(cx, |editor, cx| editor.text(cx)),
16506 leader.update(cx, |editor, cx| editor.text(cx))
16507 );
16508}
16509
16510#[gpui::test]
16511async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16512 init_test(cx, |_| {});
16513
16514 let mut cx = EditorTestContext::new(cx).await;
16515 let lsp_store =
16516 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
16517
16518 cx.set_state(indoc! {"
16519 ˇfn func(abc def: i32) -> u32 {
16520 }
16521 "});
16522
16523 cx.update(|_, cx| {
16524 lsp_store.update(cx, |lsp_store, cx| {
16525 lsp_store
16526 .update_diagnostics(
16527 LanguageServerId(0),
16528 lsp::PublishDiagnosticsParams {
16529 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
16530 version: None,
16531 diagnostics: vec![
16532 lsp::Diagnostic {
16533 range: lsp::Range::new(
16534 lsp::Position::new(0, 11),
16535 lsp::Position::new(0, 12),
16536 ),
16537 severity: Some(lsp::DiagnosticSeverity::ERROR),
16538 ..Default::default()
16539 },
16540 lsp::Diagnostic {
16541 range: lsp::Range::new(
16542 lsp::Position::new(0, 12),
16543 lsp::Position::new(0, 15),
16544 ),
16545 severity: Some(lsp::DiagnosticSeverity::ERROR),
16546 ..Default::default()
16547 },
16548 lsp::Diagnostic {
16549 range: lsp::Range::new(
16550 lsp::Position::new(0, 25),
16551 lsp::Position::new(0, 28),
16552 ),
16553 severity: Some(lsp::DiagnosticSeverity::ERROR),
16554 ..Default::default()
16555 },
16556 ],
16557 },
16558 None,
16559 DiagnosticSourceKind::Pushed,
16560 &[],
16561 cx,
16562 )
16563 .unwrap()
16564 });
16565 });
16566
16567 executor.run_until_parked();
16568
16569 cx.update_editor(|editor, window, cx| {
16570 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16571 });
16572
16573 cx.assert_editor_state(indoc! {"
16574 fn func(abc def: i32) -> ˇu32 {
16575 }
16576 "});
16577
16578 cx.update_editor(|editor, window, cx| {
16579 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16580 });
16581
16582 cx.assert_editor_state(indoc! {"
16583 fn func(abc ˇdef: i32) -> u32 {
16584 }
16585 "});
16586
16587 cx.update_editor(|editor, window, cx| {
16588 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16589 });
16590
16591 cx.assert_editor_state(indoc! {"
16592 fn func(abcˇ def: i32) -> u32 {
16593 }
16594 "});
16595
16596 cx.update_editor(|editor, window, cx| {
16597 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16598 });
16599
16600 cx.assert_editor_state(indoc! {"
16601 fn func(abc def: i32) -> ˇu32 {
16602 }
16603 "});
16604}
16605
16606#[gpui::test]
16607async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16608 init_test(cx, |_| {});
16609
16610 let mut cx = EditorTestContext::new(cx).await;
16611
16612 let diff_base = r#"
16613 use some::mod;
16614
16615 const A: u32 = 42;
16616
16617 fn main() {
16618 println!("hello");
16619
16620 println!("world");
16621 }
16622 "#
16623 .unindent();
16624
16625 // Edits are modified, removed, modified, added
16626 cx.set_state(
16627 &r#"
16628 use some::modified;
16629
16630 ˇ
16631 fn main() {
16632 println!("hello there");
16633
16634 println!("around the");
16635 println!("world");
16636 }
16637 "#
16638 .unindent(),
16639 );
16640
16641 cx.set_head_text(&diff_base);
16642 executor.run_until_parked();
16643
16644 cx.update_editor(|editor, window, cx| {
16645 //Wrap around the bottom of the buffer
16646 for _ in 0..3 {
16647 editor.go_to_next_hunk(&GoToHunk, window, cx);
16648 }
16649 });
16650
16651 cx.assert_editor_state(
16652 &r#"
16653 ˇuse some::modified;
16654
16655
16656 fn main() {
16657 println!("hello there");
16658
16659 println!("around the");
16660 println!("world");
16661 }
16662 "#
16663 .unindent(),
16664 );
16665
16666 cx.update_editor(|editor, window, cx| {
16667 //Wrap around the top of the buffer
16668 for _ in 0..2 {
16669 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16670 }
16671 });
16672
16673 cx.assert_editor_state(
16674 &r#"
16675 use some::modified;
16676
16677
16678 fn main() {
16679 ˇ println!("hello there");
16680
16681 println!("around the");
16682 println!("world");
16683 }
16684 "#
16685 .unindent(),
16686 );
16687
16688 cx.update_editor(|editor, window, cx| {
16689 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16690 });
16691
16692 cx.assert_editor_state(
16693 &r#"
16694 use some::modified;
16695
16696 ˇ
16697 fn main() {
16698 println!("hello there");
16699
16700 println!("around the");
16701 println!("world");
16702 }
16703 "#
16704 .unindent(),
16705 );
16706
16707 cx.update_editor(|editor, window, cx| {
16708 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16709 });
16710
16711 cx.assert_editor_state(
16712 &r#"
16713 ˇuse some::modified;
16714
16715
16716 fn main() {
16717 println!("hello there");
16718
16719 println!("around the");
16720 println!("world");
16721 }
16722 "#
16723 .unindent(),
16724 );
16725
16726 cx.update_editor(|editor, window, cx| {
16727 for _ in 0..2 {
16728 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16729 }
16730 });
16731
16732 cx.assert_editor_state(
16733 &r#"
16734 use some::modified;
16735
16736
16737 fn main() {
16738 ˇ println!("hello there");
16739
16740 println!("around the");
16741 println!("world");
16742 }
16743 "#
16744 .unindent(),
16745 );
16746
16747 cx.update_editor(|editor, window, cx| {
16748 editor.fold(&Fold, window, cx);
16749 });
16750
16751 cx.update_editor(|editor, window, cx| {
16752 editor.go_to_next_hunk(&GoToHunk, window, cx);
16753 });
16754
16755 cx.assert_editor_state(
16756 &r#"
16757 ˇuse some::modified;
16758
16759
16760 fn main() {
16761 println!("hello there");
16762
16763 println!("around the");
16764 println!("world");
16765 }
16766 "#
16767 .unindent(),
16768 );
16769}
16770
16771#[test]
16772fn test_split_words() {
16773 fn split(text: &str) -> Vec<&str> {
16774 split_words(text).collect()
16775 }
16776
16777 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
16778 assert_eq!(split("hello_world"), &["hello_", "world"]);
16779 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
16780 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
16781 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
16782 assert_eq!(split("helloworld"), &["helloworld"]);
16783
16784 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
16785}
16786
16787#[gpui::test]
16788async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
16789 init_test(cx, |_| {});
16790
16791 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
16792 let mut assert = |before, after| {
16793 let _state_context = cx.set_state(before);
16794 cx.run_until_parked();
16795 cx.update_editor(|editor, window, cx| {
16796 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
16797 });
16798 cx.run_until_parked();
16799 cx.assert_editor_state(after);
16800 };
16801
16802 // Outside bracket jumps to outside of matching bracket
16803 assert("console.logˇ(var);", "console.log(var)ˇ;");
16804 assert("console.log(var)ˇ;", "console.logˇ(var);");
16805
16806 // Inside bracket jumps to inside of matching bracket
16807 assert("console.log(ˇvar);", "console.log(varˇ);");
16808 assert("console.log(varˇ);", "console.log(ˇvar);");
16809
16810 // When outside a bracket and inside, favor jumping to the inside bracket
16811 assert(
16812 "console.log('foo', [1, 2, 3]ˇ);",
16813 "console.log(ˇ'foo', [1, 2, 3]);",
16814 );
16815 assert(
16816 "console.log(ˇ'foo', [1, 2, 3]);",
16817 "console.log('foo', [1, 2, 3]ˇ);",
16818 );
16819
16820 // Bias forward if two options are equally likely
16821 assert(
16822 "let result = curried_fun()ˇ();",
16823 "let result = curried_fun()()ˇ;",
16824 );
16825
16826 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
16827 assert(
16828 indoc! {"
16829 function test() {
16830 console.log('test')ˇ
16831 }"},
16832 indoc! {"
16833 function test() {
16834 console.logˇ('test')
16835 }"},
16836 );
16837}
16838
16839#[gpui::test]
16840async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
16841 init_test(cx, |_| {});
16842
16843 let fs = FakeFs::new(cx.executor());
16844 fs.insert_tree(
16845 path!("/a"),
16846 json!({
16847 "main.rs": "fn main() { let a = 5; }",
16848 "other.rs": "// Test file",
16849 }),
16850 )
16851 .await;
16852 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16853
16854 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
16855 language_registry.add(Arc::new(Language::new(
16856 LanguageConfig {
16857 name: "Rust".into(),
16858 matcher: LanguageMatcher {
16859 path_suffixes: vec!["rs".to_string()],
16860 ..Default::default()
16861 },
16862 brackets: BracketPairConfig {
16863 pairs: vec![BracketPair {
16864 start: "{".to_string(),
16865 end: "}".to_string(),
16866 close: true,
16867 surround: true,
16868 newline: true,
16869 }],
16870 disabled_scopes_by_bracket_ix: Vec::new(),
16871 },
16872 ..Default::default()
16873 },
16874 Some(tree_sitter_rust::LANGUAGE.into()),
16875 )));
16876 let mut fake_servers = language_registry.register_fake_lsp(
16877 "Rust",
16878 FakeLspAdapter {
16879 capabilities: lsp::ServerCapabilities {
16880 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
16881 first_trigger_character: "{".to_string(),
16882 more_trigger_character: None,
16883 }),
16884 ..Default::default()
16885 },
16886 ..Default::default()
16887 },
16888 );
16889
16890 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16891
16892 let cx = &mut VisualTestContext::from_window(*workspace, cx);
16893
16894 let worktree_id = workspace
16895 .update(cx, |workspace, _, cx| {
16896 workspace.project().update(cx, |project, cx| {
16897 project.worktrees(cx).next().unwrap().read(cx).id()
16898 })
16899 })
16900 .unwrap();
16901
16902 let buffer = project
16903 .update(cx, |project, cx| {
16904 project.open_local_buffer(path!("/a/main.rs"), cx)
16905 })
16906 .await
16907 .unwrap();
16908 let editor_handle = workspace
16909 .update(cx, |workspace, window, cx| {
16910 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
16911 })
16912 .unwrap()
16913 .await
16914 .unwrap()
16915 .downcast::<Editor>()
16916 .unwrap();
16917
16918 cx.executor().start_waiting();
16919 let fake_server = fake_servers.next().await.unwrap();
16920
16921 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
16922 |params, _| async move {
16923 assert_eq!(
16924 params.text_document_position.text_document.uri,
16925 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
16926 );
16927 assert_eq!(
16928 params.text_document_position.position,
16929 lsp::Position::new(0, 21),
16930 );
16931
16932 Ok(Some(vec![lsp::TextEdit {
16933 new_text: "]".to_string(),
16934 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
16935 }]))
16936 },
16937 );
16938
16939 editor_handle.update_in(cx, |editor, window, cx| {
16940 window.focus(&editor.focus_handle(cx));
16941 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16942 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
16943 });
16944 editor.handle_input("{", window, cx);
16945 });
16946
16947 cx.executor().run_until_parked();
16948
16949 buffer.update(cx, |buffer, _| {
16950 assert_eq!(
16951 buffer.text(),
16952 "fn main() { let a = {5}; }",
16953 "No extra braces from on type formatting should appear in the buffer"
16954 )
16955 });
16956}
16957
16958#[gpui::test(iterations = 20, seeds(31))]
16959async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
16960 init_test(cx, |_| {});
16961
16962 let mut cx = EditorLspTestContext::new_rust(
16963 lsp::ServerCapabilities {
16964 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
16965 first_trigger_character: ".".to_string(),
16966 more_trigger_character: None,
16967 }),
16968 ..Default::default()
16969 },
16970 cx,
16971 )
16972 .await;
16973
16974 cx.update_buffer(|buffer, _| {
16975 // This causes autoindent to be async.
16976 buffer.set_sync_parse_timeout(Duration::ZERO)
16977 });
16978
16979 cx.set_state("fn c() {\n d()ˇ\n}\n");
16980 cx.simulate_keystroke("\n");
16981 cx.run_until_parked();
16982
16983 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
16984 let mut request =
16985 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
16986 let buffer_cloned = buffer_cloned.clone();
16987 async move {
16988 buffer_cloned.update(&mut cx, |buffer, _| {
16989 assert_eq!(
16990 buffer.text(),
16991 "fn c() {\n d()\n .\n}\n",
16992 "OnTypeFormatting should triggered after autoindent applied"
16993 )
16994 })?;
16995
16996 Ok(Some(vec![]))
16997 }
16998 });
16999
17000 cx.simulate_keystroke(".");
17001 cx.run_until_parked();
17002
17003 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
17004 assert!(request.next().await.is_some());
17005 request.close();
17006 assert!(request.next().await.is_none());
17007}
17008
17009#[gpui::test]
17010async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
17011 init_test(cx, |_| {});
17012
17013 let fs = FakeFs::new(cx.executor());
17014 fs.insert_tree(
17015 path!("/a"),
17016 json!({
17017 "main.rs": "fn main() { let a = 5; }",
17018 "other.rs": "// Test file",
17019 }),
17020 )
17021 .await;
17022
17023 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17024
17025 let server_restarts = Arc::new(AtomicUsize::new(0));
17026 let closure_restarts = Arc::clone(&server_restarts);
17027 let language_server_name = "test language server";
17028 let language_name: LanguageName = "Rust".into();
17029
17030 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17031 language_registry.add(Arc::new(Language::new(
17032 LanguageConfig {
17033 name: language_name.clone(),
17034 matcher: LanguageMatcher {
17035 path_suffixes: vec!["rs".to_string()],
17036 ..Default::default()
17037 },
17038 ..Default::default()
17039 },
17040 Some(tree_sitter_rust::LANGUAGE.into()),
17041 )));
17042 let mut fake_servers = language_registry.register_fake_lsp(
17043 "Rust",
17044 FakeLspAdapter {
17045 name: language_server_name,
17046 initialization_options: Some(json!({
17047 "testOptionValue": true
17048 })),
17049 initializer: Some(Box::new(move |fake_server| {
17050 let task_restarts = Arc::clone(&closure_restarts);
17051 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
17052 task_restarts.fetch_add(1, atomic::Ordering::Release);
17053 futures::future::ready(Ok(()))
17054 });
17055 })),
17056 ..Default::default()
17057 },
17058 );
17059
17060 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17061 let _buffer = project
17062 .update(cx, |project, cx| {
17063 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
17064 })
17065 .await
17066 .unwrap();
17067 let _fake_server = fake_servers.next().await.unwrap();
17068 update_test_language_settings(cx, |language_settings| {
17069 language_settings.languages.0.insert(
17070 language_name.clone(),
17071 LanguageSettingsContent {
17072 tab_size: NonZeroU32::new(8),
17073 ..Default::default()
17074 },
17075 );
17076 });
17077 cx.executor().run_until_parked();
17078 assert_eq!(
17079 server_restarts.load(atomic::Ordering::Acquire),
17080 0,
17081 "Should not restart LSP server on an unrelated change"
17082 );
17083
17084 update_test_project_settings(cx, |project_settings| {
17085 project_settings.lsp.insert(
17086 "Some other server name".into(),
17087 LspSettings {
17088 binary: None,
17089 settings: None,
17090 initialization_options: Some(json!({
17091 "some other init value": false
17092 })),
17093 enable_lsp_tasks: false,
17094 fetch: None,
17095 },
17096 );
17097 });
17098 cx.executor().run_until_parked();
17099 assert_eq!(
17100 server_restarts.load(atomic::Ordering::Acquire),
17101 0,
17102 "Should not restart LSP server on an unrelated LSP settings change"
17103 );
17104
17105 update_test_project_settings(cx, |project_settings| {
17106 project_settings.lsp.insert(
17107 language_server_name.into(),
17108 LspSettings {
17109 binary: None,
17110 settings: None,
17111 initialization_options: Some(json!({
17112 "anotherInitValue": false
17113 })),
17114 enable_lsp_tasks: false,
17115 fetch: None,
17116 },
17117 );
17118 });
17119 cx.executor().run_until_parked();
17120 assert_eq!(
17121 server_restarts.load(atomic::Ordering::Acquire),
17122 1,
17123 "Should restart LSP server on a related LSP settings change"
17124 );
17125
17126 update_test_project_settings(cx, |project_settings| {
17127 project_settings.lsp.insert(
17128 language_server_name.into(),
17129 LspSettings {
17130 binary: None,
17131 settings: None,
17132 initialization_options: Some(json!({
17133 "anotherInitValue": false
17134 })),
17135 enable_lsp_tasks: false,
17136 fetch: None,
17137 },
17138 );
17139 });
17140 cx.executor().run_until_parked();
17141 assert_eq!(
17142 server_restarts.load(atomic::Ordering::Acquire),
17143 1,
17144 "Should not restart LSP server on a related LSP settings change that is the same"
17145 );
17146
17147 update_test_project_settings(cx, |project_settings| {
17148 project_settings.lsp.insert(
17149 language_server_name.into(),
17150 LspSettings {
17151 binary: None,
17152 settings: None,
17153 initialization_options: None,
17154 enable_lsp_tasks: false,
17155 fetch: None,
17156 },
17157 );
17158 });
17159 cx.executor().run_until_parked();
17160 assert_eq!(
17161 server_restarts.load(atomic::Ordering::Acquire),
17162 2,
17163 "Should restart LSP server on another related LSP settings change"
17164 );
17165}
17166
17167#[gpui::test]
17168async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
17169 init_test(cx, |_| {});
17170
17171 let mut cx = EditorLspTestContext::new_rust(
17172 lsp::ServerCapabilities {
17173 completion_provider: Some(lsp::CompletionOptions {
17174 trigger_characters: Some(vec![".".to_string()]),
17175 resolve_provider: Some(true),
17176 ..Default::default()
17177 }),
17178 ..Default::default()
17179 },
17180 cx,
17181 )
17182 .await;
17183
17184 cx.set_state("fn main() { let a = 2ˇ; }");
17185 cx.simulate_keystroke(".");
17186 let completion_item = lsp::CompletionItem {
17187 label: "some".into(),
17188 kind: Some(lsp::CompletionItemKind::SNIPPET),
17189 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
17190 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
17191 kind: lsp::MarkupKind::Markdown,
17192 value: "```rust\nSome(2)\n```".to_string(),
17193 })),
17194 deprecated: Some(false),
17195 sort_text: Some("fffffff2".to_string()),
17196 filter_text: Some("some".to_string()),
17197 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
17198 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17199 range: lsp::Range {
17200 start: lsp::Position {
17201 line: 0,
17202 character: 22,
17203 },
17204 end: lsp::Position {
17205 line: 0,
17206 character: 22,
17207 },
17208 },
17209 new_text: "Some(2)".to_string(),
17210 })),
17211 additional_text_edits: Some(vec![lsp::TextEdit {
17212 range: lsp::Range {
17213 start: lsp::Position {
17214 line: 0,
17215 character: 20,
17216 },
17217 end: lsp::Position {
17218 line: 0,
17219 character: 22,
17220 },
17221 },
17222 new_text: "".to_string(),
17223 }]),
17224 ..Default::default()
17225 };
17226
17227 let closure_completion_item = completion_item.clone();
17228 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17229 let task_completion_item = closure_completion_item.clone();
17230 async move {
17231 Ok(Some(lsp::CompletionResponse::Array(vec![
17232 task_completion_item,
17233 ])))
17234 }
17235 });
17236
17237 request.next().await;
17238
17239 cx.condition(|editor, _| editor.context_menu_visible())
17240 .await;
17241 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
17242 editor
17243 .confirm_completion(&ConfirmCompletion::default(), window, cx)
17244 .unwrap()
17245 });
17246 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
17247
17248 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
17249 let task_completion_item = completion_item.clone();
17250 async move { Ok(task_completion_item) }
17251 })
17252 .next()
17253 .await
17254 .unwrap();
17255 apply_additional_edits.await.unwrap();
17256 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
17257}
17258
17259#[gpui::test]
17260async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
17261 init_test(cx, |_| {});
17262
17263 let mut cx = EditorLspTestContext::new_rust(
17264 lsp::ServerCapabilities {
17265 completion_provider: Some(lsp::CompletionOptions {
17266 trigger_characters: Some(vec![".".to_string()]),
17267 resolve_provider: Some(true),
17268 ..Default::default()
17269 }),
17270 ..Default::default()
17271 },
17272 cx,
17273 )
17274 .await;
17275
17276 cx.set_state("fn main() { let a = 2ˇ; }");
17277 cx.simulate_keystroke(".");
17278
17279 let item1 = lsp::CompletionItem {
17280 label: "method id()".to_string(),
17281 filter_text: Some("id".to_string()),
17282 detail: None,
17283 documentation: None,
17284 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17285 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17286 new_text: ".id".to_string(),
17287 })),
17288 ..lsp::CompletionItem::default()
17289 };
17290
17291 let item2 = lsp::CompletionItem {
17292 label: "other".to_string(),
17293 filter_text: Some("other".to_string()),
17294 detail: None,
17295 documentation: None,
17296 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17297 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17298 new_text: ".other".to_string(),
17299 })),
17300 ..lsp::CompletionItem::default()
17301 };
17302
17303 let item1 = item1.clone();
17304 cx.set_request_handler::<lsp::request::Completion, _, _>({
17305 let item1 = item1.clone();
17306 move |_, _, _| {
17307 let item1 = item1.clone();
17308 let item2 = item2.clone();
17309 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
17310 }
17311 })
17312 .next()
17313 .await;
17314
17315 cx.condition(|editor, _| editor.context_menu_visible())
17316 .await;
17317 cx.update_editor(|editor, _, _| {
17318 let context_menu = editor.context_menu.borrow_mut();
17319 let context_menu = context_menu
17320 .as_ref()
17321 .expect("Should have the context menu deployed");
17322 match context_menu {
17323 CodeContextMenu::Completions(completions_menu) => {
17324 let completions = completions_menu.completions.borrow_mut();
17325 assert_eq!(
17326 completions
17327 .iter()
17328 .map(|completion| &completion.label.text)
17329 .collect::<Vec<_>>(),
17330 vec!["method id()", "other"]
17331 )
17332 }
17333 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17334 }
17335 });
17336
17337 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
17338 let item1 = item1.clone();
17339 move |_, item_to_resolve, _| {
17340 let item1 = item1.clone();
17341 async move {
17342 if item1 == item_to_resolve {
17343 Ok(lsp::CompletionItem {
17344 label: "method id()".to_string(),
17345 filter_text: Some("id".to_string()),
17346 detail: Some("Now resolved!".to_string()),
17347 documentation: Some(lsp::Documentation::String("Docs".to_string())),
17348 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17349 range: lsp::Range::new(
17350 lsp::Position::new(0, 22),
17351 lsp::Position::new(0, 22),
17352 ),
17353 new_text: ".id".to_string(),
17354 })),
17355 ..lsp::CompletionItem::default()
17356 })
17357 } else {
17358 Ok(item_to_resolve)
17359 }
17360 }
17361 }
17362 })
17363 .next()
17364 .await
17365 .unwrap();
17366 cx.run_until_parked();
17367
17368 cx.update_editor(|editor, window, cx| {
17369 editor.context_menu_next(&Default::default(), window, cx);
17370 });
17371
17372 cx.update_editor(|editor, _, _| {
17373 let context_menu = editor.context_menu.borrow_mut();
17374 let context_menu = context_menu
17375 .as_ref()
17376 .expect("Should have the context menu deployed");
17377 match context_menu {
17378 CodeContextMenu::Completions(completions_menu) => {
17379 let completions = completions_menu.completions.borrow_mut();
17380 assert_eq!(
17381 completions
17382 .iter()
17383 .map(|completion| &completion.label.text)
17384 .collect::<Vec<_>>(),
17385 vec!["method id() Now resolved!", "other"],
17386 "Should update first completion label, but not second as the filter text did not match."
17387 );
17388 }
17389 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17390 }
17391 });
17392}
17393
17394#[gpui::test]
17395async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
17396 init_test(cx, |_| {});
17397 let mut cx = EditorLspTestContext::new_rust(
17398 lsp::ServerCapabilities {
17399 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
17400 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
17401 completion_provider: Some(lsp::CompletionOptions {
17402 resolve_provider: Some(true),
17403 ..Default::default()
17404 }),
17405 ..Default::default()
17406 },
17407 cx,
17408 )
17409 .await;
17410 cx.set_state(indoc! {"
17411 struct TestStruct {
17412 field: i32
17413 }
17414
17415 fn mainˇ() {
17416 let unused_var = 42;
17417 let test_struct = TestStruct { field: 42 };
17418 }
17419 "});
17420 let symbol_range = cx.lsp_range(indoc! {"
17421 struct TestStruct {
17422 field: i32
17423 }
17424
17425 «fn main»() {
17426 let unused_var = 42;
17427 let test_struct = TestStruct { field: 42 };
17428 }
17429 "});
17430 let mut hover_requests =
17431 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
17432 Ok(Some(lsp::Hover {
17433 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
17434 kind: lsp::MarkupKind::Markdown,
17435 value: "Function documentation".to_string(),
17436 }),
17437 range: Some(symbol_range),
17438 }))
17439 });
17440
17441 // Case 1: Test that code action menu hide hover popover
17442 cx.dispatch_action(Hover);
17443 hover_requests.next().await;
17444 cx.condition(|editor, _| editor.hover_state.visible()).await;
17445 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
17446 move |_, _, _| async move {
17447 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
17448 lsp::CodeAction {
17449 title: "Remove unused variable".to_string(),
17450 kind: Some(CodeActionKind::QUICKFIX),
17451 edit: Some(lsp::WorkspaceEdit {
17452 changes: Some(
17453 [(
17454 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
17455 vec![lsp::TextEdit {
17456 range: lsp::Range::new(
17457 lsp::Position::new(5, 4),
17458 lsp::Position::new(5, 27),
17459 ),
17460 new_text: "".to_string(),
17461 }],
17462 )]
17463 .into_iter()
17464 .collect(),
17465 ),
17466 ..Default::default()
17467 }),
17468 ..Default::default()
17469 },
17470 )]))
17471 },
17472 );
17473 cx.update_editor(|editor, window, cx| {
17474 editor.toggle_code_actions(
17475 &ToggleCodeActions {
17476 deployed_from: None,
17477 quick_launch: false,
17478 },
17479 window,
17480 cx,
17481 );
17482 });
17483 code_action_requests.next().await;
17484 cx.run_until_parked();
17485 cx.condition(|editor, _| editor.context_menu_visible())
17486 .await;
17487 cx.update_editor(|editor, _, _| {
17488 assert!(
17489 !editor.hover_state.visible(),
17490 "Hover popover should be hidden when code action menu is shown"
17491 );
17492 // Hide code actions
17493 editor.context_menu.take();
17494 });
17495
17496 // Case 2: Test that code completions hide hover popover
17497 cx.dispatch_action(Hover);
17498 hover_requests.next().await;
17499 cx.condition(|editor, _| editor.hover_state.visible()).await;
17500 let counter = Arc::new(AtomicUsize::new(0));
17501 let mut completion_requests =
17502 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17503 let counter = counter.clone();
17504 async move {
17505 counter.fetch_add(1, atomic::Ordering::Release);
17506 Ok(Some(lsp::CompletionResponse::Array(vec![
17507 lsp::CompletionItem {
17508 label: "main".into(),
17509 kind: Some(lsp::CompletionItemKind::FUNCTION),
17510 detail: Some("() -> ()".to_string()),
17511 ..Default::default()
17512 },
17513 lsp::CompletionItem {
17514 label: "TestStruct".into(),
17515 kind: Some(lsp::CompletionItemKind::STRUCT),
17516 detail: Some("struct TestStruct".to_string()),
17517 ..Default::default()
17518 },
17519 ])))
17520 }
17521 });
17522 cx.update_editor(|editor, window, cx| {
17523 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
17524 });
17525 completion_requests.next().await;
17526 cx.condition(|editor, _| editor.context_menu_visible())
17527 .await;
17528 cx.update_editor(|editor, _, _| {
17529 assert!(
17530 !editor.hover_state.visible(),
17531 "Hover popover should be hidden when completion menu is shown"
17532 );
17533 });
17534}
17535
17536#[gpui::test]
17537async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
17538 init_test(cx, |_| {});
17539
17540 let mut cx = EditorLspTestContext::new_rust(
17541 lsp::ServerCapabilities {
17542 completion_provider: Some(lsp::CompletionOptions {
17543 trigger_characters: Some(vec![".".to_string()]),
17544 resolve_provider: Some(true),
17545 ..Default::default()
17546 }),
17547 ..Default::default()
17548 },
17549 cx,
17550 )
17551 .await;
17552
17553 cx.set_state("fn main() { let a = 2ˇ; }");
17554 cx.simulate_keystroke(".");
17555
17556 let unresolved_item_1 = lsp::CompletionItem {
17557 label: "id".to_string(),
17558 filter_text: Some("id".to_string()),
17559 detail: None,
17560 documentation: None,
17561 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17562 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17563 new_text: ".id".to_string(),
17564 })),
17565 ..lsp::CompletionItem::default()
17566 };
17567 let resolved_item_1 = lsp::CompletionItem {
17568 additional_text_edits: Some(vec![lsp::TextEdit {
17569 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17570 new_text: "!!".to_string(),
17571 }]),
17572 ..unresolved_item_1.clone()
17573 };
17574 let unresolved_item_2 = lsp::CompletionItem {
17575 label: "other".to_string(),
17576 filter_text: Some("other".to_string()),
17577 detail: None,
17578 documentation: None,
17579 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17580 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17581 new_text: ".other".to_string(),
17582 })),
17583 ..lsp::CompletionItem::default()
17584 };
17585 let resolved_item_2 = lsp::CompletionItem {
17586 additional_text_edits: Some(vec![lsp::TextEdit {
17587 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17588 new_text: "??".to_string(),
17589 }]),
17590 ..unresolved_item_2.clone()
17591 };
17592
17593 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
17594 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
17595 cx.lsp
17596 .server
17597 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
17598 let unresolved_item_1 = unresolved_item_1.clone();
17599 let resolved_item_1 = resolved_item_1.clone();
17600 let unresolved_item_2 = unresolved_item_2.clone();
17601 let resolved_item_2 = resolved_item_2.clone();
17602 let resolve_requests_1 = resolve_requests_1.clone();
17603 let resolve_requests_2 = resolve_requests_2.clone();
17604 move |unresolved_request, _| {
17605 let unresolved_item_1 = unresolved_item_1.clone();
17606 let resolved_item_1 = resolved_item_1.clone();
17607 let unresolved_item_2 = unresolved_item_2.clone();
17608 let resolved_item_2 = resolved_item_2.clone();
17609 let resolve_requests_1 = resolve_requests_1.clone();
17610 let resolve_requests_2 = resolve_requests_2.clone();
17611 async move {
17612 if unresolved_request == unresolved_item_1 {
17613 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
17614 Ok(resolved_item_1.clone())
17615 } else if unresolved_request == unresolved_item_2 {
17616 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
17617 Ok(resolved_item_2.clone())
17618 } else {
17619 panic!("Unexpected completion item {unresolved_request:?}")
17620 }
17621 }
17622 }
17623 })
17624 .detach();
17625
17626 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17627 let unresolved_item_1 = unresolved_item_1.clone();
17628 let unresolved_item_2 = unresolved_item_2.clone();
17629 async move {
17630 Ok(Some(lsp::CompletionResponse::Array(vec![
17631 unresolved_item_1,
17632 unresolved_item_2,
17633 ])))
17634 }
17635 })
17636 .next()
17637 .await;
17638
17639 cx.condition(|editor, _| editor.context_menu_visible())
17640 .await;
17641 cx.update_editor(|editor, _, _| {
17642 let context_menu = editor.context_menu.borrow_mut();
17643 let context_menu = context_menu
17644 .as_ref()
17645 .expect("Should have the context menu deployed");
17646 match context_menu {
17647 CodeContextMenu::Completions(completions_menu) => {
17648 let completions = completions_menu.completions.borrow_mut();
17649 assert_eq!(
17650 completions
17651 .iter()
17652 .map(|completion| &completion.label.text)
17653 .collect::<Vec<_>>(),
17654 vec!["id", "other"]
17655 )
17656 }
17657 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17658 }
17659 });
17660 cx.run_until_parked();
17661
17662 cx.update_editor(|editor, window, cx| {
17663 editor.context_menu_next(&ContextMenuNext, window, cx);
17664 });
17665 cx.run_until_parked();
17666 cx.update_editor(|editor, window, cx| {
17667 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
17668 });
17669 cx.run_until_parked();
17670 cx.update_editor(|editor, window, cx| {
17671 editor.context_menu_next(&ContextMenuNext, window, cx);
17672 });
17673 cx.run_until_parked();
17674 cx.update_editor(|editor, window, cx| {
17675 editor
17676 .compose_completion(&ComposeCompletion::default(), window, cx)
17677 .expect("No task returned")
17678 })
17679 .await
17680 .expect("Completion failed");
17681 cx.run_until_parked();
17682
17683 cx.update_editor(|editor, _, cx| {
17684 assert_eq!(
17685 resolve_requests_1.load(atomic::Ordering::Acquire),
17686 1,
17687 "Should always resolve once despite multiple selections"
17688 );
17689 assert_eq!(
17690 resolve_requests_2.load(atomic::Ordering::Acquire),
17691 1,
17692 "Should always resolve once after multiple selections and applying the completion"
17693 );
17694 assert_eq!(
17695 editor.text(cx),
17696 "fn main() { let a = ??.other; }",
17697 "Should use resolved data when applying the completion"
17698 );
17699 });
17700}
17701
17702#[gpui::test]
17703async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
17704 init_test(cx, |_| {});
17705
17706 let item_0 = lsp::CompletionItem {
17707 label: "abs".into(),
17708 insert_text: Some("abs".into()),
17709 data: Some(json!({ "very": "special"})),
17710 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
17711 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
17712 lsp::InsertReplaceEdit {
17713 new_text: "abs".to_string(),
17714 insert: lsp::Range::default(),
17715 replace: lsp::Range::default(),
17716 },
17717 )),
17718 ..lsp::CompletionItem::default()
17719 };
17720 let items = iter::once(item_0.clone())
17721 .chain((11..51).map(|i| lsp::CompletionItem {
17722 label: format!("item_{}", i),
17723 insert_text: Some(format!("item_{}", i)),
17724 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
17725 ..lsp::CompletionItem::default()
17726 }))
17727 .collect::<Vec<_>>();
17728
17729 let default_commit_characters = vec!["?".to_string()];
17730 let default_data = json!({ "default": "data"});
17731 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
17732 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
17733 let default_edit_range = lsp::Range {
17734 start: lsp::Position {
17735 line: 0,
17736 character: 5,
17737 },
17738 end: lsp::Position {
17739 line: 0,
17740 character: 5,
17741 },
17742 };
17743
17744 let mut cx = EditorLspTestContext::new_rust(
17745 lsp::ServerCapabilities {
17746 completion_provider: Some(lsp::CompletionOptions {
17747 trigger_characters: Some(vec![".".to_string()]),
17748 resolve_provider: Some(true),
17749 ..Default::default()
17750 }),
17751 ..Default::default()
17752 },
17753 cx,
17754 )
17755 .await;
17756
17757 cx.set_state("fn main() { let a = 2ˇ; }");
17758 cx.simulate_keystroke(".");
17759
17760 let completion_data = default_data.clone();
17761 let completion_characters = default_commit_characters.clone();
17762 let completion_items = items.clone();
17763 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17764 let default_data = completion_data.clone();
17765 let default_commit_characters = completion_characters.clone();
17766 let items = completion_items.clone();
17767 async move {
17768 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
17769 items,
17770 item_defaults: Some(lsp::CompletionListItemDefaults {
17771 data: Some(default_data.clone()),
17772 commit_characters: Some(default_commit_characters.clone()),
17773 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
17774 default_edit_range,
17775 )),
17776 insert_text_format: Some(default_insert_text_format),
17777 insert_text_mode: Some(default_insert_text_mode),
17778 }),
17779 ..lsp::CompletionList::default()
17780 })))
17781 }
17782 })
17783 .next()
17784 .await;
17785
17786 let resolved_items = Arc::new(Mutex::new(Vec::new()));
17787 cx.lsp
17788 .server
17789 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
17790 let closure_resolved_items = resolved_items.clone();
17791 move |item_to_resolve, _| {
17792 let closure_resolved_items = closure_resolved_items.clone();
17793 async move {
17794 closure_resolved_items.lock().push(item_to_resolve.clone());
17795 Ok(item_to_resolve)
17796 }
17797 }
17798 })
17799 .detach();
17800
17801 cx.condition(|editor, _| editor.context_menu_visible())
17802 .await;
17803 cx.run_until_parked();
17804 cx.update_editor(|editor, _, _| {
17805 let menu = editor.context_menu.borrow_mut();
17806 match menu.as_ref().expect("should have the completions menu") {
17807 CodeContextMenu::Completions(completions_menu) => {
17808 assert_eq!(
17809 completions_menu
17810 .entries
17811 .borrow()
17812 .iter()
17813 .map(|mat| mat.string.clone())
17814 .collect::<Vec<String>>(),
17815 items
17816 .iter()
17817 .map(|completion| completion.label.clone())
17818 .collect::<Vec<String>>()
17819 );
17820 }
17821 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
17822 }
17823 });
17824 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
17825 // with 4 from the end.
17826 assert_eq!(
17827 *resolved_items.lock(),
17828 [&items[0..16], &items[items.len() - 4..items.len()]]
17829 .concat()
17830 .iter()
17831 .cloned()
17832 .map(|mut item| {
17833 if item.data.is_none() {
17834 item.data = Some(default_data.clone());
17835 }
17836 item
17837 })
17838 .collect::<Vec<lsp::CompletionItem>>(),
17839 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
17840 );
17841 resolved_items.lock().clear();
17842
17843 cx.update_editor(|editor, window, cx| {
17844 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
17845 });
17846 cx.run_until_parked();
17847 // Completions that have already been resolved are skipped.
17848 assert_eq!(
17849 *resolved_items.lock(),
17850 items[items.len() - 17..items.len() - 4]
17851 .iter()
17852 .cloned()
17853 .map(|mut item| {
17854 if item.data.is_none() {
17855 item.data = Some(default_data.clone());
17856 }
17857 item
17858 })
17859 .collect::<Vec<lsp::CompletionItem>>()
17860 );
17861 resolved_items.lock().clear();
17862}
17863
17864#[gpui::test]
17865async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
17866 init_test(cx, |_| {});
17867
17868 let mut cx = EditorLspTestContext::new(
17869 Language::new(
17870 LanguageConfig {
17871 matcher: LanguageMatcher {
17872 path_suffixes: vec!["jsx".into()],
17873 ..Default::default()
17874 },
17875 overrides: [(
17876 "element".into(),
17877 LanguageConfigOverride {
17878 completion_query_characters: Override::Set(['-'].into_iter().collect()),
17879 ..Default::default()
17880 },
17881 )]
17882 .into_iter()
17883 .collect(),
17884 ..Default::default()
17885 },
17886 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
17887 )
17888 .with_override_query("(jsx_self_closing_element) @element")
17889 .unwrap(),
17890 lsp::ServerCapabilities {
17891 completion_provider: Some(lsp::CompletionOptions {
17892 trigger_characters: Some(vec![":".to_string()]),
17893 ..Default::default()
17894 }),
17895 ..Default::default()
17896 },
17897 cx,
17898 )
17899 .await;
17900
17901 cx.lsp
17902 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
17903 Ok(Some(lsp::CompletionResponse::Array(vec![
17904 lsp::CompletionItem {
17905 label: "bg-blue".into(),
17906 ..Default::default()
17907 },
17908 lsp::CompletionItem {
17909 label: "bg-red".into(),
17910 ..Default::default()
17911 },
17912 lsp::CompletionItem {
17913 label: "bg-yellow".into(),
17914 ..Default::default()
17915 },
17916 ])))
17917 });
17918
17919 cx.set_state(r#"<p class="bgˇ" />"#);
17920
17921 // Trigger completion when typing a dash, because the dash is an extra
17922 // word character in the 'element' scope, which contains the cursor.
17923 cx.simulate_keystroke("-");
17924 cx.executor().run_until_parked();
17925 cx.update_editor(|editor, _, _| {
17926 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17927 {
17928 assert_eq!(
17929 completion_menu_entries(menu),
17930 &["bg-blue", "bg-red", "bg-yellow"]
17931 );
17932 } else {
17933 panic!("expected completion menu to be open");
17934 }
17935 });
17936
17937 cx.simulate_keystroke("l");
17938 cx.executor().run_until_parked();
17939 cx.update_editor(|editor, _, _| {
17940 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17941 {
17942 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
17943 } else {
17944 panic!("expected completion menu to be open");
17945 }
17946 });
17947
17948 // When filtering completions, consider the character after the '-' to
17949 // be the start of a subword.
17950 cx.set_state(r#"<p class="yelˇ" />"#);
17951 cx.simulate_keystroke("l");
17952 cx.executor().run_until_parked();
17953 cx.update_editor(|editor, _, _| {
17954 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17955 {
17956 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
17957 } else {
17958 panic!("expected completion menu to be open");
17959 }
17960 });
17961}
17962
17963fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
17964 let entries = menu.entries.borrow();
17965 entries.iter().map(|mat| mat.string.clone()).collect()
17966}
17967
17968#[gpui::test]
17969async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
17970 init_test(cx, |settings| {
17971 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
17972 Formatter::Prettier,
17973 )))
17974 });
17975
17976 let fs = FakeFs::new(cx.executor());
17977 fs.insert_file(path!("/file.ts"), Default::default()).await;
17978
17979 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
17980 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17981
17982 language_registry.add(Arc::new(Language::new(
17983 LanguageConfig {
17984 name: "TypeScript".into(),
17985 matcher: LanguageMatcher {
17986 path_suffixes: vec!["ts".to_string()],
17987 ..Default::default()
17988 },
17989 ..Default::default()
17990 },
17991 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
17992 )));
17993 update_test_language_settings(cx, |settings| {
17994 settings.defaults.prettier = Some(PrettierSettings {
17995 allowed: true,
17996 ..PrettierSettings::default()
17997 });
17998 });
17999
18000 let test_plugin = "test_plugin";
18001 let _ = language_registry.register_fake_lsp(
18002 "TypeScript",
18003 FakeLspAdapter {
18004 prettier_plugins: vec![test_plugin],
18005 ..Default::default()
18006 },
18007 );
18008
18009 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
18010 let buffer = project
18011 .update(cx, |project, cx| {
18012 project.open_local_buffer(path!("/file.ts"), cx)
18013 })
18014 .await
18015 .unwrap();
18016
18017 let buffer_text = "one\ntwo\nthree\n";
18018 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
18019 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
18020 editor.update_in(cx, |editor, window, cx| {
18021 editor.set_text(buffer_text, window, cx)
18022 });
18023
18024 editor
18025 .update_in(cx, |editor, window, cx| {
18026 editor.perform_format(
18027 project.clone(),
18028 FormatTrigger::Manual,
18029 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18030 window,
18031 cx,
18032 )
18033 })
18034 .unwrap()
18035 .await;
18036 assert_eq!(
18037 editor.update(cx, |editor, cx| editor.text(cx)),
18038 buffer_text.to_string() + prettier_format_suffix,
18039 "Test prettier formatting was not applied to the original buffer text",
18040 );
18041
18042 update_test_language_settings(cx, |settings| {
18043 settings.defaults.formatter = Some(SelectedFormatter::Auto)
18044 });
18045 let format = editor.update_in(cx, |editor, window, cx| {
18046 editor.perform_format(
18047 project.clone(),
18048 FormatTrigger::Manual,
18049 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18050 window,
18051 cx,
18052 )
18053 });
18054 format.await.unwrap();
18055 assert_eq!(
18056 editor.update(cx, |editor, cx| editor.text(cx)),
18057 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
18058 "Autoformatting (via test prettier) was not applied to the original buffer text",
18059 );
18060}
18061
18062#[gpui::test]
18063async fn test_addition_reverts(cx: &mut TestAppContext) {
18064 init_test(cx, |_| {});
18065 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18066 let base_text = indoc! {r#"
18067 struct Row;
18068 struct Row1;
18069 struct Row2;
18070
18071 struct Row4;
18072 struct Row5;
18073 struct Row6;
18074
18075 struct Row8;
18076 struct Row9;
18077 struct Row10;"#};
18078
18079 // When addition hunks are not adjacent to carets, no hunk revert is performed
18080 assert_hunk_revert(
18081 indoc! {r#"struct Row;
18082 struct Row1;
18083 struct Row1.1;
18084 struct Row1.2;
18085 struct Row2;ˇ
18086
18087 struct Row4;
18088 struct Row5;
18089 struct Row6;
18090
18091 struct Row8;
18092 ˇstruct Row9;
18093 struct Row9.1;
18094 struct Row9.2;
18095 struct Row9.3;
18096 struct Row10;"#},
18097 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18098 indoc! {r#"struct Row;
18099 struct Row1;
18100 struct Row1.1;
18101 struct Row1.2;
18102 struct Row2;ˇ
18103
18104 struct Row4;
18105 struct Row5;
18106 struct Row6;
18107
18108 struct Row8;
18109 ˇstruct Row9;
18110 struct Row9.1;
18111 struct Row9.2;
18112 struct Row9.3;
18113 struct Row10;"#},
18114 base_text,
18115 &mut cx,
18116 );
18117 // Same for selections
18118 assert_hunk_revert(
18119 indoc! {r#"struct Row;
18120 struct Row1;
18121 struct Row2;
18122 struct Row2.1;
18123 struct Row2.2;
18124 «ˇ
18125 struct Row4;
18126 struct» Row5;
18127 «struct Row6;
18128 ˇ»
18129 struct Row9.1;
18130 struct Row9.2;
18131 struct Row9.3;
18132 struct Row8;
18133 struct Row9;
18134 struct Row10;"#},
18135 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18136 indoc! {r#"struct Row;
18137 struct Row1;
18138 struct Row2;
18139 struct Row2.1;
18140 struct Row2.2;
18141 «ˇ
18142 struct Row4;
18143 struct» Row5;
18144 «struct Row6;
18145 ˇ»
18146 struct Row9.1;
18147 struct Row9.2;
18148 struct Row9.3;
18149 struct Row8;
18150 struct Row9;
18151 struct Row10;"#},
18152 base_text,
18153 &mut cx,
18154 );
18155
18156 // When carets and selections intersect the addition hunks, those are reverted.
18157 // Adjacent carets got merged.
18158 assert_hunk_revert(
18159 indoc! {r#"struct Row;
18160 ˇ// something on the top
18161 struct Row1;
18162 struct Row2;
18163 struct Roˇw3.1;
18164 struct Row2.2;
18165 struct Row2.3;ˇ
18166
18167 struct Row4;
18168 struct ˇRow5.1;
18169 struct Row5.2;
18170 struct «Rowˇ»5.3;
18171 struct Row5;
18172 struct Row6;
18173 ˇ
18174 struct Row9.1;
18175 struct «Rowˇ»9.2;
18176 struct «ˇRow»9.3;
18177 struct Row8;
18178 struct Row9;
18179 «ˇ// something on bottom»
18180 struct Row10;"#},
18181 vec![
18182 DiffHunkStatusKind::Added,
18183 DiffHunkStatusKind::Added,
18184 DiffHunkStatusKind::Added,
18185 DiffHunkStatusKind::Added,
18186 DiffHunkStatusKind::Added,
18187 ],
18188 indoc! {r#"struct Row;
18189 ˇstruct Row1;
18190 struct Row2;
18191 ˇ
18192 struct Row4;
18193 ˇstruct Row5;
18194 struct Row6;
18195 ˇ
18196 ˇstruct Row8;
18197 struct Row9;
18198 ˇstruct Row10;"#},
18199 base_text,
18200 &mut cx,
18201 );
18202}
18203
18204#[gpui::test]
18205async fn test_modification_reverts(cx: &mut TestAppContext) {
18206 init_test(cx, |_| {});
18207 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18208 let base_text = indoc! {r#"
18209 struct Row;
18210 struct Row1;
18211 struct Row2;
18212
18213 struct Row4;
18214 struct Row5;
18215 struct Row6;
18216
18217 struct Row8;
18218 struct Row9;
18219 struct Row10;"#};
18220
18221 // Modification hunks behave the same as the addition ones.
18222 assert_hunk_revert(
18223 indoc! {r#"struct Row;
18224 struct Row1;
18225 struct Row33;
18226 ˇ
18227 struct Row4;
18228 struct Row5;
18229 struct Row6;
18230 ˇ
18231 struct Row99;
18232 struct Row9;
18233 struct Row10;"#},
18234 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18235 indoc! {r#"struct Row;
18236 struct Row1;
18237 struct Row33;
18238 ˇ
18239 struct Row4;
18240 struct Row5;
18241 struct Row6;
18242 ˇ
18243 struct Row99;
18244 struct Row9;
18245 struct Row10;"#},
18246 base_text,
18247 &mut cx,
18248 );
18249 assert_hunk_revert(
18250 indoc! {r#"struct Row;
18251 struct Row1;
18252 struct Row33;
18253 «ˇ
18254 struct Row4;
18255 struct» Row5;
18256 «struct Row6;
18257 ˇ»
18258 struct Row99;
18259 struct Row9;
18260 struct Row10;"#},
18261 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18262 indoc! {r#"struct Row;
18263 struct Row1;
18264 struct Row33;
18265 «ˇ
18266 struct Row4;
18267 struct» Row5;
18268 «struct Row6;
18269 ˇ»
18270 struct Row99;
18271 struct Row9;
18272 struct Row10;"#},
18273 base_text,
18274 &mut cx,
18275 );
18276
18277 assert_hunk_revert(
18278 indoc! {r#"ˇstruct Row1.1;
18279 struct Row1;
18280 «ˇstr»uct Row22;
18281
18282 struct ˇRow44;
18283 struct Row5;
18284 struct «Rˇ»ow66;ˇ
18285
18286 «struˇ»ct Row88;
18287 struct Row9;
18288 struct Row1011;ˇ"#},
18289 vec![
18290 DiffHunkStatusKind::Modified,
18291 DiffHunkStatusKind::Modified,
18292 DiffHunkStatusKind::Modified,
18293 DiffHunkStatusKind::Modified,
18294 DiffHunkStatusKind::Modified,
18295 DiffHunkStatusKind::Modified,
18296 ],
18297 indoc! {r#"struct Row;
18298 ˇstruct Row1;
18299 struct Row2;
18300 ˇ
18301 struct Row4;
18302 ˇstruct Row5;
18303 struct Row6;
18304 ˇ
18305 struct Row8;
18306 ˇstruct Row9;
18307 struct Row10;ˇ"#},
18308 base_text,
18309 &mut cx,
18310 );
18311}
18312
18313#[gpui::test]
18314async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
18315 init_test(cx, |_| {});
18316 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18317 let base_text = indoc! {r#"
18318 one
18319
18320 two
18321 three
18322 "#};
18323
18324 cx.set_head_text(base_text);
18325 cx.set_state("\nˇ\n");
18326 cx.executor().run_until_parked();
18327 cx.update_editor(|editor, _window, cx| {
18328 editor.expand_selected_diff_hunks(cx);
18329 });
18330 cx.executor().run_until_parked();
18331 cx.update_editor(|editor, window, cx| {
18332 editor.backspace(&Default::default(), window, cx);
18333 });
18334 cx.run_until_parked();
18335 cx.assert_state_with_diff(
18336 indoc! {r#"
18337
18338 - two
18339 - threeˇ
18340 +
18341 "#}
18342 .to_string(),
18343 );
18344}
18345
18346#[gpui::test]
18347async fn test_deletion_reverts(cx: &mut TestAppContext) {
18348 init_test(cx, |_| {});
18349 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18350 let base_text = indoc! {r#"struct Row;
18351struct Row1;
18352struct Row2;
18353
18354struct Row4;
18355struct Row5;
18356struct Row6;
18357
18358struct Row8;
18359struct Row9;
18360struct Row10;"#};
18361
18362 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
18363 assert_hunk_revert(
18364 indoc! {r#"struct Row;
18365 struct Row2;
18366
18367 ˇstruct Row4;
18368 struct Row5;
18369 struct Row6;
18370 ˇ
18371 struct Row8;
18372 struct Row10;"#},
18373 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18374 indoc! {r#"struct Row;
18375 struct Row2;
18376
18377 ˇstruct Row4;
18378 struct Row5;
18379 struct Row6;
18380 ˇ
18381 struct Row8;
18382 struct Row10;"#},
18383 base_text,
18384 &mut cx,
18385 );
18386 assert_hunk_revert(
18387 indoc! {r#"struct Row;
18388 struct Row2;
18389
18390 «ˇstruct Row4;
18391 struct» Row5;
18392 «struct Row6;
18393 ˇ»
18394 struct Row8;
18395 struct Row10;"#},
18396 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18397 indoc! {r#"struct Row;
18398 struct Row2;
18399
18400 «ˇstruct Row4;
18401 struct» Row5;
18402 «struct Row6;
18403 ˇ»
18404 struct Row8;
18405 struct Row10;"#},
18406 base_text,
18407 &mut cx,
18408 );
18409
18410 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
18411 assert_hunk_revert(
18412 indoc! {r#"struct Row;
18413 ˇstruct Row2;
18414
18415 struct Row4;
18416 struct Row5;
18417 struct Row6;
18418
18419 struct Row8;ˇ
18420 struct Row10;"#},
18421 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18422 indoc! {r#"struct Row;
18423 struct Row1;
18424 ˇstruct Row2;
18425
18426 struct Row4;
18427 struct Row5;
18428 struct Row6;
18429
18430 struct Row8;ˇ
18431 struct Row9;
18432 struct Row10;"#},
18433 base_text,
18434 &mut cx,
18435 );
18436 assert_hunk_revert(
18437 indoc! {r#"struct Row;
18438 struct Row2«ˇ;
18439 struct Row4;
18440 struct» Row5;
18441 «struct Row6;
18442
18443 struct Row8;ˇ»
18444 struct Row10;"#},
18445 vec![
18446 DiffHunkStatusKind::Deleted,
18447 DiffHunkStatusKind::Deleted,
18448 DiffHunkStatusKind::Deleted,
18449 ],
18450 indoc! {r#"struct Row;
18451 struct Row1;
18452 struct Row2«ˇ;
18453
18454 struct Row4;
18455 struct» Row5;
18456 «struct Row6;
18457
18458 struct Row8;ˇ»
18459 struct Row9;
18460 struct Row10;"#},
18461 base_text,
18462 &mut cx,
18463 );
18464}
18465
18466#[gpui::test]
18467async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
18468 init_test(cx, |_| {});
18469
18470 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
18471 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
18472 let base_text_3 =
18473 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
18474
18475 let text_1 = edit_first_char_of_every_line(base_text_1);
18476 let text_2 = edit_first_char_of_every_line(base_text_2);
18477 let text_3 = edit_first_char_of_every_line(base_text_3);
18478
18479 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
18480 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
18481 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
18482
18483 let multibuffer = cx.new(|cx| {
18484 let mut multibuffer = MultiBuffer::new(ReadWrite);
18485 multibuffer.push_excerpts(
18486 buffer_1.clone(),
18487 [
18488 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18489 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18490 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18491 ],
18492 cx,
18493 );
18494 multibuffer.push_excerpts(
18495 buffer_2.clone(),
18496 [
18497 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18498 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18499 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18500 ],
18501 cx,
18502 );
18503 multibuffer.push_excerpts(
18504 buffer_3.clone(),
18505 [
18506 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18507 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18508 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18509 ],
18510 cx,
18511 );
18512 multibuffer
18513 });
18514
18515 let fs = FakeFs::new(cx.executor());
18516 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
18517 let (editor, cx) = cx
18518 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
18519 editor.update_in(cx, |editor, _window, cx| {
18520 for (buffer, diff_base) in [
18521 (buffer_1.clone(), base_text_1),
18522 (buffer_2.clone(), base_text_2),
18523 (buffer_3.clone(), base_text_3),
18524 ] {
18525 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
18526 editor
18527 .buffer
18528 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
18529 }
18530 });
18531 cx.executor().run_until_parked();
18532
18533 editor.update_in(cx, |editor, window, cx| {
18534 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}");
18535 editor.select_all(&SelectAll, window, cx);
18536 editor.git_restore(&Default::default(), window, cx);
18537 });
18538 cx.executor().run_until_parked();
18539
18540 // When all ranges are selected, all buffer hunks are reverted.
18541 editor.update(cx, |editor, cx| {
18542 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");
18543 });
18544 buffer_1.update(cx, |buffer, _| {
18545 assert_eq!(buffer.text(), base_text_1);
18546 });
18547 buffer_2.update(cx, |buffer, _| {
18548 assert_eq!(buffer.text(), base_text_2);
18549 });
18550 buffer_3.update(cx, |buffer, _| {
18551 assert_eq!(buffer.text(), base_text_3);
18552 });
18553
18554 editor.update_in(cx, |editor, window, cx| {
18555 editor.undo(&Default::default(), window, cx);
18556 });
18557
18558 editor.update_in(cx, |editor, window, cx| {
18559 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18560 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
18561 });
18562 editor.git_restore(&Default::default(), window, cx);
18563 });
18564
18565 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
18566 // but not affect buffer_2 and its related excerpts.
18567 editor.update(cx, |editor, cx| {
18568 assert_eq!(
18569 editor.text(cx),
18570 "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}"
18571 );
18572 });
18573 buffer_1.update(cx, |buffer, _| {
18574 assert_eq!(buffer.text(), base_text_1);
18575 });
18576 buffer_2.update(cx, |buffer, _| {
18577 assert_eq!(
18578 buffer.text(),
18579 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
18580 );
18581 });
18582 buffer_3.update(cx, |buffer, _| {
18583 assert_eq!(
18584 buffer.text(),
18585 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
18586 );
18587 });
18588
18589 fn edit_first_char_of_every_line(text: &str) -> String {
18590 text.split('\n')
18591 .map(|line| format!("X{}", &line[1..]))
18592 .collect::<Vec<_>>()
18593 .join("\n")
18594 }
18595}
18596
18597#[gpui::test]
18598async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
18599 init_test(cx, |_| {});
18600
18601 let cols = 4;
18602 let rows = 10;
18603 let sample_text_1 = sample_text(rows, cols, 'a');
18604 assert_eq!(
18605 sample_text_1,
18606 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
18607 );
18608 let sample_text_2 = sample_text(rows, cols, 'l');
18609 assert_eq!(
18610 sample_text_2,
18611 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
18612 );
18613 let sample_text_3 = sample_text(rows, cols, 'v');
18614 assert_eq!(
18615 sample_text_3,
18616 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
18617 );
18618
18619 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
18620 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
18621 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
18622
18623 let multi_buffer = cx.new(|cx| {
18624 let mut multibuffer = MultiBuffer::new(ReadWrite);
18625 multibuffer.push_excerpts(
18626 buffer_1.clone(),
18627 [
18628 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18629 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18630 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18631 ],
18632 cx,
18633 );
18634 multibuffer.push_excerpts(
18635 buffer_2.clone(),
18636 [
18637 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18638 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18639 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18640 ],
18641 cx,
18642 );
18643 multibuffer.push_excerpts(
18644 buffer_3.clone(),
18645 [
18646 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18647 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18648 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18649 ],
18650 cx,
18651 );
18652 multibuffer
18653 });
18654
18655 let fs = FakeFs::new(cx.executor());
18656 fs.insert_tree(
18657 "/a",
18658 json!({
18659 "main.rs": sample_text_1,
18660 "other.rs": sample_text_2,
18661 "lib.rs": sample_text_3,
18662 }),
18663 )
18664 .await;
18665 let project = Project::test(fs, ["/a".as_ref()], cx).await;
18666 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18667 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18668 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18669 Editor::new(
18670 EditorMode::full(),
18671 multi_buffer,
18672 Some(project.clone()),
18673 window,
18674 cx,
18675 )
18676 });
18677 let multibuffer_item_id = workspace
18678 .update(cx, |workspace, window, cx| {
18679 assert!(
18680 workspace.active_item(cx).is_none(),
18681 "active item should be None before the first item is added"
18682 );
18683 workspace.add_item_to_active_pane(
18684 Box::new(multi_buffer_editor.clone()),
18685 None,
18686 true,
18687 window,
18688 cx,
18689 );
18690 let active_item = workspace
18691 .active_item(cx)
18692 .expect("should have an active item after adding the multi buffer");
18693 assert!(
18694 !active_item.is_singleton(cx),
18695 "A multi buffer was expected to active after adding"
18696 );
18697 active_item.item_id()
18698 })
18699 .unwrap();
18700 cx.executor().run_until_parked();
18701
18702 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18703 editor.change_selections(
18704 SelectionEffects::scroll(Autoscroll::Next),
18705 window,
18706 cx,
18707 |s| s.select_ranges(Some(1..2)),
18708 );
18709 editor.open_excerpts(&OpenExcerpts, window, cx);
18710 });
18711 cx.executor().run_until_parked();
18712 let first_item_id = workspace
18713 .update(cx, |workspace, window, cx| {
18714 let active_item = workspace
18715 .active_item(cx)
18716 .expect("should have an active item after navigating into the 1st buffer");
18717 let first_item_id = active_item.item_id();
18718 assert_ne!(
18719 first_item_id, multibuffer_item_id,
18720 "Should navigate into the 1st buffer and activate it"
18721 );
18722 assert!(
18723 active_item.is_singleton(cx),
18724 "New active item should be a singleton buffer"
18725 );
18726 assert_eq!(
18727 active_item
18728 .act_as::<Editor>(cx)
18729 .expect("should have navigated into an editor for the 1st buffer")
18730 .read(cx)
18731 .text(cx),
18732 sample_text_1
18733 );
18734
18735 workspace
18736 .go_back(workspace.active_pane().downgrade(), window, cx)
18737 .detach_and_log_err(cx);
18738
18739 first_item_id
18740 })
18741 .unwrap();
18742 cx.executor().run_until_parked();
18743 workspace
18744 .update(cx, |workspace, _, cx| {
18745 let active_item = workspace
18746 .active_item(cx)
18747 .expect("should have an active item after navigating back");
18748 assert_eq!(
18749 active_item.item_id(),
18750 multibuffer_item_id,
18751 "Should navigate back to the multi buffer"
18752 );
18753 assert!(!active_item.is_singleton(cx));
18754 })
18755 .unwrap();
18756
18757 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18758 editor.change_selections(
18759 SelectionEffects::scroll(Autoscroll::Next),
18760 window,
18761 cx,
18762 |s| s.select_ranges(Some(39..40)),
18763 );
18764 editor.open_excerpts(&OpenExcerpts, window, cx);
18765 });
18766 cx.executor().run_until_parked();
18767 let second_item_id = workspace
18768 .update(cx, |workspace, window, cx| {
18769 let active_item = workspace
18770 .active_item(cx)
18771 .expect("should have an active item after navigating into the 2nd buffer");
18772 let second_item_id = active_item.item_id();
18773 assert_ne!(
18774 second_item_id, multibuffer_item_id,
18775 "Should navigate away from the multibuffer"
18776 );
18777 assert_ne!(
18778 second_item_id, first_item_id,
18779 "Should navigate into the 2nd buffer and activate it"
18780 );
18781 assert!(
18782 active_item.is_singleton(cx),
18783 "New active item should be a singleton buffer"
18784 );
18785 assert_eq!(
18786 active_item
18787 .act_as::<Editor>(cx)
18788 .expect("should have navigated into an editor")
18789 .read(cx)
18790 .text(cx),
18791 sample_text_2
18792 );
18793
18794 workspace
18795 .go_back(workspace.active_pane().downgrade(), window, cx)
18796 .detach_and_log_err(cx);
18797
18798 second_item_id
18799 })
18800 .unwrap();
18801 cx.executor().run_until_parked();
18802 workspace
18803 .update(cx, |workspace, _, cx| {
18804 let active_item = workspace
18805 .active_item(cx)
18806 .expect("should have an active item after navigating back from the 2nd buffer");
18807 assert_eq!(
18808 active_item.item_id(),
18809 multibuffer_item_id,
18810 "Should navigate back from the 2nd buffer to the multi buffer"
18811 );
18812 assert!(!active_item.is_singleton(cx));
18813 })
18814 .unwrap();
18815
18816 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18817 editor.change_selections(
18818 SelectionEffects::scroll(Autoscroll::Next),
18819 window,
18820 cx,
18821 |s| s.select_ranges(Some(70..70)),
18822 );
18823 editor.open_excerpts(&OpenExcerpts, window, cx);
18824 });
18825 cx.executor().run_until_parked();
18826 workspace
18827 .update(cx, |workspace, window, cx| {
18828 let active_item = workspace
18829 .active_item(cx)
18830 .expect("should have an active item after navigating into the 3rd buffer");
18831 let third_item_id = active_item.item_id();
18832 assert_ne!(
18833 third_item_id, multibuffer_item_id,
18834 "Should navigate into the 3rd buffer and activate it"
18835 );
18836 assert_ne!(third_item_id, first_item_id);
18837 assert_ne!(third_item_id, second_item_id);
18838 assert!(
18839 active_item.is_singleton(cx),
18840 "New active item should be a singleton buffer"
18841 );
18842 assert_eq!(
18843 active_item
18844 .act_as::<Editor>(cx)
18845 .expect("should have navigated into an editor")
18846 .read(cx)
18847 .text(cx),
18848 sample_text_3
18849 );
18850
18851 workspace
18852 .go_back(workspace.active_pane().downgrade(), window, cx)
18853 .detach_and_log_err(cx);
18854 })
18855 .unwrap();
18856 cx.executor().run_until_parked();
18857 workspace
18858 .update(cx, |workspace, _, cx| {
18859 let active_item = workspace
18860 .active_item(cx)
18861 .expect("should have an active item after navigating back from the 3rd buffer");
18862 assert_eq!(
18863 active_item.item_id(),
18864 multibuffer_item_id,
18865 "Should navigate back from the 3rd buffer to the multi buffer"
18866 );
18867 assert!(!active_item.is_singleton(cx));
18868 })
18869 .unwrap();
18870}
18871
18872#[gpui::test]
18873async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
18874 init_test(cx, |_| {});
18875
18876 let mut cx = EditorTestContext::new(cx).await;
18877
18878 let diff_base = r#"
18879 use some::mod;
18880
18881 const A: u32 = 42;
18882
18883 fn main() {
18884 println!("hello");
18885
18886 println!("world");
18887 }
18888 "#
18889 .unindent();
18890
18891 cx.set_state(
18892 &r#"
18893 use some::modified;
18894
18895 ˇ
18896 fn main() {
18897 println!("hello there");
18898
18899 println!("around the");
18900 println!("world");
18901 }
18902 "#
18903 .unindent(),
18904 );
18905
18906 cx.set_head_text(&diff_base);
18907 executor.run_until_parked();
18908
18909 cx.update_editor(|editor, window, cx| {
18910 editor.go_to_next_hunk(&GoToHunk, window, cx);
18911 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
18912 });
18913 executor.run_until_parked();
18914 cx.assert_state_with_diff(
18915 r#"
18916 use some::modified;
18917
18918
18919 fn main() {
18920 - println!("hello");
18921 + ˇ println!("hello there");
18922
18923 println!("around the");
18924 println!("world");
18925 }
18926 "#
18927 .unindent(),
18928 );
18929
18930 cx.update_editor(|editor, window, cx| {
18931 for _ in 0..2 {
18932 editor.go_to_next_hunk(&GoToHunk, window, cx);
18933 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
18934 }
18935 });
18936 executor.run_until_parked();
18937 cx.assert_state_with_diff(
18938 r#"
18939 - use some::mod;
18940 + ˇuse some::modified;
18941
18942
18943 fn main() {
18944 - println!("hello");
18945 + println!("hello there");
18946
18947 + println!("around the");
18948 println!("world");
18949 }
18950 "#
18951 .unindent(),
18952 );
18953
18954 cx.update_editor(|editor, window, cx| {
18955 editor.go_to_next_hunk(&GoToHunk, window, cx);
18956 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
18957 });
18958 executor.run_until_parked();
18959 cx.assert_state_with_diff(
18960 r#"
18961 - use some::mod;
18962 + use some::modified;
18963
18964 - const A: u32 = 42;
18965 ˇ
18966 fn main() {
18967 - println!("hello");
18968 + println!("hello there");
18969
18970 + println!("around the");
18971 println!("world");
18972 }
18973 "#
18974 .unindent(),
18975 );
18976
18977 cx.update_editor(|editor, window, cx| {
18978 editor.cancel(&Cancel, window, cx);
18979 });
18980
18981 cx.assert_state_with_diff(
18982 r#"
18983 use some::modified;
18984
18985 ˇ
18986 fn main() {
18987 println!("hello there");
18988
18989 println!("around the");
18990 println!("world");
18991 }
18992 "#
18993 .unindent(),
18994 );
18995}
18996
18997#[gpui::test]
18998async fn test_diff_base_change_with_expanded_diff_hunks(
18999 executor: BackgroundExecutor,
19000 cx: &mut TestAppContext,
19001) {
19002 init_test(cx, |_| {});
19003
19004 let mut cx = EditorTestContext::new(cx).await;
19005
19006 let diff_base = r#"
19007 use some::mod1;
19008 use some::mod2;
19009
19010 const A: u32 = 42;
19011 const B: u32 = 42;
19012 const C: u32 = 42;
19013
19014 fn main() {
19015 println!("hello");
19016
19017 println!("world");
19018 }
19019 "#
19020 .unindent();
19021
19022 cx.set_state(
19023 &r#"
19024 use some::mod2;
19025
19026 const A: u32 = 42;
19027 const C: u32 = 42;
19028
19029 fn main(ˇ) {
19030 //println!("hello");
19031
19032 println!("world");
19033 //
19034 //
19035 }
19036 "#
19037 .unindent(),
19038 );
19039
19040 cx.set_head_text(&diff_base);
19041 executor.run_until_parked();
19042
19043 cx.update_editor(|editor, window, cx| {
19044 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19045 });
19046 executor.run_until_parked();
19047 cx.assert_state_with_diff(
19048 r#"
19049 - use some::mod1;
19050 use some::mod2;
19051
19052 const A: u32 = 42;
19053 - const B: u32 = 42;
19054 const C: u32 = 42;
19055
19056 fn main(ˇ) {
19057 - println!("hello");
19058 + //println!("hello");
19059
19060 println!("world");
19061 + //
19062 + //
19063 }
19064 "#
19065 .unindent(),
19066 );
19067
19068 cx.set_head_text("new diff base!");
19069 executor.run_until_parked();
19070 cx.assert_state_with_diff(
19071 r#"
19072 - new diff base!
19073 + use some::mod2;
19074 +
19075 + const A: u32 = 42;
19076 + const C: u32 = 42;
19077 +
19078 + fn main(ˇ) {
19079 + //println!("hello");
19080 +
19081 + println!("world");
19082 + //
19083 + //
19084 + }
19085 "#
19086 .unindent(),
19087 );
19088}
19089
19090#[gpui::test]
19091async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
19092 init_test(cx, |_| {});
19093
19094 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19095 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19096 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19097 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19098 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
19099 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
19100
19101 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
19102 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
19103 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
19104
19105 let multi_buffer = cx.new(|cx| {
19106 let mut multibuffer = MultiBuffer::new(ReadWrite);
19107 multibuffer.push_excerpts(
19108 buffer_1.clone(),
19109 [
19110 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19111 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19112 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19113 ],
19114 cx,
19115 );
19116 multibuffer.push_excerpts(
19117 buffer_2.clone(),
19118 [
19119 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19120 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19121 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19122 ],
19123 cx,
19124 );
19125 multibuffer.push_excerpts(
19126 buffer_3.clone(),
19127 [
19128 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19129 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19130 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19131 ],
19132 cx,
19133 );
19134 multibuffer
19135 });
19136
19137 let editor =
19138 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19139 editor
19140 .update(cx, |editor, _window, cx| {
19141 for (buffer, diff_base) in [
19142 (buffer_1.clone(), file_1_old),
19143 (buffer_2.clone(), file_2_old),
19144 (buffer_3.clone(), file_3_old),
19145 ] {
19146 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
19147 editor
19148 .buffer
19149 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
19150 }
19151 })
19152 .unwrap();
19153
19154 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19155 cx.run_until_parked();
19156
19157 cx.assert_editor_state(
19158 &"
19159 ˇaaa
19160 ccc
19161 ddd
19162
19163 ggg
19164 hhh
19165
19166
19167 lll
19168 mmm
19169 NNN
19170
19171 qqq
19172 rrr
19173
19174 uuu
19175 111
19176 222
19177 333
19178
19179 666
19180 777
19181
19182 000
19183 !!!"
19184 .unindent(),
19185 );
19186
19187 cx.update_editor(|editor, window, cx| {
19188 editor.select_all(&SelectAll, window, cx);
19189 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19190 });
19191 cx.executor().run_until_parked();
19192
19193 cx.assert_state_with_diff(
19194 "
19195 «aaa
19196 - bbb
19197 ccc
19198 ddd
19199
19200 ggg
19201 hhh
19202
19203
19204 lll
19205 mmm
19206 - nnn
19207 + NNN
19208
19209 qqq
19210 rrr
19211
19212 uuu
19213 111
19214 222
19215 333
19216
19217 + 666
19218 777
19219
19220 000
19221 !!!ˇ»"
19222 .unindent(),
19223 );
19224}
19225
19226#[gpui::test]
19227async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
19228 init_test(cx, |_| {});
19229
19230 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
19231 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
19232
19233 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
19234 let multi_buffer = cx.new(|cx| {
19235 let mut multibuffer = MultiBuffer::new(ReadWrite);
19236 multibuffer.push_excerpts(
19237 buffer.clone(),
19238 [
19239 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
19240 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
19241 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
19242 ],
19243 cx,
19244 );
19245 multibuffer
19246 });
19247
19248 let editor =
19249 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19250 editor
19251 .update(cx, |editor, _window, cx| {
19252 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
19253 editor
19254 .buffer
19255 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
19256 })
19257 .unwrap();
19258
19259 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19260 cx.run_until_parked();
19261
19262 cx.update_editor(|editor, window, cx| {
19263 editor.expand_all_diff_hunks(&Default::default(), window, cx)
19264 });
19265 cx.executor().run_until_parked();
19266
19267 // When the start of a hunk coincides with the start of its excerpt,
19268 // the hunk is expanded. When the start of a hunk is earlier than
19269 // the start of its excerpt, the hunk is not expanded.
19270 cx.assert_state_with_diff(
19271 "
19272 ˇaaa
19273 - bbb
19274 + BBB
19275
19276 - ddd
19277 - eee
19278 + DDD
19279 + EEE
19280 fff
19281
19282 iii
19283 "
19284 .unindent(),
19285 );
19286}
19287
19288#[gpui::test]
19289async fn test_edits_around_expanded_insertion_hunks(
19290 executor: BackgroundExecutor,
19291 cx: &mut TestAppContext,
19292) {
19293 init_test(cx, |_| {});
19294
19295 let mut cx = EditorTestContext::new(cx).await;
19296
19297 let diff_base = r#"
19298 use some::mod1;
19299 use some::mod2;
19300
19301 const A: u32 = 42;
19302
19303 fn main() {
19304 println!("hello");
19305
19306 println!("world");
19307 }
19308 "#
19309 .unindent();
19310 executor.run_until_parked();
19311 cx.set_state(
19312 &r#"
19313 use some::mod1;
19314 use some::mod2;
19315
19316 const A: u32 = 42;
19317 const B: u32 = 42;
19318 const C: u32 = 42;
19319 ˇ
19320
19321 fn main() {
19322 println!("hello");
19323
19324 println!("world");
19325 }
19326 "#
19327 .unindent(),
19328 );
19329
19330 cx.set_head_text(&diff_base);
19331 executor.run_until_parked();
19332
19333 cx.update_editor(|editor, window, cx| {
19334 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19335 });
19336 executor.run_until_parked();
19337
19338 cx.assert_state_with_diff(
19339 r#"
19340 use some::mod1;
19341 use some::mod2;
19342
19343 const A: u32 = 42;
19344 + const B: u32 = 42;
19345 + const C: u32 = 42;
19346 + ˇ
19347
19348 fn main() {
19349 println!("hello");
19350
19351 println!("world");
19352 }
19353 "#
19354 .unindent(),
19355 );
19356
19357 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
19358 executor.run_until_parked();
19359
19360 cx.assert_state_with_diff(
19361 r#"
19362 use some::mod1;
19363 use some::mod2;
19364
19365 const A: u32 = 42;
19366 + const B: u32 = 42;
19367 + const C: u32 = 42;
19368 + const D: u32 = 42;
19369 + ˇ
19370
19371 fn main() {
19372 println!("hello");
19373
19374 println!("world");
19375 }
19376 "#
19377 .unindent(),
19378 );
19379
19380 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
19381 executor.run_until_parked();
19382
19383 cx.assert_state_with_diff(
19384 r#"
19385 use some::mod1;
19386 use some::mod2;
19387
19388 const A: u32 = 42;
19389 + const B: u32 = 42;
19390 + const C: u32 = 42;
19391 + const D: u32 = 42;
19392 + const E: u32 = 42;
19393 + ˇ
19394
19395 fn main() {
19396 println!("hello");
19397
19398 println!("world");
19399 }
19400 "#
19401 .unindent(),
19402 );
19403
19404 cx.update_editor(|editor, window, cx| {
19405 editor.delete_line(&DeleteLine, window, cx);
19406 });
19407 executor.run_until_parked();
19408
19409 cx.assert_state_with_diff(
19410 r#"
19411 use some::mod1;
19412 use some::mod2;
19413
19414 const A: u32 = 42;
19415 + const B: u32 = 42;
19416 + const C: u32 = 42;
19417 + const D: u32 = 42;
19418 + const E: u32 = 42;
19419 ˇ
19420 fn main() {
19421 println!("hello");
19422
19423 println!("world");
19424 }
19425 "#
19426 .unindent(),
19427 );
19428
19429 cx.update_editor(|editor, window, cx| {
19430 editor.move_up(&MoveUp, window, cx);
19431 editor.delete_line(&DeleteLine, window, cx);
19432 editor.move_up(&MoveUp, window, cx);
19433 editor.delete_line(&DeleteLine, window, cx);
19434 editor.move_up(&MoveUp, window, cx);
19435 editor.delete_line(&DeleteLine, window, cx);
19436 });
19437 executor.run_until_parked();
19438 cx.assert_state_with_diff(
19439 r#"
19440 use some::mod1;
19441 use some::mod2;
19442
19443 const A: u32 = 42;
19444 + const B: u32 = 42;
19445 ˇ
19446 fn main() {
19447 println!("hello");
19448
19449 println!("world");
19450 }
19451 "#
19452 .unindent(),
19453 );
19454
19455 cx.update_editor(|editor, window, cx| {
19456 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
19457 editor.delete_line(&DeleteLine, window, cx);
19458 });
19459 executor.run_until_parked();
19460 cx.assert_state_with_diff(
19461 r#"
19462 ˇ
19463 fn main() {
19464 println!("hello");
19465
19466 println!("world");
19467 }
19468 "#
19469 .unindent(),
19470 );
19471}
19472
19473#[gpui::test]
19474async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
19475 init_test(cx, |_| {});
19476
19477 let mut cx = EditorTestContext::new(cx).await;
19478 cx.set_head_text(indoc! { "
19479 one
19480 two
19481 three
19482 four
19483 five
19484 "
19485 });
19486 cx.set_state(indoc! { "
19487 one
19488 ˇthree
19489 five
19490 "});
19491 cx.run_until_parked();
19492 cx.update_editor(|editor, window, cx| {
19493 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19494 });
19495 cx.assert_state_with_diff(
19496 indoc! { "
19497 one
19498 - two
19499 ˇthree
19500 - four
19501 five
19502 "}
19503 .to_string(),
19504 );
19505 cx.update_editor(|editor, window, cx| {
19506 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19507 });
19508
19509 cx.assert_state_with_diff(
19510 indoc! { "
19511 one
19512 ˇthree
19513 five
19514 "}
19515 .to_string(),
19516 );
19517
19518 cx.set_state(indoc! { "
19519 one
19520 ˇTWO
19521 three
19522 four
19523 five
19524 "});
19525 cx.run_until_parked();
19526 cx.update_editor(|editor, window, cx| {
19527 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19528 });
19529
19530 cx.assert_state_with_diff(
19531 indoc! { "
19532 one
19533 - two
19534 + ˇTWO
19535 three
19536 four
19537 five
19538 "}
19539 .to_string(),
19540 );
19541 cx.update_editor(|editor, window, cx| {
19542 editor.move_up(&Default::default(), window, cx);
19543 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19544 });
19545 cx.assert_state_with_diff(
19546 indoc! { "
19547 one
19548 ˇTWO
19549 three
19550 four
19551 five
19552 "}
19553 .to_string(),
19554 );
19555}
19556
19557#[gpui::test]
19558async fn test_edits_around_expanded_deletion_hunks(
19559 executor: BackgroundExecutor,
19560 cx: &mut TestAppContext,
19561) {
19562 init_test(cx, |_| {});
19563
19564 let mut cx = EditorTestContext::new(cx).await;
19565
19566 let diff_base = r#"
19567 use some::mod1;
19568 use some::mod2;
19569
19570 const A: u32 = 42;
19571 const B: u32 = 42;
19572 const C: u32 = 42;
19573
19574
19575 fn main() {
19576 println!("hello");
19577
19578 println!("world");
19579 }
19580 "#
19581 .unindent();
19582 executor.run_until_parked();
19583 cx.set_state(
19584 &r#"
19585 use some::mod1;
19586 use some::mod2;
19587
19588 ˇconst B: u32 = 42;
19589 const C: u32 = 42;
19590
19591
19592 fn main() {
19593 println!("hello");
19594
19595 println!("world");
19596 }
19597 "#
19598 .unindent(),
19599 );
19600
19601 cx.set_head_text(&diff_base);
19602 executor.run_until_parked();
19603
19604 cx.update_editor(|editor, window, cx| {
19605 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19606 });
19607 executor.run_until_parked();
19608
19609 cx.assert_state_with_diff(
19610 r#"
19611 use some::mod1;
19612 use some::mod2;
19613
19614 - const A: u32 = 42;
19615 ˇconst B: u32 = 42;
19616 const C: u32 = 42;
19617
19618
19619 fn main() {
19620 println!("hello");
19621
19622 println!("world");
19623 }
19624 "#
19625 .unindent(),
19626 );
19627
19628 cx.update_editor(|editor, window, cx| {
19629 editor.delete_line(&DeleteLine, window, cx);
19630 });
19631 executor.run_until_parked();
19632 cx.assert_state_with_diff(
19633 r#"
19634 use some::mod1;
19635 use some::mod2;
19636
19637 - const A: u32 = 42;
19638 - const B: u32 = 42;
19639 ˇconst C: u32 = 42;
19640
19641
19642 fn main() {
19643 println!("hello");
19644
19645 println!("world");
19646 }
19647 "#
19648 .unindent(),
19649 );
19650
19651 cx.update_editor(|editor, window, cx| {
19652 editor.delete_line(&DeleteLine, window, cx);
19653 });
19654 executor.run_until_parked();
19655 cx.assert_state_with_diff(
19656 r#"
19657 use some::mod1;
19658 use some::mod2;
19659
19660 - const A: u32 = 42;
19661 - const B: u32 = 42;
19662 - const C: u32 = 42;
19663 ˇ
19664
19665 fn main() {
19666 println!("hello");
19667
19668 println!("world");
19669 }
19670 "#
19671 .unindent(),
19672 );
19673
19674 cx.update_editor(|editor, window, cx| {
19675 editor.handle_input("replacement", window, cx);
19676 });
19677 executor.run_until_parked();
19678 cx.assert_state_with_diff(
19679 r#"
19680 use some::mod1;
19681 use some::mod2;
19682
19683 - const A: u32 = 42;
19684 - const B: u32 = 42;
19685 - const C: u32 = 42;
19686 -
19687 + replacementˇ
19688
19689 fn main() {
19690 println!("hello");
19691
19692 println!("world");
19693 }
19694 "#
19695 .unindent(),
19696 );
19697}
19698
19699#[gpui::test]
19700async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19701 init_test(cx, |_| {});
19702
19703 let mut cx = EditorTestContext::new(cx).await;
19704
19705 let base_text = r#"
19706 one
19707 two
19708 three
19709 four
19710 five
19711 "#
19712 .unindent();
19713 executor.run_until_parked();
19714 cx.set_state(
19715 &r#"
19716 one
19717 two
19718 fˇour
19719 five
19720 "#
19721 .unindent(),
19722 );
19723
19724 cx.set_head_text(&base_text);
19725 executor.run_until_parked();
19726
19727 cx.update_editor(|editor, window, cx| {
19728 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19729 });
19730 executor.run_until_parked();
19731
19732 cx.assert_state_with_diff(
19733 r#"
19734 one
19735 two
19736 - three
19737 fˇour
19738 five
19739 "#
19740 .unindent(),
19741 );
19742
19743 cx.update_editor(|editor, window, cx| {
19744 editor.backspace(&Backspace, window, cx);
19745 editor.backspace(&Backspace, window, cx);
19746 });
19747 executor.run_until_parked();
19748 cx.assert_state_with_diff(
19749 r#"
19750 one
19751 two
19752 - threeˇ
19753 - four
19754 + our
19755 five
19756 "#
19757 .unindent(),
19758 );
19759}
19760
19761#[gpui::test]
19762async fn test_edit_after_expanded_modification_hunk(
19763 executor: BackgroundExecutor,
19764 cx: &mut TestAppContext,
19765) {
19766 init_test(cx, |_| {});
19767
19768 let mut cx = EditorTestContext::new(cx).await;
19769
19770 let diff_base = r#"
19771 use some::mod1;
19772 use some::mod2;
19773
19774 const A: u32 = 42;
19775 const B: u32 = 42;
19776 const C: u32 = 42;
19777 const D: u32 = 42;
19778
19779
19780 fn main() {
19781 println!("hello");
19782
19783 println!("world");
19784 }"#
19785 .unindent();
19786
19787 cx.set_state(
19788 &r#"
19789 use some::mod1;
19790 use some::mod2;
19791
19792 const A: u32 = 42;
19793 const B: u32 = 42;
19794 const C: u32 = 43ˇ
19795 const D: u32 = 42;
19796
19797
19798 fn main() {
19799 println!("hello");
19800
19801 println!("world");
19802 }"#
19803 .unindent(),
19804 );
19805
19806 cx.set_head_text(&diff_base);
19807 executor.run_until_parked();
19808 cx.update_editor(|editor, window, cx| {
19809 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19810 });
19811 executor.run_until_parked();
19812
19813 cx.assert_state_with_diff(
19814 r#"
19815 use some::mod1;
19816 use some::mod2;
19817
19818 const A: u32 = 42;
19819 const B: u32 = 42;
19820 - const C: u32 = 42;
19821 + const C: u32 = 43ˇ
19822 const D: u32 = 42;
19823
19824
19825 fn main() {
19826 println!("hello");
19827
19828 println!("world");
19829 }"#
19830 .unindent(),
19831 );
19832
19833 cx.update_editor(|editor, window, cx| {
19834 editor.handle_input("\nnew_line\n", window, cx);
19835 });
19836 executor.run_until_parked();
19837
19838 cx.assert_state_with_diff(
19839 r#"
19840 use some::mod1;
19841 use some::mod2;
19842
19843 const A: u32 = 42;
19844 const B: u32 = 42;
19845 - const C: u32 = 42;
19846 + const C: u32 = 43
19847 + new_line
19848 + ˇ
19849 const D: u32 = 42;
19850
19851
19852 fn main() {
19853 println!("hello");
19854
19855 println!("world");
19856 }"#
19857 .unindent(),
19858 );
19859}
19860
19861#[gpui::test]
19862async fn test_stage_and_unstage_added_file_hunk(
19863 executor: BackgroundExecutor,
19864 cx: &mut TestAppContext,
19865) {
19866 init_test(cx, |_| {});
19867
19868 let mut cx = EditorTestContext::new(cx).await;
19869 cx.update_editor(|editor, _, cx| {
19870 editor.set_expand_all_diff_hunks(cx);
19871 });
19872
19873 let working_copy = r#"
19874 ˇfn main() {
19875 println!("hello, world!");
19876 }
19877 "#
19878 .unindent();
19879
19880 cx.set_state(&working_copy);
19881 executor.run_until_parked();
19882
19883 cx.assert_state_with_diff(
19884 r#"
19885 + ˇfn main() {
19886 + println!("hello, world!");
19887 + }
19888 "#
19889 .unindent(),
19890 );
19891 cx.assert_index_text(None);
19892
19893 cx.update_editor(|editor, window, cx| {
19894 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19895 });
19896 executor.run_until_parked();
19897 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
19898 cx.assert_state_with_diff(
19899 r#"
19900 + ˇfn main() {
19901 + println!("hello, world!");
19902 + }
19903 "#
19904 .unindent(),
19905 );
19906
19907 cx.update_editor(|editor, window, cx| {
19908 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19909 });
19910 executor.run_until_parked();
19911 cx.assert_index_text(None);
19912}
19913
19914async fn setup_indent_guides_editor(
19915 text: &str,
19916 cx: &mut TestAppContext,
19917) -> (BufferId, EditorTestContext) {
19918 init_test(cx, |_| {});
19919
19920 let mut cx = EditorTestContext::new(cx).await;
19921
19922 let buffer_id = cx.update_editor(|editor, window, cx| {
19923 editor.set_text(text, window, cx);
19924 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
19925
19926 buffer_ids[0]
19927 });
19928
19929 (buffer_id, cx)
19930}
19931
19932fn assert_indent_guides(
19933 range: Range<u32>,
19934 expected: Vec<IndentGuide>,
19935 active_indices: Option<Vec<usize>>,
19936 cx: &mut EditorTestContext,
19937) {
19938 let indent_guides = cx.update_editor(|editor, window, cx| {
19939 let snapshot = editor.snapshot(window, cx).display_snapshot;
19940 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
19941 editor,
19942 MultiBufferRow(range.start)..MultiBufferRow(range.end),
19943 true,
19944 &snapshot,
19945 cx,
19946 );
19947
19948 indent_guides.sort_by(|a, b| {
19949 a.depth.cmp(&b.depth).then(
19950 a.start_row
19951 .cmp(&b.start_row)
19952 .then(a.end_row.cmp(&b.end_row)),
19953 )
19954 });
19955 indent_guides
19956 });
19957
19958 if let Some(expected) = active_indices {
19959 let active_indices = cx.update_editor(|editor, window, cx| {
19960 let snapshot = editor.snapshot(window, cx).display_snapshot;
19961 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
19962 });
19963
19964 assert_eq!(
19965 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
19966 expected,
19967 "Active indent guide indices do not match"
19968 );
19969 }
19970
19971 assert_eq!(indent_guides, expected, "Indent guides do not match");
19972}
19973
19974fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
19975 IndentGuide {
19976 buffer_id,
19977 start_row: MultiBufferRow(start_row),
19978 end_row: MultiBufferRow(end_row),
19979 depth,
19980 tab_size: 4,
19981 settings: IndentGuideSettings {
19982 enabled: true,
19983 line_width: 1,
19984 active_line_width: 1,
19985 ..Default::default()
19986 },
19987 }
19988}
19989
19990#[gpui::test]
19991async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
19992 let (buffer_id, mut cx) = setup_indent_guides_editor(
19993 &"
19994 fn main() {
19995 let a = 1;
19996 }"
19997 .unindent(),
19998 cx,
19999 )
20000 .await;
20001
20002 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20003}
20004
20005#[gpui::test]
20006async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
20007 let (buffer_id, mut cx) = setup_indent_guides_editor(
20008 &"
20009 fn main() {
20010 let a = 1;
20011 let b = 2;
20012 }"
20013 .unindent(),
20014 cx,
20015 )
20016 .await;
20017
20018 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
20019}
20020
20021#[gpui::test]
20022async fn test_indent_guide_nested(cx: &mut TestAppContext) {
20023 let (buffer_id, mut cx) = setup_indent_guides_editor(
20024 &"
20025 fn main() {
20026 let a = 1;
20027 if a == 3 {
20028 let b = 2;
20029 } else {
20030 let c = 3;
20031 }
20032 }"
20033 .unindent(),
20034 cx,
20035 )
20036 .await;
20037
20038 assert_indent_guides(
20039 0..8,
20040 vec![
20041 indent_guide(buffer_id, 1, 6, 0),
20042 indent_guide(buffer_id, 3, 3, 1),
20043 indent_guide(buffer_id, 5, 5, 1),
20044 ],
20045 None,
20046 &mut cx,
20047 );
20048}
20049
20050#[gpui::test]
20051async fn test_indent_guide_tab(cx: &mut TestAppContext) {
20052 let (buffer_id, mut cx) = setup_indent_guides_editor(
20053 &"
20054 fn main() {
20055 let a = 1;
20056 let b = 2;
20057 let c = 3;
20058 }"
20059 .unindent(),
20060 cx,
20061 )
20062 .await;
20063
20064 assert_indent_guides(
20065 0..5,
20066 vec![
20067 indent_guide(buffer_id, 1, 3, 0),
20068 indent_guide(buffer_id, 2, 2, 1),
20069 ],
20070 None,
20071 &mut cx,
20072 );
20073}
20074
20075#[gpui::test]
20076async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
20077 let (buffer_id, mut cx) = setup_indent_guides_editor(
20078 &"
20079 fn main() {
20080 let a = 1;
20081
20082 let c = 3;
20083 }"
20084 .unindent(),
20085 cx,
20086 )
20087 .await;
20088
20089 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
20090}
20091
20092#[gpui::test]
20093async fn test_indent_guide_complex(cx: &mut TestAppContext) {
20094 let (buffer_id, mut cx) = setup_indent_guides_editor(
20095 &"
20096 fn main() {
20097 let a = 1;
20098
20099 let c = 3;
20100
20101 if a == 3 {
20102 let b = 2;
20103 } else {
20104 let c = 3;
20105 }
20106 }"
20107 .unindent(),
20108 cx,
20109 )
20110 .await;
20111
20112 assert_indent_guides(
20113 0..11,
20114 vec![
20115 indent_guide(buffer_id, 1, 9, 0),
20116 indent_guide(buffer_id, 6, 6, 1),
20117 indent_guide(buffer_id, 8, 8, 1),
20118 ],
20119 None,
20120 &mut cx,
20121 );
20122}
20123
20124#[gpui::test]
20125async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
20126 let (buffer_id, mut cx) = setup_indent_guides_editor(
20127 &"
20128 fn main() {
20129 let a = 1;
20130
20131 let c = 3;
20132
20133 if a == 3 {
20134 let b = 2;
20135 } else {
20136 let c = 3;
20137 }
20138 }"
20139 .unindent(),
20140 cx,
20141 )
20142 .await;
20143
20144 assert_indent_guides(
20145 1..11,
20146 vec![
20147 indent_guide(buffer_id, 1, 9, 0),
20148 indent_guide(buffer_id, 6, 6, 1),
20149 indent_guide(buffer_id, 8, 8, 1),
20150 ],
20151 None,
20152 &mut cx,
20153 );
20154}
20155
20156#[gpui::test]
20157async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
20158 let (buffer_id, mut cx) = setup_indent_guides_editor(
20159 &"
20160 fn main() {
20161 let a = 1;
20162
20163 let c = 3;
20164
20165 if a == 3 {
20166 let b = 2;
20167 } else {
20168 let c = 3;
20169 }
20170 }"
20171 .unindent(),
20172 cx,
20173 )
20174 .await;
20175
20176 assert_indent_guides(
20177 1..10,
20178 vec![
20179 indent_guide(buffer_id, 1, 9, 0),
20180 indent_guide(buffer_id, 6, 6, 1),
20181 indent_guide(buffer_id, 8, 8, 1),
20182 ],
20183 None,
20184 &mut cx,
20185 );
20186}
20187
20188#[gpui::test]
20189async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
20190 let (buffer_id, mut cx) = setup_indent_guides_editor(
20191 &"
20192 fn main() {
20193 if a {
20194 b(
20195 c,
20196 d,
20197 )
20198 } else {
20199 e(
20200 f
20201 )
20202 }
20203 }"
20204 .unindent(),
20205 cx,
20206 )
20207 .await;
20208
20209 assert_indent_guides(
20210 0..11,
20211 vec![
20212 indent_guide(buffer_id, 1, 10, 0),
20213 indent_guide(buffer_id, 2, 5, 1),
20214 indent_guide(buffer_id, 7, 9, 1),
20215 indent_guide(buffer_id, 3, 4, 2),
20216 indent_guide(buffer_id, 8, 8, 2),
20217 ],
20218 None,
20219 &mut cx,
20220 );
20221
20222 cx.update_editor(|editor, window, cx| {
20223 editor.fold_at(MultiBufferRow(2), window, cx);
20224 assert_eq!(
20225 editor.display_text(cx),
20226 "
20227 fn main() {
20228 if a {
20229 b(⋯
20230 )
20231 } else {
20232 e(
20233 f
20234 )
20235 }
20236 }"
20237 .unindent()
20238 );
20239 });
20240
20241 assert_indent_guides(
20242 0..11,
20243 vec![
20244 indent_guide(buffer_id, 1, 10, 0),
20245 indent_guide(buffer_id, 2, 5, 1),
20246 indent_guide(buffer_id, 7, 9, 1),
20247 indent_guide(buffer_id, 8, 8, 2),
20248 ],
20249 None,
20250 &mut cx,
20251 );
20252}
20253
20254#[gpui::test]
20255async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
20256 let (buffer_id, mut cx) = setup_indent_guides_editor(
20257 &"
20258 block1
20259 block2
20260 block3
20261 block4
20262 block2
20263 block1
20264 block1"
20265 .unindent(),
20266 cx,
20267 )
20268 .await;
20269
20270 assert_indent_guides(
20271 1..10,
20272 vec![
20273 indent_guide(buffer_id, 1, 4, 0),
20274 indent_guide(buffer_id, 2, 3, 1),
20275 indent_guide(buffer_id, 3, 3, 2),
20276 ],
20277 None,
20278 &mut cx,
20279 );
20280}
20281
20282#[gpui::test]
20283async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
20284 let (buffer_id, mut cx) = setup_indent_guides_editor(
20285 &"
20286 block1
20287 block2
20288 block3
20289
20290 block1
20291 block1"
20292 .unindent(),
20293 cx,
20294 )
20295 .await;
20296
20297 assert_indent_guides(
20298 0..6,
20299 vec![
20300 indent_guide(buffer_id, 1, 2, 0),
20301 indent_guide(buffer_id, 2, 2, 1),
20302 ],
20303 None,
20304 &mut cx,
20305 );
20306}
20307
20308#[gpui::test]
20309async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
20310 let (buffer_id, mut cx) = setup_indent_guides_editor(
20311 &"
20312 function component() {
20313 \treturn (
20314 \t\t\t
20315 \t\t<div>
20316 \t\t\t<abc></abc>
20317 \t\t</div>
20318 \t)
20319 }"
20320 .unindent(),
20321 cx,
20322 )
20323 .await;
20324
20325 assert_indent_guides(
20326 0..8,
20327 vec![
20328 indent_guide(buffer_id, 1, 6, 0),
20329 indent_guide(buffer_id, 2, 5, 1),
20330 indent_guide(buffer_id, 4, 4, 2),
20331 ],
20332 None,
20333 &mut cx,
20334 );
20335}
20336
20337#[gpui::test]
20338async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
20339 let (buffer_id, mut cx) = setup_indent_guides_editor(
20340 &"
20341 function component() {
20342 \treturn (
20343 \t
20344 \t\t<div>
20345 \t\t\t<abc></abc>
20346 \t\t</div>
20347 \t)
20348 }"
20349 .unindent(),
20350 cx,
20351 )
20352 .await;
20353
20354 assert_indent_guides(
20355 0..8,
20356 vec![
20357 indent_guide(buffer_id, 1, 6, 0),
20358 indent_guide(buffer_id, 2, 5, 1),
20359 indent_guide(buffer_id, 4, 4, 2),
20360 ],
20361 None,
20362 &mut cx,
20363 );
20364}
20365
20366#[gpui::test]
20367async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
20368 let (buffer_id, mut cx) = setup_indent_guides_editor(
20369 &"
20370 block1
20371
20372
20373
20374 block2
20375 "
20376 .unindent(),
20377 cx,
20378 )
20379 .await;
20380
20381 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20382}
20383
20384#[gpui::test]
20385async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
20386 let (buffer_id, mut cx) = setup_indent_guides_editor(
20387 &"
20388 def a:
20389 \tb = 3
20390 \tif True:
20391 \t\tc = 4
20392 \t\td = 5
20393 \tprint(b)
20394 "
20395 .unindent(),
20396 cx,
20397 )
20398 .await;
20399
20400 assert_indent_guides(
20401 0..6,
20402 vec![
20403 indent_guide(buffer_id, 1, 5, 0),
20404 indent_guide(buffer_id, 3, 4, 1),
20405 ],
20406 None,
20407 &mut cx,
20408 );
20409}
20410
20411#[gpui::test]
20412async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
20413 let (buffer_id, mut cx) = setup_indent_guides_editor(
20414 &"
20415 fn main() {
20416 let a = 1;
20417 }"
20418 .unindent(),
20419 cx,
20420 )
20421 .await;
20422
20423 cx.update_editor(|editor, window, cx| {
20424 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20425 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20426 });
20427 });
20428
20429 assert_indent_guides(
20430 0..3,
20431 vec![indent_guide(buffer_id, 1, 1, 0)],
20432 Some(vec![0]),
20433 &mut cx,
20434 );
20435}
20436
20437#[gpui::test]
20438async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
20439 let (buffer_id, mut cx) = setup_indent_guides_editor(
20440 &"
20441 fn main() {
20442 if 1 == 2 {
20443 let a = 1;
20444 }
20445 }"
20446 .unindent(),
20447 cx,
20448 )
20449 .await;
20450
20451 cx.update_editor(|editor, window, cx| {
20452 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20453 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20454 });
20455 });
20456
20457 assert_indent_guides(
20458 0..4,
20459 vec![
20460 indent_guide(buffer_id, 1, 3, 0),
20461 indent_guide(buffer_id, 2, 2, 1),
20462 ],
20463 Some(vec![1]),
20464 &mut cx,
20465 );
20466
20467 cx.update_editor(|editor, window, cx| {
20468 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20469 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20470 });
20471 });
20472
20473 assert_indent_guides(
20474 0..4,
20475 vec![
20476 indent_guide(buffer_id, 1, 3, 0),
20477 indent_guide(buffer_id, 2, 2, 1),
20478 ],
20479 Some(vec![1]),
20480 &mut cx,
20481 );
20482
20483 cx.update_editor(|editor, window, cx| {
20484 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20485 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
20486 });
20487 });
20488
20489 assert_indent_guides(
20490 0..4,
20491 vec![
20492 indent_guide(buffer_id, 1, 3, 0),
20493 indent_guide(buffer_id, 2, 2, 1),
20494 ],
20495 Some(vec![0]),
20496 &mut cx,
20497 );
20498}
20499
20500#[gpui::test]
20501async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
20502 let (buffer_id, mut cx) = setup_indent_guides_editor(
20503 &"
20504 fn main() {
20505 let a = 1;
20506
20507 let b = 2;
20508 }"
20509 .unindent(),
20510 cx,
20511 )
20512 .await;
20513
20514 cx.update_editor(|editor, window, cx| {
20515 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20516 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20517 });
20518 });
20519
20520 assert_indent_guides(
20521 0..5,
20522 vec![indent_guide(buffer_id, 1, 3, 0)],
20523 Some(vec![0]),
20524 &mut cx,
20525 );
20526}
20527
20528#[gpui::test]
20529async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
20530 let (buffer_id, mut cx) = setup_indent_guides_editor(
20531 &"
20532 def m:
20533 a = 1
20534 pass"
20535 .unindent(),
20536 cx,
20537 )
20538 .await;
20539
20540 cx.update_editor(|editor, window, cx| {
20541 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20542 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20543 });
20544 });
20545
20546 assert_indent_guides(
20547 0..3,
20548 vec![indent_guide(buffer_id, 1, 2, 0)],
20549 Some(vec![0]),
20550 &mut cx,
20551 );
20552}
20553
20554#[gpui::test]
20555async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
20556 init_test(cx, |_| {});
20557 let mut cx = EditorTestContext::new(cx).await;
20558 let text = indoc! {
20559 "
20560 impl A {
20561 fn b() {
20562 0;
20563 3;
20564 5;
20565 6;
20566 7;
20567 }
20568 }
20569 "
20570 };
20571 let base_text = indoc! {
20572 "
20573 impl A {
20574 fn b() {
20575 0;
20576 1;
20577 2;
20578 3;
20579 4;
20580 }
20581 fn c() {
20582 5;
20583 6;
20584 7;
20585 }
20586 }
20587 "
20588 };
20589
20590 cx.update_editor(|editor, window, cx| {
20591 editor.set_text(text, window, cx);
20592
20593 editor.buffer().update(cx, |multibuffer, cx| {
20594 let buffer = multibuffer.as_singleton().unwrap();
20595 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
20596
20597 multibuffer.set_all_diff_hunks_expanded(cx);
20598 multibuffer.add_diff(diff, cx);
20599
20600 buffer.read(cx).remote_id()
20601 })
20602 });
20603 cx.run_until_parked();
20604
20605 cx.assert_state_with_diff(
20606 indoc! { "
20607 impl A {
20608 fn b() {
20609 0;
20610 - 1;
20611 - 2;
20612 3;
20613 - 4;
20614 - }
20615 - fn c() {
20616 5;
20617 6;
20618 7;
20619 }
20620 }
20621 ˇ"
20622 }
20623 .to_string(),
20624 );
20625
20626 let mut actual_guides = cx.update_editor(|editor, window, cx| {
20627 editor
20628 .snapshot(window, cx)
20629 .buffer_snapshot
20630 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
20631 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
20632 .collect::<Vec<_>>()
20633 });
20634 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
20635 assert_eq!(
20636 actual_guides,
20637 vec![
20638 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
20639 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
20640 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
20641 ]
20642 );
20643}
20644
20645#[gpui::test]
20646async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20647 init_test(cx, |_| {});
20648 let mut cx = EditorTestContext::new(cx).await;
20649
20650 let diff_base = r#"
20651 a
20652 b
20653 c
20654 "#
20655 .unindent();
20656
20657 cx.set_state(
20658 &r#"
20659 ˇA
20660 b
20661 C
20662 "#
20663 .unindent(),
20664 );
20665 cx.set_head_text(&diff_base);
20666 cx.update_editor(|editor, window, cx| {
20667 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20668 });
20669 executor.run_until_parked();
20670
20671 let both_hunks_expanded = r#"
20672 - a
20673 + ˇA
20674 b
20675 - c
20676 + C
20677 "#
20678 .unindent();
20679
20680 cx.assert_state_with_diff(both_hunks_expanded.clone());
20681
20682 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20683 let snapshot = editor.snapshot(window, cx);
20684 let hunks = editor
20685 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20686 .collect::<Vec<_>>();
20687 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20688 let buffer_id = hunks[0].buffer_id;
20689 hunks
20690 .into_iter()
20691 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20692 .collect::<Vec<_>>()
20693 });
20694 assert_eq!(hunk_ranges.len(), 2);
20695
20696 cx.update_editor(|editor, _, cx| {
20697 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20698 });
20699 executor.run_until_parked();
20700
20701 let second_hunk_expanded = r#"
20702 ˇA
20703 b
20704 - c
20705 + C
20706 "#
20707 .unindent();
20708
20709 cx.assert_state_with_diff(second_hunk_expanded);
20710
20711 cx.update_editor(|editor, _, cx| {
20712 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20713 });
20714 executor.run_until_parked();
20715
20716 cx.assert_state_with_diff(both_hunks_expanded.clone());
20717
20718 cx.update_editor(|editor, _, cx| {
20719 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20720 });
20721 executor.run_until_parked();
20722
20723 let first_hunk_expanded = r#"
20724 - a
20725 + ˇA
20726 b
20727 C
20728 "#
20729 .unindent();
20730
20731 cx.assert_state_with_diff(first_hunk_expanded);
20732
20733 cx.update_editor(|editor, _, cx| {
20734 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20735 });
20736 executor.run_until_parked();
20737
20738 cx.assert_state_with_diff(both_hunks_expanded);
20739
20740 cx.set_state(
20741 &r#"
20742 ˇA
20743 b
20744 "#
20745 .unindent(),
20746 );
20747 cx.run_until_parked();
20748
20749 // TODO this cursor position seems bad
20750 cx.assert_state_with_diff(
20751 r#"
20752 - ˇa
20753 + A
20754 b
20755 "#
20756 .unindent(),
20757 );
20758
20759 cx.update_editor(|editor, window, cx| {
20760 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20761 });
20762
20763 cx.assert_state_with_diff(
20764 r#"
20765 - ˇa
20766 + A
20767 b
20768 - c
20769 "#
20770 .unindent(),
20771 );
20772
20773 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20774 let snapshot = editor.snapshot(window, cx);
20775 let hunks = editor
20776 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20777 .collect::<Vec<_>>();
20778 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20779 let buffer_id = hunks[0].buffer_id;
20780 hunks
20781 .into_iter()
20782 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20783 .collect::<Vec<_>>()
20784 });
20785 assert_eq!(hunk_ranges.len(), 2);
20786
20787 cx.update_editor(|editor, _, cx| {
20788 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20789 });
20790 executor.run_until_parked();
20791
20792 cx.assert_state_with_diff(
20793 r#"
20794 - ˇa
20795 + A
20796 b
20797 "#
20798 .unindent(),
20799 );
20800}
20801
20802#[gpui::test]
20803async fn test_toggle_deletion_hunk_at_start_of_file(
20804 executor: BackgroundExecutor,
20805 cx: &mut TestAppContext,
20806) {
20807 init_test(cx, |_| {});
20808 let mut cx = EditorTestContext::new(cx).await;
20809
20810 let diff_base = r#"
20811 a
20812 b
20813 c
20814 "#
20815 .unindent();
20816
20817 cx.set_state(
20818 &r#"
20819 ˇb
20820 c
20821 "#
20822 .unindent(),
20823 );
20824 cx.set_head_text(&diff_base);
20825 cx.update_editor(|editor, window, cx| {
20826 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20827 });
20828 executor.run_until_parked();
20829
20830 let hunk_expanded = r#"
20831 - a
20832 ˇb
20833 c
20834 "#
20835 .unindent();
20836
20837 cx.assert_state_with_diff(hunk_expanded.clone());
20838
20839 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20840 let snapshot = editor.snapshot(window, cx);
20841 let hunks = editor
20842 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20843 .collect::<Vec<_>>();
20844 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20845 let buffer_id = hunks[0].buffer_id;
20846 hunks
20847 .into_iter()
20848 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20849 .collect::<Vec<_>>()
20850 });
20851 assert_eq!(hunk_ranges.len(), 1);
20852
20853 cx.update_editor(|editor, _, cx| {
20854 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20855 });
20856 executor.run_until_parked();
20857
20858 let hunk_collapsed = r#"
20859 ˇb
20860 c
20861 "#
20862 .unindent();
20863
20864 cx.assert_state_with_diff(hunk_collapsed);
20865
20866 cx.update_editor(|editor, _, cx| {
20867 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20868 });
20869 executor.run_until_parked();
20870
20871 cx.assert_state_with_diff(hunk_expanded);
20872}
20873
20874#[gpui::test]
20875async fn test_display_diff_hunks(cx: &mut TestAppContext) {
20876 init_test(cx, |_| {});
20877
20878 let fs = FakeFs::new(cx.executor());
20879 fs.insert_tree(
20880 path!("/test"),
20881 json!({
20882 ".git": {},
20883 "file-1": "ONE\n",
20884 "file-2": "TWO\n",
20885 "file-3": "THREE\n",
20886 }),
20887 )
20888 .await;
20889
20890 fs.set_head_for_repo(
20891 path!("/test/.git").as_ref(),
20892 &[
20893 ("file-1".into(), "one\n".into()),
20894 ("file-2".into(), "two\n".into()),
20895 ("file-3".into(), "three\n".into()),
20896 ],
20897 "deadbeef",
20898 );
20899
20900 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
20901 let mut buffers = vec![];
20902 for i in 1..=3 {
20903 let buffer = project
20904 .update(cx, |project, cx| {
20905 let path = format!(path!("/test/file-{}"), i);
20906 project.open_local_buffer(path, cx)
20907 })
20908 .await
20909 .unwrap();
20910 buffers.push(buffer);
20911 }
20912
20913 let multibuffer = cx.new(|cx| {
20914 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
20915 multibuffer.set_all_diff_hunks_expanded(cx);
20916 for buffer in &buffers {
20917 let snapshot = buffer.read(cx).snapshot();
20918 multibuffer.set_excerpts_for_path(
20919 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
20920 buffer.clone(),
20921 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
20922 2,
20923 cx,
20924 );
20925 }
20926 multibuffer
20927 });
20928
20929 let editor = cx.add_window(|window, cx| {
20930 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
20931 });
20932 cx.run_until_parked();
20933
20934 let snapshot = editor
20935 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
20936 .unwrap();
20937 let hunks = snapshot
20938 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
20939 .map(|hunk| match hunk {
20940 DisplayDiffHunk::Unfolded {
20941 display_row_range, ..
20942 } => display_row_range,
20943 DisplayDiffHunk::Folded { .. } => unreachable!(),
20944 })
20945 .collect::<Vec<_>>();
20946 assert_eq!(
20947 hunks,
20948 [
20949 DisplayRow(2)..DisplayRow(4),
20950 DisplayRow(7)..DisplayRow(9),
20951 DisplayRow(12)..DisplayRow(14),
20952 ]
20953 );
20954}
20955
20956#[gpui::test]
20957async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
20958 init_test(cx, |_| {});
20959
20960 let mut cx = EditorTestContext::new(cx).await;
20961 cx.set_head_text(indoc! { "
20962 one
20963 two
20964 three
20965 four
20966 five
20967 "
20968 });
20969 cx.set_index_text(indoc! { "
20970 one
20971 two
20972 three
20973 four
20974 five
20975 "
20976 });
20977 cx.set_state(indoc! {"
20978 one
20979 TWO
20980 ˇTHREE
20981 FOUR
20982 five
20983 "});
20984 cx.run_until_parked();
20985 cx.update_editor(|editor, window, cx| {
20986 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20987 });
20988 cx.run_until_parked();
20989 cx.assert_index_text(Some(indoc! {"
20990 one
20991 TWO
20992 THREE
20993 FOUR
20994 five
20995 "}));
20996 cx.set_state(indoc! { "
20997 one
20998 TWO
20999 ˇTHREE-HUNDRED
21000 FOUR
21001 five
21002 "});
21003 cx.run_until_parked();
21004 cx.update_editor(|editor, window, cx| {
21005 let snapshot = editor.snapshot(window, cx);
21006 let hunks = editor
21007 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
21008 .collect::<Vec<_>>();
21009 assert_eq!(hunks.len(), 1);
21010 assert_eq!(
21011 hunks[0].status(),
21012 DiffHunkStatus {
21013 kind: DiffHunkStatusKind::Modified,
21014 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
21015 }
21016 );
21017
21018 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21019 });
21020 cx.run_until_parked();
21021 cx.assert_index_text(Some(indoc! {"
21022 one
21023 TWO
21024 THREE-HUNDRED
21025 FOUR
21026 five
21027 "}));
21028}
21029
21030#[gpui::test]
21031fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
21032 init_test(cx, |_| {});
21033
21034 let editor = cx.add_window(|window, cx| {
21035 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
21036 build_editor(buffer, window, cx)
21037 });
21038
21039 let render_args = Arc::new(Mutex::new(None));
21040 let snapshot = editor
21041 .update(cx, |editor, window, cx| {
21042 let snapshot = editor.buffer().read(cx).snapshot(cx);
21043 let range =
21044 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
21045
21046 struct RenderArgs {
21047 row: MultiBufferRow,
21048 folded: bool,
21049 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
21050 }
21051
21052 let crease = Crease::inline(
21053 range,
21054 FoldPlaceholder::test(),
21055 {
21056 let toggle_callback = render_args.clone();
21057 move |row, folded, callback, _window, _cx| {
21058 *toggle_callback.lock() = Some(RenderArgs {
21059 row,
21060 folded,
21061 callback,
21062 });
21063 div()
21064 }
21065 },
21066 |_row, _folded, _window, _cx| div(),
21067 );
21068
21069 editor.insert_creases(Some(crease), cx);
21070 let snapshot = editor.snapshot(window, cx);
21071 let _div =
21072 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
21073 snapshot
21074 })
21075 .unwrap();
21076
21077 let render_args = render_args.lock().take().unwrap();
21078 assert_eq!(render_args.row, MultiBufferRow(1));
21079 assert!(!render_args.folded);
21080 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21081
21082 cx.update_window(*editor, |_, window, cx| {
21083 (render_args.callback)(true, window, cx)
21084 })
21085 .unwrap();
21086 let snapshot = editor
21087 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21088 .unwrap();
21089 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
21090
21091 cx.update_window(*editor, |_, window, cx| {
21092 (render_args.callback)(false, window, cx)
21093 })
21094 .unwrap();
21095 let snapshot = editor
21096 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21097 .unwrap();
21098 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21099}
21100
21101#[gpui::test]
21102async fn test_input_text(cx: &mut TestAppContext) {
21103 init_test(cx, |_| {});
21104 let mut cx = EditorTestContext::new(cx).await;
21105
21106 cx.set_state(
21107 &r#"ˇone
21108 two
21109
21110 three
21111 fourˇ
21112 five
21113
21114 siˇx"#
21115 .unindent(),
21116 );
21117
21118 cx.dispatch_action(HandleInput(String::new()));
21119 cx.assert_editor_state(
21120 &r#"ˇone
21121 two
21122
21123 three
21124 fourˇ
21125 five
21126
21127 siˇx"#
21128 .unindent(),
21129 );
21130
21131 cx.dispatch_action(HandleInput("AAAA".to_string()));
21132 cx.assert_editor_state(
21133 &r#"AAAAˇone
21134 two
21135
21136 three
21137 fourAAAAˇ
21138 five
21139
21140 siAAAAˇx"#
21141 .unindent(),
21142 );
21143}
21144
21145#[gpui::test]
21146async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
21147 init_test(cx, |_| {});
21148
21149 let mut cx = EditorTestContext::new(cx).await;
21150 cx.set_state(
21151 r#"let foo = 1;
21152let foo = 2;
21153let foo = 3;
21154let fooˇ = 4;
21155let foo = 5;
21156let foo = 6;
21157let foo = 7;
21158let foo = 8;
21159let foo = 9;
21160let foo = 10;
21161let foo = 11;
21162let foo = 12;
21163let foo = 13;
21164let foo = 14;
21165let foo = 15;"#,
21166 );
21167
21168 cx.update_editor(|e, window, cx| {
21169 assert_eq!(
21170 e.next_scroll_position,
21171 NextScrollCursorCenterTopBottom::Center,
21172 "Default next scroll direction is center",
21173 );
21174
21175 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21176 assert_eq!(
21177 e.next_scroll_position,
21178 NextScrollCursorCenterTopBottom::Top,
21179 "After center, next scroll direction should be top",
21180 );
21181
21182 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21183 assert_eq!(
21184 e.next_scroll_position,
21185 NextScrollCursorCenterTopBottom::Bottom,
21186 "After top, next scroll direction should be bottom",
21187 );
21188
21189 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21190 assert_eq!(
21191 e.next_scroll_position,
21192 NextScrollCursorCenterTopBottom::Center,
21193 "After bottom, scrolling should start over",
21194 );
21195
21196 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21197 assert_eq!(
21198 e.next_scroll_position,
21199 NextScrollCursorCenterTopBottom::Top,
21200 "Scrolling continues if retriggered fast enough"
21201 );
21202 });
21203
21204 cx.executor()
21205 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
21206 cx.executor().run_until_parked();
21207 cx.update_editor(|e, _, _| {
21208 assert_eq!(
21209 e.next_scroll_position,
21210 NextScrollCursorCenterTopBottom::Center,
21211 "If scrolling is not triggered fast enough, it should reset"
21212 );
21213 });
21214}
21215
21216#[gpui::test]
21217async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
21218 init_test(cx, |_| {});
21219 let mut cx = EditorLspTestContext::new_rust(
21220 lsp::ServerCapabilities {
21221 definition_provider: Some(lsp::OneOf::Left(true)),
21222 references_provider: Some(lsp::OneOf::Left(true)),
21223 ..lsp::ServerCapabilities::default()
21224 },
21225 cx,
21226 )
21227 .await;
21228
21229 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
21230 let go_to_definition = cx
21231 .lsp
21232 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21233 move |params, _| async move {
21234 if empty_go_to_definition {
21235 Ok(None)
21236 } else {
21237 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
21238 uri: params.text_document_position_params.text_document.uri,
21239 range: lsp::Range::new(
21240 lsp::Position::new(4, 3),
21241 lsp::Position::new(4, 6),
21242 ),
21243 })))
21244 }
21245 },
21246 );
21247 let references = cx
21248 .lsp
21249 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21250 Ok(Some(vec![lsp::Location {
21251 uri: params.text_document_position.text_document.uri,
21252 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
21253 }]))
21254 });
21255 (go_to_definition, references)
21256 };
21257
21258 cx.set_state(
21259 &r#"fn one() {
21260 let mut a = ˇtwo();
21261 }
21262
21263 fn two() {}"#
21264 .unindent(),
21265 );
21266 set_up_lsp_handlers(false, &mut cx);
21267 let navigated = cx
21268 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21269 .await
21270 .expect("Failed to navigate to definition");
21271 assert_eq!(
21272 navigated,
21273 Navigated::Yes,
21274 "Should have navigated to definition from the GetDefinition response"
21275 );
21276 cx.assert_editor_state(
21277 &r#"fn one() {
21278 let mut a = two();
21279 }
21280
21281 fn «twoˇ»() {}"#
21282 .unindent(),
21283 );
21284
21285 let editors = cx.update_workspace(|workspace, _, cx| {
21286 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21287 });
21288 cx.update_editor(|_, _, test_editor_cx| {
21289 assert_eq!(
21290 editors.len(),
21291 1,
21292 "Initially, only one, test, editor should be open in the workspace"
21293 );
21294 assert_eq!(
21295 test_editor_cx.entity(),
21296 editors.last().expect("Asserted len is 1").clone()
21297 );
21298 });
21299
21300 set_up_lsp_handlers(true, &mut cx);
21301 let navigated = cx
21302 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21303 .await
21304 .expect("Failed to navigate to lookup references");
21305 assert_eq!(
21306 navigated,
21307 Navigated::Yes,
21308 "Should have navigated to references as a fallback after empty GoToDefinition response"
21309 );
21310 // We should not change the selections in the existing file,
21311 // if opening another milti buffer with the references
21312 cx.assert_editor_state(
21313 &r#"fn one() {
21314 let mut a = two();
21315 }
21316
21317 fn «twoˇ»() {}"#
21318 .unindent(),
21319 );
21320 let editors = cx.update_workspace(|workspace, _, cx| {
21321 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21322 });
21323 cx.update_editor(|_, _, test_editor_cx| {
21324 assert_eq!(
21325 editors.len(),
21326 2,
21327 "After falling back to references search, we open a new editor with the results"
21328 );
21329 let references_fallback_text = editors
21330 .into_iter()
21331 .find(|new_editor| *new_editor != test_editor_cx.entity())
21332 .expect("Should have one non-test editor now")
21333 .read(test_editor_cx)
21334 .text(test_editor_cx);
21335 assert_eq!(
21336 references_fallback_text, "fn one() {\n let mut a = two();\n}",
21337 "Should use the range from the references response and not the GoToDefinition one"
21338 );
21339 });
21340}
21341
21342#[gpui::test]
21343async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
21344 init_test(cx, |_| {});
21345 cx.update(|cx| {
21346 let mut editor_settings = EditorSettings::get_global(cx).clone();
21347 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
21348 EditorSettings::override_global(editor_settings, cx);
21349 });
21350 let mut cx = EditorLspTestContext::new_rust(
21351 lsp::ServerCapabilities {
21352 definition_provider: Some(lsp::OneOf::Left(true)),
21353 references_provider: Some(lsp::OneOf::Left(true)),
21354 ..lsp::ServerCapabilities::default()
21355 },
21356 cx,
21357 )
21358 .await;
21359 let original_state = r#"fn one() {
21360 let mut a = ˇtwo();
21361 }
21362
21363 fn two() {}"#
21364 .unindent();
21365 cx.set_state(&original_state);
21366
21367 let mut go_to_definition = cx
21368 .lsp
21369 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21370 move |_, _| async move { Ok(None) },
21371 );
21372 let _references = cx
21373 .lsp
21374 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
21375 panic!("Should not call for references with no go to definition fallback")
21376 });
21377
21378 let navigated = cx
21379 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21380 .await
21381 .expect("Failed to navigate to lookup references");
21382 go_to_definition
21383 .next()
21384 .await
21385 .expect("Should have called the go_to_definition handler");
21386
21387 assert_eq!(
21388 navigated,
21389 Navigated::No,
21390 "Should have navigated to references as a fallback after empty GoToDefinition response"
21391 );
21392 cx.assert_editor_state(&original_state);
21393 let editors = cx.update_workspace(|workspace, _, cx| {
21394 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21395 });
21396 cx.update_editor(|_, _, _| {
21397 assert_eq!(
21398 editors.len(),
21399 1,
21400 "After unsuccessful fallback, no other editor should have been opened"
21401 );
21402 });
21403}
21404
21405#[gpui::test]
21406async fn test_find_all_references_editor_reuse(cx: &mut TestAppContext) {
21407 init_test(cx, |_| {});
21408 let mut cx = EditorLspTestContext::new_rust(
21409 lsp::ServerCapabilities {
21410 references_provider: Some(lsp::OneOf::Left(true)),
21411 ..lsp::ServerCapabilities::default()
21412 },
21413 cx,
21414 )
21415 .await;
21416
21417 cx.set_state(
21418 &r#"
21419 fn one() {
21420 let mut a = two();
21421 }
21422
21423 fn ˇtwo() {}"#
21424 .unindent(),
21425 );
21426 cx.lsp
21427 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21428 Ok(Some(vec![
21429 lsp::Location {
21430 uri: params.text_document_position.text_document.uri.clone(),
21431 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21432 },
21433 lsp::Location {
21434 uri: params.text_document_position.text_document.uri,
21435 range: lsp::Range::new(lsp::Position::new(4, 4), lsp::Position::new(4, 7)),
21436 },
21437 ]))
21438 });
21439 let navigated = cx
21440 .update_editor(|editor, window, cx| {
21441 editor.find_all_references(&FindAllReferences, window, cx)
21442 })
21443 .unwrap()
21444 .await
21445 .expect("Failed to navigate to references");
21446 assert_eq!(
21447 navigated,
21448 Navigated::Yes,
21449 "Should have navigated to references from the FindAllReferences response"
21450 );
21451 cx.assert_editor_state(
21452 &r#"fn one() {
21453 let mut a = two();
21454 }
21455
21456 fn ˇtwo() {}"#
21457 .unindent(),
21458 );
21459
21460 let editors = cx.update_workspace(|workspace, _, cx| {
21461 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21462 });
21463 cx.update_editor(|_, _, _| {
21464 assert_eq!(editors.len(), 2, "We should have opened a new multibuffer");
21465 });
21466
21467 cx.set_state(
21468 &r#"fn one() {
21469 let mut a = ˇtwo();
21470 }
21471
21472 fn two() {}"#
21473 .unindent(),
21474 );
21475 let navigated = cx
21476 .update_editor(|editor, window, cx| {
21477 editor.find_all_references(&FindAllReferences, window, cx)
21478 })
21479 .unwrap()
21480 .await
21481 .expect("Failed to navigate to references");
21482 assert_eq!(
21483 navigated,
21484 Navigated::Yes,
21485 "Should have navigated to references from the FindAllReferences response"
21486 );
21487 cx.assert_editor_state(
21488 &r#"fn one() {
21489 let mut a = ˇtwo();
21490 }
21491
21492 fn two() {}"#
21493 .unindent(),
21494 );
21495 let editors = cx.update_workspace(|workspace, _, cx| {
21496 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21497 });
21498 cx.update_editor(|_, _, _| {
21499 assert_eq!(
21500 editors.len(),
21501 2,
21502 "should have re-used the previous multibuffer"
21503 );
21504 });
21505
21506 cx.set_state(
21507 &r#"fn one() {
21508 let mut a = ˇtwo();
21509 }
21510 fn three() {}
21511 fn two() {}"#
21512 .unindent(),
21513 );
21514 cx.lsp
21515 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21516 Ok(Some(vec![
21517 lsp::Location {
21518 uri: params.text_document_position.text_document.uri.clone(),
21519 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21520 },
21521 lsp::Location {
21522 uri: params.text_document_position.text_document.uri,
21523 range: lsp::Range::new(lsp::Position::new(5, 4), lsp::Position::new(5, 7)),
21524 },
21525 ]))
21526 });
21527 let navigated = cx
21528 .update_editor(|editor, window, cx| {
21529 editor.find_all_references(&FindAllReferences, window, cx)
21530 })
21531 .unwrap()
21532 .await
21533 .expect("Failed to navigate to references");
21534 assert_eq!(
21535 navigated,
21536 Navigated::Yes,
21537 "Should have navigated to references from the FindAllReferences response"
21538 );
21539 cx.assert_editor_state(
21540 &r#"fn one() {
21541 let mut a = ˇtwo();
21542 }
21543 fn three() {}
21544 fn two() {}"#
21545 .unindent(),
21546 );
21547 let editors = cx.update_workspace(|workspace, _, cx| {
21548 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21549 });
21550 cx.update_editor(|_, _, _| {
21551 assert_eq!(
21552 editors.len(),
21553 3,
21554 "should have used a new multibuffer as offsets changed"
21555 );
21556 });
21557}
21558#[gpui::test]
21559async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
21560 init_test(cx, |_| {});
21561
21562 let language = Arc::new(Language::new(
21563 LanguageConfig::default(),
21564 Some(tree_sitter_rust::LANGUAGE.into()),
21565 ));
21566
21567 let text = r#"
21568 #[cfg(test)]
21569 mod tests() {
21570 #[test]
21571 fn runnable_1() {
21572 let a = 1;
21573 }
21574
21575 #[test]
21576 fn runnable_2() {
21577 let a = 1;
21578 let b = 2;
21579 }
21580 }
21581 "#
21582 .unindent();
21583
21584 let fs = FakeFs::new(cx.executor());
21585 fs.insert_file("/file.rs", Default::default()).await;
21586
21587 let project = Project::test(fs, ["/a".as_ref()], cx).await;
21588 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21589 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21590 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
21591 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
21592
21593 let editor = cx.new_window_entity(|window, cx| {
21594 Editor::new(
21595 EditorMode::full(),
21596 multi_buffer,
21597 Some(project.clone()),
21598 window,
21599 cx,
21600 )
21601 });
21602
21603 editor.update_in(cx, |editor, window, cx| {
21604 let snapshot = editor.buffer().read(cx).snapshot(cx);
21605 editor.tasks.insert(
21606 (buffer.read(cx).remote_id(), 3),
21607 RunnableTasks {
21608 templates: vec![],
21609 offset: snapshot.anchor_before(43),
21610 column: 0,
21611 extra_variables: HashMap::default(),
21612 context_range: BufferOffset(43)..BufferOffset(85),
21613 },
21614 );
21615 editor.tasks.insert(
21616 (buffer.read(cx).remote_id(), 8),
21617 RunnableTasks {
21618 templates: vec![],
21619 offset: snapshot.anchor_before(86),
21620 column: 0,
21621 extra_variables: HashMap::default(),
21622 context_range: BufferOffset(86)..BufferOffset(191),
21623 },
21624 );
21625
21626 // Test finding task when cursor is inside function body
21627 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21628 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
21629 });
21630 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
21631 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
21632
21633 // Test finding task when cursor is on function name
21634 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21635 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
21636 });
21637 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
21638 assert_eq!(row, 8, "Should find task when cursor is on function name");
21639 });
21640}
21641
21642#[gpui::test]
21643async fn test_folding_buffers(cx: &mut TestAppContext) {
21644 init_test(cx, |_| {});
21645
21646 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
21647 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
21648 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
21649
21650 let fs = FakeFs::new(cx.executor());
21651 fs.insert_tree(
21652 path!("/a"),
21653 json!({
21654 "first.rs": sample_text_1,
21655 "second.rs": sample_text_2,
21656 "third.rs": sample_text_3,
21657 }),
21658 )
21659 .await;
21660 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21661 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21662 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21663 let worktree = project.update(cx, |project, cx| {
21664 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
21665 assert_eq!(worktrees.len(), 1);
21666 worktrees.pop().unwrap()
21667 });
21668 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
21669
21670 let buffer_1 = project
21671 .update(cx, |project, cx| {
21672 project.open_buffer((worktree_id, "first.rs"), cx)
21673 })
21674 .await
21675 .unwrap();
21676 let buffer_2 = project
21677 .update(cx, |project, cx| {
21678 project.open_buffer((worktree_id, "second.rs"), cx)
21679 })
21680 .await
21681 .unwrap();
21682 let buffer_3 = project
21683 .update(cx, |project, cx| {
21684 project.open_buffer((worktree_id, "third.rs"), cx)
21685 })
21686 .await
21687 .unwrap();
21688
21689 let multi_buffer = cx.new(|cx| {
21690 let mut multi_buffer = MultiBuffer::new(ReadWrite);
21691 multi_buffer.push_excerpts(
21692 buffer_1.clone(),
21693 [
21694 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21695 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21696 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21697 ],
21698 cx,
21699 );
21700 multi_buffer.push_excerpts(
21701 buffer_2.clone(),
21702 [
21703 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21704 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21705 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21706 ],
21707 cx,
21708 );
21709 multi_buffer.push_excerpts(
21710 buffer_3.clone(),
21711 [
21712 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21713 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21714 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21715 ],
21716 cx,
21717 );
21718 multi_buffer
21719 });
21720 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
21721 Editor::new(
21722 EditorMode::full(),
21723 multi_buffer.clone(),
21724 Some(project.clone()),
21725 window,
21726 cx,
21727 )
21728 });
21729
21730 assert_eq!(
21731 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21732 "\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",
21733 );
21734
21735 multi_buffer_editor.update(cx, |editor, cx| {
21736 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
21737 });
21738 assert_eq!(
21739 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21740 "\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",
21741 "After folding the first buffer, its text should not be displayed"
21742 );
21743
21744 multi_buffer_editor.update(cx, |editor, cx| {
21745 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
21746 });
21747 assert_eq!(
21748 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21749 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
21750 "After folding the second buffer, its text should not be displayed"
21751 );
21752
21753 multi_buffer_editor.update(cx, |editor, cx| {
21754 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
21755 });
21756 assert_eq!(
21757 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21758 "\n\n\n\n\n",
21759 "After folding the third buffer, its text should not be displayed"
21760 );
21761
21762 // Emulate selection inside the fold logic, that should work
21763 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21764 editor
21765 .snapshot(window, cx)
21766 .next_line_boundary(Point::new(0, 4));
21767 });
21768
21769 multi_buffer_editor.update(cx, |editor, cx| {
21770 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
21771 });
21772 assert_eq!(
21773 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21774 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
21775 "After unfolding the second buffer, its text should be displayed"
21776 );
21777
21778 // Typing inside of buffer 1 causes that buffer to be unfolded.
21779 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21780 assert_eq!(
21781 multi_buffer
21782 .read(cx)
21783 .snapshot(cx)
21784 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
21785 .collect::<String>(),
21786 "bbbb"
21787 );
21788 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
21789 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
21790 });
21791 editor.handle_input("B", window, cx);
21792 });
21793
21794 assert_eq!(
21795 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21796 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
21797 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
21798 );
21799
21800 multi_buffer_editor.update(cx, |editor, cx| {
21801 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
21802 });
21803 assert_eq!(
21804 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21805 "\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",
21806 "After unfolding the all buffers, all original text should be displayed"
21807 );
21808}
21809
21810#[gpui::test]
21811async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
21812 init_test(cx, |_| {});
21813
21814 let sample_text_1 = "1111\n2222\n3333".to_string();
21815 let sample_text_2 = "4444\n5555\n6666".to_string();
21816 let sample_text_3 = "7777\n8888\n9999".to_string();
21817
21818 let fs = FakeFs::new(cx.executor());
21819 fs.insert_tree(
21820 path!("/a"),
21821 json!({
21822 "first.rs": sample_text_1,
21823 "second.rs": sample_text_2,
21824 "third.rs": sample_text_3,
21825 }),
21826 )
21827 .await;
21828 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21829 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21830 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21831 let worktree = project.update(cx, |project, cx| {
21832 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
21833 assert_eq!(worktrees.len(), 1);
21834 worktrees.pop().unwrap()
21835 });
21836 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
21837
21838 let buffer_1 = project
21839 .update(cx, |project, cx| {
21840 project.open_buffer((worktree_id, "first.rs"), cx)
21841 })
21842 .await
21843 .unwrap();
21844 let buffer_2 = project
21845 .update(cx, |project, cx| {
21846 project.open_buffer((worktree_id, "second.rs"), cx)
21847 })
21848 .await
21849 .unwrap();
21850 let buffer_3 = project
21851 .update(cx, |project, cx| {
21852 project.open_buffer((worktree_id, "third.rs"), cx)
21853 })
21854 .await
21855 .unwrap();
21856
21857 let multi_buffer = cx.new(|cx| {
21858 let mut multi_buffer = MultiBuffer::new(ReadWrite);
21859 multi_buffer.push_excerpts(
21860 buffer_1.clone(),
21861 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21862 cx,
21863 );
21864 multi_buffer.push_excerpts(
21865 buffer_2.clone(),
21866 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21867 cx,
21868 );
21869 multi_buffer.push_excerpts(
21870 buffer_3.clone(),
21871 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21872 cx,
21873 );
21874 multi_buffer
21875 });
21876
21877 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
21878 Editor::new(
21879 EditorMode::full(),
21880 multi_buffer,
21881 Some(project.clone()),
21882 window,
21883 cx,
21884 )
21885 });
21886
21887 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
21888 assert_eq!(
21889 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21890 full_text,
21891 );
21892
21893 multi_buffer_editor.update(cx, |editor, cx| {
21894 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
21895 });
21896 assert_eq!(
21897 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21898 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
21899 "After folding the first buffer, its text should not be displayed"
21900 );
21901
21902 multi_buffer_editor.update(cx, |editor, cx| {
21903 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
21904 });
21905
21906 assert_eq!(
21907 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21908 "\n\n\n\n\n\n7777\n8888\n9999",
21909 "After folding the second buffer, its text should not be displayed"
21910 );
21911
21912 multi_buffer_editor.update(cx, |editor, cx| {
21913 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
21914 });
21915 assert_eq!(
21916 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21917 "\n\n\n\n\n",
21918 "After folding the third buffer, its text should not be displayed"
21919 );
21920
21921 multi_buffer_editor.update(cx, |editor, cx| {
21922 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
21923 });
21924 assert_eq!(
21925 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21926 "\n\n\n\n4444\n5555\n6666\n\n",
21927 "After unfolding the second buffer, its text should be displayed"
21928 );
21929
21930 multi_buffer_editor.update(cx, |editor, cx| {
21931 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
21932 });
21933 assert_eq!(
21934 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21935 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
21936 "After unfolding the first buffer, its text should be displayed"
21937 );
21938
21939 multi_buffer_editor.update(cx, |editor, cx| {
21940 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
21941 });
21942 assert_eq!(
21943 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21944 full_text,
21945 "After unfolding all buffers, all original text should be displayed"
21946 );
21947}
21948
21949#[gpui::test]
21950async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
21951 init_test(cx, |_| {});
21952
21953 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
21954
21955 let fs = FakeFs::new(cx.executor());
21956 fs.insert_tree(
21957 path!("/a"),
21958 json!({
21959 "main.rs": sample_text,
21960 }),
21961 )
21962 .await;
21963 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21964 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21965 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21966 let worktree = project.update(cx, |project, cx| {
21967 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
21968 assert_eq!(worktrees.len(), 1);
21969 worktrees.pop().unwrap()
21970 });
21971 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
21972
21973 let buffer_1 = project
21974 .update(cx, |project, cx| {
21975 project.open_buffer((worktree_id, "main.rs"), cx)
21976 })
21977 .await
21978 .unwrap();
21979
21980 let multi_buffer = cx.new(|cx| {
21981 let mut multi_buffer = MultiBuffer::new(ReadWrite);
21982 multi_buffer.push_excerpts(
21983 buffer_1.clone(),
21984 [ExcerptRange::new(
21985 Point::new(0, 0)
21986 ..Point::new(
21987 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
21988 0,
21989 ),
21990 )],
21991 cx,
21992 );
21993 multi_buffer
21994 });
21995 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
21996 Editor::new(
21997 EditorMode::full(),
21998 multi_buffer,
21999 Some(project.clone()),
22000 window,
22001 cx,
22002 )
22003 });
22004
22005 let selection_range = Point::new(1, 0)..Point::new(2, 0);
22006 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22007 enum TestHighlight {}
22008 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
22009 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
22010 editor.highlight_text::<TestHighlight>(
22011 vec![highlight_range.clone()],
22012 HighlightStyle::color(Hsla::green()),
22013 cx,
22014 );
22015 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22016 s.select_ranges(Some(highlight_range))
22017 });
22018 });
22019
22020 let full_text = format!("\n\n{sample_text}");
22021 assert_eq!(
22022 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22023 full_text,
22024 );
22025}
22026
22027#[gpui::test]
22028async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
22029 init_test(cx, |_| {});
22030 cx.update(|cx| {
22031 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
22032 "keymaps/default-linux.json",
22033 cx,
22034 )
22035 .unwrap();
22036 cx.bind_keys(default_key_bindings);
22037 });
22038
22039 let (editor, cx) = cx.add_window_view(|window, cx| {
22040 let multi_buffer = MultiBuffer::build_multi(
22041 [
22042 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
22043 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
22044 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
22045 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
22046 ],
22047 cx,
22048 );
22049 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
22050
22051 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
22052 // fold all but the second buffer, so that we test navigating between two
22053 // adjacent folded buffers, as well as folded buffers at the start and
22054 // end the multibuffer
22055 editor.fold_buffer(buffer_ids[0], cx);
22056 editor.fold_buffer(buffer_ids[2], cx);
22057 editor.fold_buffer(buffer_ids[3], cx);
22058
22059 editor
22060 });
22061 cx.simulate_resize(size(px(1000.), px(1000.)));
22062
22063 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
22064 cx.assert_excerpts_with_selections(indoc! {"
22065 [EXCERPT]
22066 ˇ[FOLDED]
22067 [EXCERPT]
22068 a1
22069 b1
22070 [EXCERPT]
22071 [FOLDED]
22072 [EXCERPT]
22073 [FOLDED]
22074 "
22075 });
22076 cx.simulate_keystroke("down");
22077 cx.assert_excerpts_with_selections(indoc! {"
22078 [EXCERPT]
22079 [FOLDED]
22080 [EXCERPT]
22081 ˇa1
22082 b1
22083 [EXCERPT]
22084 [FOLDED]
22085 [EXCERPT]
22086 [FOLDED]
22087 "
22088 });
22089 cx.simulate_keystroke("down");
22090 cx.assert_excerpts_with_selections(indoc! {"
22091 [EXCERPT]
22092 [FOLDED]
22093 [EXCERPT]
22094 a1
22095 ˇb1
22096 [EXCERPT]
22097 [FOLDED]
22098 [EXCERPT]
22099 [FOLDED]
22100 "
22101 });
22102 cx.simulate_keystroke("down");
22103 cx.assert_excerpts_with_selections(indoc! {"
22104 [EXCERPT]
22105 [FOLDED]
22106 [EXCERPT]
22107 a1
22108 b1
22109 ˇ[EXCERPT]
22110 [FOLDED]
22111 [EXCERPT]
22112 [FOLDED]
22113 "
22114 });
22115 cx.simulate_keystroke("down");
22116 cx.assert_excerpts_with_selections(indoc! {"
22117 [EXCERPT]
22118 [FOLDED]
22119 [EXCERPT]
22120 a1
22121 b1
22122 [EXCERPT]
22123 ˇ[FOLDED]
22124 [EXCERPT]
22125 [FOLDED]
22126 "
22127 });
22128 for _ in 0..5 {
22129 cx.simulate_keystroke("down");
22130 cx.assert_excerpts_with_selections(indoc! {"
22131 [EXCERPT]
22132 [FOLDED]
22133 [EXCERPT]
22134 a1
22135 b1
22136 [EXCERPT]
22137 [FOLDED]
22138 [EXCERPT]
22139 ˇ[FOLDED]
22140 "
22141 });
22142 }
22143
22144 cx.simulate_keystroke("up");
22145 cx.assert_excerpts_with_selections(indoc! {"
22146 [EXCERPT]
22147 [FOLDED]
22148 [EXCERPT]
22149 a1
22150 b1
22151 [EXCERPT]
22152 ˇ[FOLDED]
22153 [EXCERPT]
22154 [FOLDED]
22155 "
22156 });
22157 cx.simulate_keystroke("up");
22158 cx.assert_excerpts_with_selections(indoc! {"
22159 [EXCERPT]
22160 [FOLDED]
22161 [EXCERPT]
22162 a1
22163 b1
22164 ˇ[EXCERPT]
22165 [FOLDED]
22166 [EXCERPT]
22167 [FOLDED]
22168 "
22169 });
22170 cx.simulate_keystroke("up");
22171 cx.assert_excerpts_with_selections(indoc! {"
22172 [EXCERPT]
22173 [FOLDED]
22174 [EXCERPT]
22175 a1
22176 ˇb1
22177 [EXCERPT]
22178 [FOLDED]
22179 [EXCERPT]
22180 [FOLDED]
22181 "
22182 });
22183 cx.simulate_keystroke("up");
22184 cx.assert_excerpts_with_selections(indoc! {"
22185 [EXCERPT]
22186 [FOLDED]
22187 [EXCERPT]
22188 ˇa1
22189 b1
22190 [EXCERPT]
22191 [FOLDED]
22192 [EXCERPT]
22193 [FOLDED]
22194 "
22195 });
22196 for _ in 0..5 {
22197 cx.simulate_keystroke("up");
22198 cx.assert_excerpts_with_selections(indoc! {"
22199 [EXCERPT]
22200 ˇ[FOLDED]
22201 [EXCERPT]
22202 a1
22203 b1
22204 [EXCERPT]
22205 [FOLDED]
22206 [EXCERPT]
22207 [FOLDED]
22208 "
22209 });
22210 }
22211}
22212
22213#[gpui::test]
22214async fn test_edit_prediction_text(cx: &mut TestAppContext) {
22215 init_test(cx, |_| {});
22216
22217 // Simple insertion
22218 assert_highlighted_edits(
22219 "Hello, world!",
22220 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
22221 true,
22222 cx,
22223 |highlighted_edits, cx| {
22224 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
22225 assert_eq!(highlighted_edits.highlights.len(), 1);
22226 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
22227 assert_eq!(
22228 highlighted_edits.highlights[0].1.background_color,
22229 Some(cx.theme().status().created_background)
22230 );
22231 },
22232 )
22233 .await;
22234
22235 // Replacement
22236 assert_highlighted_edits(
22237 "This is a test.",
22238 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
22239 false,
22240 cx,
22241 |highlighted_edits, cx| {
22242 assert_eq!(highlighted_edits.text, "That is a test.");
22243 assert_eq!(highlighted_edits.highlights.len(), 1);
22244 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
22245 assert_eq!(
22246 highlighted_edits.highlights[0].1.background_color,
22247 Some(cx.theme().status().created_background)
22248 );
22249 },
22250 )
22251 .await;
22252
22253 // Multiple edits
22254 assert_highlighted_edits(
22255 "Hello, world!",
22256 vec![
22257 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
22258 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
22259 ],
22260 false,
22261 cx,
22262 |highlighted_edits, cx| {
22263 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
22264 assert_eq!(highlighted_edits.highlights.len(), 2);
22265 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
22266 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
22267 assert_eq!(
22268 highlighted_edits.highlights[0].1.background_color,
22269 Some(cx.theme().status().created_background)
22270 );
22271 assert_eq!(
22272 highlighted_edits.highlights[1].1.background_color,
22273 Some(cx.theme().status().created_background)
22274 );
22275 },
22276 )
22277 .await;
22278
22279 // Multiple lines with edits
22280 assert_highlighted_edits(
22281 "First line\nSecond line\nThird line\nFourth line",
22282 vec![
22283 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
22284 (
22285 Point::new(2, 0)..Point::new(2, 10),
22286 "New third line".to_string(),
22287 ),
22288 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
22289 ],
22290 false,
22291 cx,
22292 |highlighted_edits, cx| {
22293 assert_eq!(
22294 highlighted_edits.text,
22295 "Second modified\nNew third line\nFourth updated line"
22296 );
22297 assert_eq!(highlighted_edits.highlights.len(), 3);
22298 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
22299 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
22300 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
22301 for highlight in &highlighted_edits.highlights {
22302 assert_eq!(
22303 highlight.1.background_color,
22304 Some(cx.theme().status().created_background)
22305 );
22306 }
22307 },
22308 )
22309 .await;
22310}
22311
22312#[gpui::test]
22313async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
22314 init_test(cx, |_| {});
22315
22316 // Deletion
22317 assert_highlighted_edits(
22318 "Hello, world!",
22319 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
22320 true,
22321 cx,
22322 |highlighted_edits, cx| {
22323 assert_eq!(highlighted_edits.text, "Hello, world!");
22324 assert_eq!(highlighted_edits.highlights.len(), 1);
22325 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
22326 assert_eq!(
22327 highlighted_edits.highlights[0].1.background_color,
22328 Some(cx.theme().status().deleted_background)
22329 );
22330 },
22331 )
22332 .await;
22333
22334 // Insertion
22335 assert_highlighted_edits(
22336 "Hello, world!",
22337 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
22338 true,
22339 cx,
22340 |highlighted_edits, cx| {
22341 assert_eq!(highlighted_edits.highlights.len(), 1);
22342 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
22343 assert_eq!(
22344 highlighted_edits.highlights[0].1.background_color,
22345 Some(cx.theme().status().created_background)
22346 );
22347 },
22348 )
22349 .await;
22350}
22351
22352async fn assert_highlighted_edits(
22353 text: &str,
22354 edits: Vec<(Range<Point>, String)>,
22355 include_deletions: bool,
22356 cx: &mut TestAppContext,
22357 assertion_fn: impl Fn(HighlightedText, &App),
22358) {
22359 let window = cx.add_window(|window, cx| {
22360 let buffer = MultiBuffer::build_simple(text, cx);
22361 Editor::new(EditorMode::full(), buffer, None, window, cx)
22362 });
22363 let cx = &mut VisualTestContext::from_window(*window, cx);
22364
22365 let (buffer, snapshot) = window
22366 .update(cx, |editor, _window, cx| {
22367 (
22368 editor.buffer().clone(),
22369 editor.buffer().read(cx).snapshot(cx),
22370 )
22371 })
22372 .unwrap();
22373
22374 let edits = edits
22375 .into_iter()
22376 .map(|(range, edit)| {
22377 (
22378 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
22379 edit,
22380 )
22381 })
22382 .collect::<Vec<_>>();
22383
22384 let text_anchor_edits = edits
22385 .clone()
22386 .into_iter()
22387 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
22388 .collect::<Vec<_>>();
22389
22390 let edit_preview = window
22391 .update(cx, |_, _window, cx| {
22392 buffer
22393 .read(cx)
22394 .as_singleton()
22395 .unwrap()
22396 .read(cx)
22397 .preview_edits(text_anchor_edits.into(), cx)
22398 })
22399 .unwrap()
22400 .await;
22401
22402 cx.update(|_window, cx| {
22403 let highlighted_edits = edit_prediction_edit_text(
22404 snapshot.as_singleton().unwrap().2,
22405 &edits,
22406 &edit_preview,
22407 include_deletions,
22408 cx,
22409 );
22410 assertion_fn(highlighted_edits, cx)
22411 });
22412}
22413
22414#[track_caller]
22415fn assert_breakpoint(
22416 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
22417 path: &Arc<Path>,
22418 expected: Vec<(u32, Breakpoint)>,
22419) {
22420 if expected.is_empty() {
22421 assert!(!breakpoints.contains_key(path), "{}", path.display());
22422 } else {
22423 let mut breakpoint = breakpoints
22424 .get(path)
22425 .unwrap()
22426 .iter()
22427 .map(|breakpoint| {
22428 (
22429 breakpoint.row,
22430 Breakpoint {
22431 message: breakpoint.message.clone(),
22432 state: breakpoint.state,
22433 condition: breakpoint.condition.clone(),
22434 hit_condition: breakpoint.hit_condition.clone(),
22435 },
22436 )
22437 })
22438 .collect::<Vec<_>>();
22439
22440 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
22441
22442 assert_eq!(expected, breakpoint);
22443 }
22444}
22445
22446fn add_log_breakpoint_at_cursor(
22447 editor: &mut Editor,
22448 log_message: &str,
22449 window: &mut Window,
22450 cx: &mut Context<Editor>,
22451) {
22452 let (anchor, bp) = editor
22453 .breakpoints_at_cursors(window, cx)
22454 .first()
22455 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
22456 .unwrap_or_else(|| {
22457 let cursor_position: Point = editor.selections.newest(cx).head();
22458
22459 let breakpoint_position = editor
22460 .snapshot(window, cx)
22461 .display_snapshot
22462 .buffer_snapshot
22463 .anchor_before(Point::new(cursor_position.row, 0));
22464
22465 (breakpoint_position, Breakpoint::new_log(log_message))
22466 });
22467
22468 editor.edit_breakpoint_at_anchor(
22469 anchor,
22470 bp,
22471 BreakpointEditAction::EditLogMessage(log_message.into()),
22472 cx,
22473 );
22474}
22475
22476#[gpui::test]
22477async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
22478 init_test(cx, |_| {});
22479
22480 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22481 let fs = FakeFs::new(cx.executor());
22482 fs.insert_tree(
22483 path!("/a"),
22484 json!({
22485 "main.rs": sample_text,
22486 }),
22487 )
22488 .await;
22489 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22490 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22491 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22492
22493 let fs = FakeFs::new(cx.executor());
22494 fs.insert_tree(
22495 path!("/a"),
22496 json!({
22497 "main.rs": sample_text,
22498 }),
22499 )
22500 .await;
22501 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22502 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22503 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22504 let worktree_id = workspace
22505 .update(cx, |workspace, _window, cx| {
22506 workspace.project().update(cx, |project, cx| {
22507 project.worktrees(cx).next().unwrap().read(cx).id()
22508 })
22509 })
22510 .unwrap();
22511
22512 let buffer = project
22513 .update(cx, |project, cx| {
22514 project.open_buffer((worktree_id, "main.rs"), cx)
22515 })
22516 .await
22517 .unwrap();
22518
22519 let (editor, cx) = cx.add_window_view(|window, cx| {
22520 Editor::new(
22521 EditorMode::full(),
22522 MultiBuffer::build_from_buffer(buffer, cx),
22523 Some(project.clone()),
22524 window,
22525 cx,
22526 )
22527 });
22528
22529 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22530 let abs_path = project.read_with(cx, |project, cx| {
22531 project
22532 .absolute_path(&project_path, cx)
22533 .map(Arc::from)
22534 .unwrap()
22535 });
22536
22537 // assert we can add breakpoint on the first line
22538 editor.update_in(cx, |editor, window, cx| {
22539 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22540 editor.move_to_end(&MoveToEnd, window, cx);
22541 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22542 });
22543
22544 let breakpoints = editor.update(cx, |editor, cx| {
22545 editor
22546 .breakpoint_store()
22547 .as_ref()
22548 .unwrap()
22549 .read(cx)
22550 .all_source_breakpoints(cx)
22551 });
22552
22553 assert_eq!(1, breakpoints.len());
22554 assert_breakpoint(
22555 &breakpoints,
22556 &abs_path,
22557 vec![
22558 (0, Breakpoint::new_standard()),
22559 (3, Breakpoint::new_standard()),
22560 ],
22561 );
22562
22563 editor.update_in(cx, |editor, window, cx| {
22564 editor.move_to_beginning(&MoveToBeginning, window, cx);
22565 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22566 });
22567
22568 let breakpoints = editor.update(cx, |editor, cx| {
22569 editor
22570 .breakpoint_store()
22571 .as_ref()
22572 .unwrap()
22573 .read(cx)
22574 .all_source_breakpoints(cx)
22575 });
22576
22577 assert_eq!(1, breakpoints.len());
22578 assert_breakpoint(
22579 &breakpoints,
22580 &abs_path,
22581 vec![(3, Breakpoint::new_standard())],
22582 );
22583
22584 editor.update_in(cx, |editor, window, cx| {
22585 editor.move_to_end(&MoveToEnd, window, cx);
22586 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22587 });
22588
22589 let breakpoints = editor.update(cx, |editor, cx| {
22590 editor
22591 .breakpoint_store()
22592 .as_ref()
22593 .unwrap()
22594 .read(cx)
22595 .all_source_breakpoints(cx)
22596 });
22597
22598 assert_eq!(0, breakpoints.len());
22599 assert_breakpoint(&breakpoints, &abs_path, vec![]);
22600}
22601
22602#[gpui::test]
22603async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
22604 init_test(cx, |_| {});
22605
22606 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22607
22608 let fs = FakeFs::new(cx.executor());
22609 fs.insert_tree(
22610 path!("/a"),
22611 json!({
22612 "main.rs": sample_text,
22613 }),
22614 )
22615 .await;
22616 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22617 let (workspace, cx) =
22618 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22619
22620 let worktree_id = workspace.update(cx, |workspace, cx| {
22621 workspace.project().update(cx, |project, cx| {
22622 project.worktrees(cx).next().unwrap().read(cx).id()
22623 })
22624 });
22625
22626 let buffer = project
22627 .update(cx, |project, cx| {
22628 project.open_buffer((worktree_id, "main.rs"), cx)
22629 })
22630 .await
22631 .unwrap();
22632
22633 let (editor, cx) = cx.add_window_view(|window, cx| {
22634 Editor::new(
22635 EditorMode::full(),
22636 MultiBuffer::build_from_buffer(buffer, cx),
22637 Some(project.clone()),
22638 window,
22639 cx,
22640 )
22641 });
22642
22643 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22644 let abs_path = project.read_with(cx, |project, cx| {
22645 project
22646 .absolute_path(&project_path, cx)
22647 .map(Arc::from)
22648 .unwrap()
22649 });
22650
22651 editor.update_in(cx, |editor, window, cx| {
22652 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
22653 });
22654
22655 let breakpoints = editor.update(cx, |editor, cx| {
22656 editor
22657 .breakpoint_store()
22658 .as_ref()
22659 .unwrap()
22660 .read(cx)
22661 .all_source_breakpoints(cx)
22662 });
22663
22664 assert_breakpoint(
22665 &breakpoints,
22666 &abs_path,
22667 vec![(0, Breakpoint::new_log("hello world"))],
22668 );
22669
22670 // Removing a log message from a log breakpoint should remove it
22671 editor.update_in(cx, |editor, window, cx| {
22672 add_log_breakpoint_at_cursor(editor, "", window, cx);
22673 });
22674
22675 let breakpoints = editor.update(cx, |editor, cx| {
22676 editor
22677 .breakpoint_store()
22678 .as_ref()
22679 .unwrap()
22680 .read(cx)
22681 .all_source_breakpoints(cx)
22682 });
22683
22684 assert_breakpoint(&breakpoints, &abs_path, vec![]);
22685
22686 editor.update_in(cx, |editor, window, cx| {
22687 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22688 editor.move_to_end(&MoveToEnd, window, cx);
22689 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22690 // Not adding a log message to a standard breakpoint shouldn't remove it
22691 add_log_breakpoint_at_cursor(editor, "", window, cx);
22692 });
22693
22694 let breakpoints = editor.update(cx, |editor, cx| {
22695 editor
22696 .breakpoint_store()
22697 .as_ref()
22698 .unwrap()
22699 .read(cx)
22700 .all_source_breakpoints(cx)
22701 });
22702
22703 assert_breakpoint(
22704 &breakpoints,
22705 &abs_path,
22706 vec![
22707 (0, Breakpoint::new_standard()),
22708 (3, Breakpoint::new_standard()),
22709 ],
22710 );
22711
22712 editor.update_in(cx, |editor, window, cx| {
22713 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
22714 });
22715
22716 let breakpoints = editor.update(cx, |editor, cx| {
22717 editor
22718 .breakpoint_store()
22719 .as_ref()
22720 .unwrap()
22721 .read(cx)
22722 .all_source_breakpoints(cx)
22723 });
22724
22725 assert_breakpoint(
22726 &breakpoints,
22727 &abs_path,
22728 vec![
22729 (0, Breakpoint::new_standard()),
22730 (3, Breakpoint::new_log("hello world")),
22731 ],
22732 );
22733
22734 editor.update_in(cx, |editor, window, cx| {
22735 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
22736 });
22737
22738 let breakpoints = editor.update(cx, |editor, cx| {
22739 editor
22740 .breakpoint_store()
22741 .as_ref()
22742 .unwrap()
22743 .read(cx)
22744 .all_source_breakpoints(cx)
22745 });
22746
22747 assert_breakpoint(
22748 &breakpoints,
22749 &abs_path,
22750 vec![
22751 (0, Breakpoint::new_standard()),
22752 (3, Breakpoint::new_log("hello Earth!!")),
22753 ],
22754 );
22755}
22756
22757/// This also tests that Editor::breakpoint_at_cursor_head is working properly
22758/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
22759/// or when breakpoints were placed out of order. This tests for a regression too
22760#[gpui::test]
22761async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
22762 init_test(cx, |_| {});
22763
22764 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22765 let fs = FakeFs::new(cx.executor());
22766 fs.insert_tree(
22767 path!("/a"),
22768 json!({
22769 "main.rs": sample_text,
22770 }),
22771 )
22772 .await;
22773 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22774 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22775 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22776
22777 let fs = FakeFs::new(cx.executor());
22778 fs.insert_tree(
22779 path!("/a"),
22780 json!({
22781 "main.rs": sample_text,
22782 }),
22783 )
22784 .await;
22785 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22786 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22787 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22788 let worktree_id = workspace
22789 .update(cx, |workspace, _window, cx| {
22790 workspace.project().update(cx, |project, cx| {
22791 project.worktrees(cx).next().unwrap().read(cx).id()
22792 })
22793 })
22794 .unwrap();
22795
22796 let buffer = project
22797 .update(cx, |project, cx| {
22798 project.open_buffer((worktree_id, "main.rs"), cx)
22799 })
22800 .await
22801 .unwrap();
22802
22803 let (editor, cx) = cx.add_window_view(|window, cx| {
22804 Editor::new(
22805 EditorMode::full(),
22806 MultiBuffer::build_from_buffer(buffer, cx),
22807 Some(project.clone()),
22808 window,
22809 cx,
22810 )
22811 });
22812
22813 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22814 let abs_path = project.read_with(cx, |project, cx| {
22815 project
22816 .absolute_path(&project_path, cx)
22817 .map(Arc::from)
22818 .unwrap()
22819 });
22820
22821 // assert we can add breakpoint on the first line
22822 editor.update_in(cx, |editor, window, cx| {
22823 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22824 editor.move_to_end(&MoveToEnd, window, cx);
22825 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22826 editor.move_up(&MoveUp, window, cx);
22827 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22828 });
22829
22830 let breakpoints = editor.update(cx, |editor, cx| {
22831 editor
22832 .breakpoint_store()
22833 .as_ref()
22834 .unwrap()
22835 .read(cx)
22836 .all_source_breakpoints(cx)
22837 });
22838
22839 assert_eq!(1, breakpoints.len());
22840 assert_breakpoint(
22841 &breakpoints,
22842 &abs_path,
22843 vec![
22844 (0, Breakpoint::new_standard()),
22845 (2, Breakpoint::new_standard()),
22846 (3, Breakpoint::new_standard()),
22847 ],
22848 );
22849
22850 editor.update_in(cx, |editor, window, cx| {
22851 editor.move_to_beginning(&MoveToBeginning, window, cx);
22852 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22853 editor.move_to_end(&MoveToEnd, window, cx);
22854 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22855 // Disabling a breakpoint that doesn't exist should do nothing
22856 editor.move_up(&MoveUp, window, cx);
22857 editor.move_up(&MoveUp, window, cx);
22858 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22859 });
22860
22861 let breakpoints = editor.update(cx, |editor, cx| {
22862 editor
22863 .breakpoint_store()
22864 .as_ref()
22865 .unwrap()
22866 .read(cx)
22867 .all_source_breakpoints(cx)
22868 });
22869
22870 let disable_breakpoint = {
22871 let mut bp = Breakpoint::new_standard();
22872 bp.state = BreakpointState::Disabled;
22873 bp
22874 };
22875
22876 assert_eq!(1, breakpoints.len());
22877 assert_breakpoint(
22878 &breakpoints,
22879 &abs_path,
22880 vec![
22881 (0, disable_breakpoint.clone()),
22882 (2, Breakpoint::new_standard()),
22883 (3, disable_breakpoint.clone()),
22884 ],
22885 );
22886
22887 editor.update_in(cx, |editor, window, cx| {
22888 editor.move_to_beginning(&MoveToBeginning, window, cx);
22889 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
22890 editor.move_to_end(&MoveToEnd, window, cx);
22891 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
22892 editor.move_up(&MoveUp, window, cx);
22893 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22894 });
22895
22896 let breakpoints = editor.update(cx, |editor, cx| {
22897 editor
22898 .breakpoint_store()
22899 .as_ref()
22900 .unwrap()
22901 .read(cx)
22902 .all_source_breakpoints(cx)
22903 });
22904
22905 assert_eq!(1, breakpoints.len());
22906 assert_breakpoint(
22907 &breakpoints,
22908 &abs_path,
22909 vec![
22910 (0, Breakpoint::new_standard()),
22911 (2, disable_breakpoint),
22912 (3, Breakpoint::new_standard()),
22913 ],
22914 );
22915}
22916
22917#[gpui::test]
22918async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
22919 init_test(cx, |_| {});
22920 let capabilities = lsp::ServerCapabilities {
22921 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
22922 prepare_provider: Some(true),
22923 work_done_progress_options: Default::default(),
22924 })),
22925 ..Default::default()
22926 };
22927 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
22928
22929 cx.set_state(indoc! {"
22930 struct Fˇoo {}
22931 "});
22932
22933 cx.update_editor(|editor, _, cx| {
22934 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
22935 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
22936 editor.highlight_background::<DocumentHighlightRead>(
22937 &[highlight_range],
22938 |theme| theme.colors().editor_document_highlight_read_background,
22939 cx,
22940 );
22941 });
22942
22943 let mut prepare_rename_handler = cx
22944 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
22945 move |_, _, _| async move {
22946 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
22947 start: lsp::Position {
22948 line: 0,
22949 character: 7,
22950 },
22951 end: lsp::Position {
22952 line: 0,
22953 character: 10,
22954 },
22955 })))
22956 },
22957 );
22958 let prepare_rename_task = cx
22959 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
22960 .expect("Prepare rename was not started");
22961 prepare_rename_handler.next().await.unwrap();
22962 prepare_rename_task.await.expect("Prepare rename failed");
22963
22964 let mut rename_handler =
22965 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
22966 let edit = lsp::TextEdit {
22967 range: lsp::Range {
22968 start: lsp::Position {
22969 line: 0,
22970 character: 7,
22971 },
22972 end: lsp::Position {
22973 line: 0,
22974 character: 10,
22975 },
22976 },
22977 new_text: "FooRenamed".to_string(),
22978 };
22979 Ok(Some(lsp::WorkspaceEdit::new(
22980 // Specify the same edit twice
22981 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
22982 )))
22983 });
22984 let rename_task = cx
22985 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
22986 .expect("Confirm rename was not started");
22987 rename_handler.next().await.unwrap();
22988 rename_task.await.expect("Confirm rename failed");
22989 cx.run_until_parked();
22990
22991 // Despite two edits, only one is actually applied as those are identical
22992 cx.assert_editor_state(indoc! {"
22993 struct FooRenamedˇ {}
22994 "});
22995}
22996
22997#[gpui::test]
22998async fn test_rename_without_prepare(cx: &mut TestAppContext) {
22999 init_test(cx, |_| {});
23000 // These capabilities indicate that the server does not support prepare rename.
23001 let capabilities = lsp::ServerCapabilities {
23002 rename_provider: Some(lsp::OneOf::Left(true)),
23003 ..Default::default()
23004 };
23005 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23006
23007 cx.set_state(indoc! {"
23008 struct Fˇoo {}
23009 "});
23010
23011 cx.update_editor(|editor, _window, cx| {
23012 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23013 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23014 editor.highlight_background::<DocumentHighlightRead>(
23015 &[highlight_range],
23016 |theme| theme.colors().editor_document_highlight_read_background,
23017 cx,
23018 );
23019 });
23020
23021 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23022 .expect("Prepare rename was not started")
23023 .await
23024 .expect("Prepare rename failed");
23025
23026 let mut rename_handler =
23027 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23028 let edit = lsp::TextEdit {
23029 range: lsp::Range {
23030 start: lsp::Position {
23031 line: 0,
23032 character: 7,
23033 },
23034 end: lsp::Position {
23035 line: 0,
23036 character: 10,
23037 },
23038 },
23039 new_text: "FooRenamed".to_string(),
23040 };
23041 Ok(Some(lsp::WorkspaceEdit::new(
23042 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
23043 )))
23044 });
23045 let rename_task = cx
23046 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23047 .expect("Confirm rename was not started");
23048 rename_handler.next().await.unwrap();
23049 rename_task.await.expect("Confirm rename failed");
23050 cx.run_until_parked();
23051
23052 // Correct range is renamed, as `surrounding_word` is used to find it.
23053 cx.assert_editor_state(indoc! {"
23054 struct FooRenamedˇ {}
23055 "});
23056}
23057
23058#[gpui::test]
23059async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
23060 init_test(cx, |_| {});
23061 let mut cx = EditorTestContext::new(cx).await;
23062
23063 let language = Arc::new(
23064 Language::new(
23065 LanguageConfig::default(),
23066 Some(tree_sitter_html::LANGUAGE.into()),
23067 )
23068 .with_brackets_query(
23069 r#"
23070 ("<" @open "/>" @close)
23071 ("</" @open ">" @close)
23072 ("<" @open ">" @close)
23073 ("\"" @open "\"" @close)
23074 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
23075 "#,
23076 )
23077 .unwrap(),
23078 );
23079 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23080
23081 cx.set_state(indoc! {"
23082 <span>ˇ</span>
23083 "});
23084 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23085 cx.assert_editor_state(indoc! {"
23086 <span>
23087 ˇ
23088 </span>
23089 "});
23090
23091 cx.set_state(indoc! {"
23092 <span><span></span>ˇ</span>
23093 "});
23094 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23095 cx.assert_editor_state(indoc! {"
23096 <span><span></span>
23097 ˇ</span>
23098 "});
23099
23100 cx.set_state(indoc! {"
23101 <span>ˇ
23102 </span>
23103 "});
23104 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23105 cx.assert_editor_state(indoc! {"
23106 <span>
23107 ˇ
23108 </span>
23109 "});
23110}
23111
23112#[gpui::test(iterations = 10)]
23113async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
23114 init_test(cx, |_| {});
23115
23116 let fs = FakeFs::new(cx.executor());
23117 fs.insert_tree(
23118 path!("/dir"),
23119 json!({
23120 "a.ts": "a",
23121 }),
23122 )
23123 .await;
23124
23125 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
23126 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23127 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23128
23129 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23130 language_registry.add(Arc::new(Language::new(
23131 LanguageConfig {
23132 name: "TypeScript".into(),
23133 matcher: LanguageMatcher {
23134 path_suffixes: vec!["ts".to_string()],
23135 ..Default::default()
23136 },
23137 ..Default::default()
23138 },
23139 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
23140 )));
23141 let mut fake_language_servers = language_registry.register_fake_lsp(
23142 "TypeScript",
23143 FakeLspAdapter {
23144 capabilities: lsp::ServerCapabilities {
23145 code_lens_provider: Some(lsp::CodeLensOptions {
23146 resolve_provider: Some(true),
23147 }),
23148 execute_command_provider: Some(lsp::ExecuteCommandOptions {
23149 commands: vec!["_the/command".to_string()],
23150 ..lsp::ExecuteCommandOptions::default()
23151 }),
23152 ..lsp::ServerCapabilities::default()
23153 },
23154 ..FakeLspAdapter::default()
23155 },
23156 );
23157
23158 let editor = workspace
23159 .update(cx, |workspace, window, cx| {
23160 workspace.open_abs_path(
23161 PathBuf::from(path!("/dir/a.ts")),
23162 OpenOptions::default(),
23163 window,
23164 cx,
23165 )
23166 })
23167 .unwrap()
23168 .await
23169 .unwrap()
23170 .downcast::<Editor>()
23171 .unwrap();
23172 cx.executor().run_until_parked();
23173
23174 let fake_server = fake_language_servers.next().await.unwrap();
23175
23176 let buffer = editor.update(cx, |editor, cx| {
23177 editor
23178 .buffer()
23179 .read(cx)
23180 .as_singleton()
23181 .expect("have opened a single file by path")
23182 });
23183
23184 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
23185 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
23186 drop(buffer_snapshot);
23187 let actions = cx
23188 .update_window(*workspace, |_, window, cx| {
23189 project.code_actions(&buffer, anchor..anchor, window, cx)
23190 })
23191 .unwrap();
23192
23193 fake_server
23194 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23195 Ok(Some(vec![
23196 lsp::CodeLens {
23197 range: lsp::Range::default(),
23198 command: Some(lsp::Command {
23199 title: "Code lens command".to_owned(),
23200 command: "_the/command".to_owned(),
23201 arguments: None,
23202 }),
23203 data: None,
23204 },
23205 lsp::CodeLens {
23206 range: lsp::Range::default(),
23207 command: Some(lsp::Command {
23208 title: "Command not in capabilities".to_owned(),
23209 command: "not in capabilities".to_owned(),
23210 arguments: None,
23211 }),
23212 data: None,
23213 },
23214 lsp::CodeLens {
23215 range: lsp::Range {
23216 start: lsp::Position {
23217 line: 1,
23218 character: 1,
23219 },
23220 end: lsp::Position {
23221 line: 1,
23222 character: 1,
23223 },
23224 },
23225 command: Some(lsp::Command {
23226 title: "Command not in range".to_owned(),
23227 command: "_the/command".to_owned(),
23228 arguments: None,
23229 }),
23230 data: None,
23231 },
23232 ]))
23233 })
23234 .next()
23235 .await;
23236
23237 let actions = actions.await.unwrap();
23238 assert_eq!(
23239 actions.len(),
23240 1,
23241 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
23242 );
23243 let action = actions[0].clone();
23244 let apply = project.update(cx, |project, cx| {
23245 project.apply_code_action(buffer.clone(), action, true, cx)
23246 });
23247
23248 // Resolving the code action does not populate its edits. In absence of
23249 // edits, we must execute the given command.
23250 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
23251 |mut lens, _| async move {
23252 let lens_command = lens.command.as_mut().expect("should have a command");
23253 assert_eq!(lens_command.title, "Code lens command");
23254 lens_command.arguments = Some(vec![json!("the-argument")]);
23255 Ok(lens)
23256 },
23257 );
23258
23259 // While executing the command, the language server sends the editor
23260 // a `workspaceEdit` request.
23261 fake_server
23262 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
23263 let fake = fake_server.clone();
23264 move |params, _| {
23265 assert_eq!(params.command, "_the/command");
23266 let fake = fake.clone();
23267 async move {
23268 fake.server
23269 .request::<lsp::request::ApplyWorkspaceEdit>(
23270 lsp::ApplyWorkspaceEditParams {
23271 label: None,
23272 edit: lsp::WorkspaceEdit {
23273 changes: Some(
23274 [(
23275 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
23276 vec![lsp::TextEdit {
23277 range: lsp::Range::new(
23278 lsp::Position::new(0, 0),
23279 lsp::Position::new(0, 0),
23280 ),
23281 new_text: "X".into(),
23282 }],
23283 )]
23284 .into_iter()
23285 .collect(),
23286 ),
23287 ..lsp::WorkspaceEdit::default()
23288 },
23289 },
23290 )
23291 .await
23292 .into_response()
23293 .unwrap();
23294 Ok(Some(json!(null)))
23295 }
23296 }
23297 })
23298 .next()
23299 .await;
23300
23301 // Applying the code lens command returns a project transaction containing the edits
23302 // sent by the language server in its `workspaceEdit` request.
23303 let transaction = apply.await.unwrap();
23304 assert!(transaction.0.contains_key(&buffer));
23305 buffer.update(cx, |buffer, cx| {
23306 assert_eq!(buffer.text(), "Xa");
23307 buffer.undo(cx);
23308 assert_eq!(buffer.text(), "a");
23309 });
23310
23311 let actions_after_edits = cx
23312 .update_window(*workspace, |_, window, cx| {
23313 project.code_actions(&buffer, anchor..anchor, window, cx)
23314 })
23315 .unwrap()
23316 .await
23317 .unwrap();
23318 assert_eq!(
23319 actions, actions_after_edits,
23320 "For the same selection, same code lens actions should be returned"
23321 );
23322
23323 let _responses =
23324 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23325 panic!("No more code lens requests are expected");
23326 });
23327 editor.update_in(cx, |editor, window, cx| {
23328 editor.select_all(&SelectAll, window, cx);
23329 });
23330 cx.executor().run_until_parked();
23331 let new_actions = cx
23332 .update_window(*workspace, |_, window, cx| {
23333 project.code_actions(&buffer, anchor..anchor, window, cx)
23334 })
23335 .unwrap()
23336 .await
23337 .unwrap();
23338 assert_eq!(
23339 actions, new_actions,
23340 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
23341 );
23342}
23343
23344#[gpui::test]
23345async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
23346 init_test(cx, |_| {});
23347
23348 let fs = FakeFs::new(cx.executor());
23349 let main_text = r#"fn main() {
23350println!("1");
23351println!("2");
23352println!("3");
23353println!("4");
23354println!("5");
23355}"#;
23356 let lib_text = "mod foo {}";
23357 fs.insert_tree(
23358 path!("/a"),
23359 json!({
23360 "lib.rs": lib_text,
23361 "main.rs": main_text,
23362 }),
23363 )
23364 .await;
23365
23366 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23367 let (workspace, cx) =
23368 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23369 let worktree_id = workspace.update(cx, |workspace, cx| {
23370 workspace.project().update(cx, |project, cx| {
23371 project.worktrees(cx).next().unwrap().read(cx).id()
23372 })
23373 });
23374
23375 let expected_ranges = vec![
23376 Point::new(0, 0)..Point::new(0, 0),
23377 Point::new(1, 0)..Point::new(1, 1),
23378 Point::new(2, 0)..Point::new(2, 2),
23379 Point::new(3, 0)..Point::new(3, 3),
23380 ];
23381
23382 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23383 let editor_1 = workspace
23384 .update_in(cx, |workspace, window, cx| {
23385 workspace.open_path(
23386 (worktree_id, "main.rs"),
23387 Some(pane_1.downgrade()),
23388 true,
23389 window,
23390 cx,
23391 )
23392 })
23393 .unwrap()
23394 .await
23395 .downcast::<Editor>()
23396 .unwrap();
23397 pane_1.update(cx, |pane, cx| {
23398 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23399 open_editor.update(cx, |editor, cx| {
23400 assert_eq!(
23401 editor.display_text(cx),
23402 main_text,
23403 "Original main.rs text on initial open",
23404 );
23405 assert_eq!(
23406 editor
23407 .selections
23408 .all::<Point>(cx)
23409 .into_iter()
23410 .map(|s| s.range())
23411 .collect::<Vec<_>>(),
23412 vec![Point::zero()..Point::zero()],
23413 "Default selections on initial open",
23414 );
23415 })
23416 });
23417 editor_1.update_in(cx, |editor, window, cx| {
23418 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23419 s.select_ranges(expected_ranges.clone());
23420 });
23421 });
23422
23423 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
23424 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
23425 });
23426 let editor_2 = workspace
23427 .update_in(cx, |workspace, window, cx| {
23428 workspace.open_path(
23429 (worktree_id, "main.rs"),
23430 Some(pane_2.downgrade()),
23431 true,
23432 window,
23433 cx,
23434 )
23435 })
23436 .unwrap()
23437 .await
23438 .downcast::<Editor>()
23439 .unwrap();
23440 pane_2.update(cx, |pane, cx| {
23441 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23442 open_editor.update(cx, |editor, cx| {
23443 assert_eq!(
23444 editor.display_text(cx),
23445 main_text,
23446 "Original main.rs text on initial open in another panel",
23447 );
23448 assert_eq!(
23449 editor
23450 .selections
23451 .all::<Point>(cx)
23452 .into_iter()
23453 .map(|s| s.range())
23454 .collect::<Vec<_>>(),
23455 vec![Point::zero()..Point::zero()],
23456 "Default selections on initial open in another panel",
23457 );
23458 })
23459 });
23460
23461 editor_2.update_in(cx, |editor, window, cx| {
23462 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
23463 });
23464
23465 let _other_editor_1 = workspace
23466 .update_in(cx, |workspace, window, cx| {
23467 workspace.open_path(
23468 (worktree_id, "lib.rs"),
23469 Some(pane_1.downgrade()),
23470 true,
23471 window,
23472 cx,
23473 )
23474 })
23475 .unwrap()
23476 .await
23477 .downcast::<Editor>()
23478 .unwrap();
23479 pane_1
23480 .update_in(cx, |pane, window, cx| {
23481 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23482 })
23483 .await
23484 .unwrap();
23485 drop(editor_1);
23486 pane_1.update(cx, |pane, cx| {
23487 pane.active_item()
23488 .unwrap()
23489 .downcast::<Editor>()
23490 .unwrap()
23491 .update(cx, |editor, cx| {
23492 assert_eq!(
23493 editor.display_text(cx),
23494 lib_text,
23495 "Other file should be open and active",
23496 );
23497 });
23498 assert_eq!(pane.items().count(), 1, "No other editors should be open");
23499 });
23500
23501 let _other_editor_2 = workspace
23502 .update_in(cx, |workspace, window, cx| {
23503 workspace.open_path(
23504 (worktree_id, "lib.rs"),
23505 Some(pane_2.downgrade()),
23506 true,
23507 window,
23508 cx,
23509 )
23510 })
23511 .unwrap()
23512 .await
23513 .downcast::<Editor>()
23514 .unwrap();
23515 pane_2
23516 .update_in(cx, |pane, window, cx| {
23517 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23518 })
23519 .await
23520 .unwrap();
23521 drop(editor_2);
23522 pane_2.update(cx, |pane, cx| {
23523 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23524 open_editor.update(cx, |editor, cx| {
23525 assert_eq!(
23526 editor.display_text(cx),
23527 lib_text,
23528 "Other file should be open and active in another panel too",
23529 );
23530 });
23531 assert_eq!(
23532 pane.items().count(),
23533 1,
23534 "No other editors should be open in another pane",
23535 );
23536 });
23537
23538 let _editor_1_reopened = workspace
23539 .update_in(cx, |workspace, window, cx| {
23540 workspace.open_path(
23541 (worktree_id, "main.rs"),
23542 Some(pane_1.downgrade()),
23543 true,
23544 window,
23545 cx,
23546 )
23547 })
23548 .unwrap()
23549 .await
23550 .downcast::<Editor>()
23551 .unwrap();
23552 let _editor_2_reopened = workspace
23553 .update_in(cx, |workspace, window, cx| {
23554 workspace.open_path(
23555 (worktree_id, "main.rs"),
23556 Some(pane_2.downgrade()),
23557 true,
23558 window,
23559 cx,
23560 )
23561 })
23562 .unwrap()
23563 .await
23564 .downcast::<Editor>()
23565 .unwrap();
23566 pane_1.update(cx, |pane, cx| {
23567 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23568 open_editor.update(cx, |editor, cx| {
23569 assert_eq!(
23570 editor.display_text(cx),
23571 main_text,
23572 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
23573 );
23574 assert_eq!(
23575 editor
23576 .selections
23577 .all::<Point>(cx)
23578 .into_iter()
23579 .map(|s| s.range())
23580 .collect::<Vec<_>>(),
23581 expected_ranges,
23582 "Previous editor in the 1st panel had selections and should get them restored on reopen",
23583 );
23584 })
23585 });
23586 pane_2.update(cx, |pane, cx| {
23587 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23588 open_editor.update(cx, |editor, cx| {
23589 assert_eq!(
23590 editor.display_text(cx),
23591 r#"fn main() {
23592⋯rintln!("1");
23593⋯intln!("2");
23594⋯ntln!("3");
23595println!("4");
23596println!("5");
23597}"#,
23598 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
23599 );
23600 assert_eq!(
23601 editor
23602 .selections
23603 .all::<Point>(cx)
23604 .into_iter()
23605 .map(|s| s.range())
23606 .collect::<Vec<_>>(),
23607 vec![Point::zero()..Point::zero()],
23608 "Previous editor in the 2nd pane had no selections changed hence should restore none",
23609 );
23610 })
23611 });
23612}
23613
23614#[gpui::test]
23615async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
23616 init_test(cx, |_| {});
23617
23618 let fs = FakeFs::new(cx.executor());
23619 let main_text = r#"fn main() {
23620println!("1");
23621println!("2");
23622println!("3");
23623println!("4");
23624println!("5");
23625}"#;
23626 let lib_text = "mod foo {}";
23627 fs.insert_tree(
23628 path!("/a"),
23629 json!({
23630 "lib.rs": lib_text,
23631 "main.rs": main_text,
23632 }),
23633 )
23634 .await;
23635
23636 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23637 let (workspace, cx) =
23638 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23639 let worktree_id = workspace.update(cx, |workspace, cx| {
23640 workspace.project().update(cx, |project, cx| {
23641 project.worktrees(cx).next().unwrap().read(cx).id()
23642 })
23643 });
23644
23645 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23646 let editor = workspace
23647 .update_in(cx, |workspace, window, cx| {
23648 workspace.open_path(
23649 (worktree_id, "main.rs"),
23650 Some(pane.downgrade()),
23651 true,
23652 window,
23653 cx,
23654 )
23655 })
23656 .unwrap()
23657 .await
23658 .downcast::<Editor>()
23659 .unwrap();
23660 pane.update(cx, |pane, cx| {
23661 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23662 open_editor.update(cx, |editor, cx| {
23663 assert_eq!(
23664 editor.display_text(cx),
23665 main_text,
23666 "Original main.rs text on initial open",
23667 );
23668 })
23669 });
23670 editor.update_in(cx, |editor, window, cx| {
23671 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
23672 });
23673
23674 cx.update_global(|store: &mut SettingsStore, cx| {
23675 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
23676 s.restore_on_file_reopen = Some(false);
23677 });
23678 });
23679 editor.update_in(cx, |editor, window, cx| {
23680 editor.fold_ranges(
23681 vec![
23682 Point::new(1, 0)..Point::new(1, 1),
23683 Point::new(2, 0)..Point::new(2, 2),
23684 Point::new(3, 0)..Point::new(3, 3),
23685 ],
23686 false,
23687 window,
23688 cx,
23689 );
23690 });
23691 pane.update_in(cx, |pane, window, cx| {
23692 pane.close_all_items(&CloseAllItems::default(), window, cx)
23693 })
23694 .await
23695 .unwrap();
23696 pane.update(cx, |pane, _| {
23697 assert!(pane.active_item().is_none());
23698 });
23699 cx.update_global(|store: &mut SettingsStore, cx| {
23700 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
23701 s.restore_on_file_reopen = Some(true);
23702 });
23703 });
23704
23705 let _editor_reopened = workspace
23706 .update_in(cx, |workspace, window, cx| {
23707 workspace.open_path(
23708 (worktree_id, "main.rs"),
23709 Some(pane.downgrade()),
23710 true,
23711 window,
23712 cx,
23713 )
23714 })
23715 .unwrap()
23716 .await
23717 .downcast::<Editor>()
23718 .unwrap();
23719 pane.update(cx, |pane, cx| {
23720 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23721 open_editor.update(cx, |editor, cx| {
23722 assert_eq!(
23723 editor.display_text(cx),
23724 main_text,
23725 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
23726 );
23727 })
23728 });
23729}
23730
23731#[gpui::test]
23732async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
23733 struct EmptyModalView {
23734 focus_handle: gpui::FocusHandle,
23735 }
23736 impl EventEmitter<DismissEvent> for EmptyModalView {}
23737 impl Render for EmptyModalView {
23738 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
23739 div()
23740 }
23741 }
23742 impl Focusable for EmptyModalView {
23743 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
23744 self.focus_handle.clone()
23745 }
23746 }
23747 impl workspace::ModalView for EmptyModalView {}
23748 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
23749 EmptyModalView {
23750 focus_handle: cx.focus_handle(),
23751 }
23752 }
23753
23754 init_test(cx, |_| {});
23755
23756 let fs = FakeFs::new(cx.executor());
23757 let project = Project::test(fs, [], cx).await;
23758 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23759 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
23760 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23761 let editor = cx.new_window_entity(|window, cx| {
23762 Editor::new(
23763 EditorMode::full(),
23764 buffer,
23765 Some(project.clone()),
23766 window,
23767 cx,
23768 )
23769 });
23770 workspace
23771 .update(cx, |workspace, window, cx| {
23772 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
23773 })
23774 .unwrap();
23775 editor.update_in(cx, |editor, window, cx| {
23776 editor.open_context_menu(&OpenContextMenu, window, cx);
23777 assert!(editor.mouse_context_menu.is_some());
23778 });
23779 workspace
23780 .update(cx, |workspace, window, cx| {
23781 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
23782 })
23783 .unwrap();
23784 cx.read(|cx| {
23785 assert!(editor.read(cx).mouse_context_menu.is_none());
23786 });
23787}
23788
23789#[gpui::test]
23790async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
23791 init_test(cx, |_| {});
23792
23793 let fs = FakeFs::new(cx.executor());
23794 fs.insert_file(path!("/file.html"), Default::default())
23795 .await;
23796
23797 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
23798
23799 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23800 let html_language = Arc::new(Language::new(
23801 LanguageConfig {
23802 name: "HTML".into(),
23803 matcher: LanguageMatcher {
23804 path_suffixes: vec!["html".to_string()],
23805 ..LanguageMatcher::default()
23806 },
23807 brackets: BracketPairConfig {
23808 pairs: vec![BracketPair {
23809 start: "<".into(),
23810 end: ">".into(),
23811 close: true,
23812 ..Default::default()
23813 }],
23814 ..Default::default()
23815 },
23816 ..Default::default()
23817 },
23818 Some(tree_sitter_html::LANGUAGE.into()),
23819 ));
23820 language_registry.add(html_language);
23821 let mut fake_servers = language_registry.register_fake_lsp(
23822 "HTML",
23823 FakeLspAdapter {
23824 capabilities: lsp::ServerCapabilities {
23825 completion_provider: Some(lsp::CompletionOptions {
23826 resolve_provider: Some(true),
23827 ..Default::default()
23828 }),
23829 ..Default::default()
23830 },
23831 ..Default::default()
23832 },
23833 );
23834
23835 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23836 let cx = &mut VisualTestContext::from_window(*workspace, cx);
23837
23838 let worktree_id = workspace
23839 .update(cx, |workspace, _window, cx| {
23840 workspace.project().update(cx, |project, cx| {
23841 project.worktrees(cx).next().unwrap().read(cx).id()
23842 })
23843 })
23844 .unwrap();
23845 project
23846 .update(cx, |project, cx| {
23847 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
23848 })
23849 .await
23850 .unwrap();
23851 let editor = workspace
23852 .update(cx, |workspace, window, cx| {
23853 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
23854 })
23855 .unwrap()
23856 .await
23857 .unwrap()
23858 .downcast::<Editor>()
23859 .unwrap();
23860
23861 let fake_server = fake_servers.next().await.unwrap();
23862 editor.update_in(cx, |editor, window, cx| {
23863 editor.set_text("<ad></ad>", window, cx);
23864 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
23865 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
23866 });
23867 let Some((buffer, _)) = editor
23868 .buffer
23869 .read(cx)
23870 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
23871 else {
23872 panic!("Failed to get buffer for selection position");
23873 };
23874 let buffer = buffer.read(cx);
23875 let buffer_id = buffer.remote_id();
23876 let opening_range =
23877 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
23878 let closing_range =
23879 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
23880 let mut linked_ranges = HashMap::default();
23881 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
23882 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
23883 });
23884 let mut completion_handle =
23885 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
23886 Ok(Some(lsp::CompletionResponse::Array(vec![
23887 lsp::CompletionItem {
23888 label: "head".to_string(),
23889 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
23890 lsp::InsertReplaceEdit {
23891 new_text: "head".to_string(),
23892 insert: lsp::Range::new(
23893 lsp::Position::new(0, 1),
23894 lsp::Position::new(0, 3),
23895 ),
23896 replace: lsp::Range::new(
23897 lsp::Position::new(0, 1),
23898 lsp::Position::new(0, 3),
23899 ),
23900 },
23901 )),
23902 ..Default::default()
23903 },
23904 ])))
23905 });
23906 editor.update_in(cx, |editor, window, cx| {
23907 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
23908 });
23909 cx.run_until_parked();
23910 completion_handle.next().await.unwrap();
23911 editor.update(cx, |editor, _| {
23912 assert!(
23913 editor.context_menu_visible(),
23914 "Completion menu should be visible"
23915 );
23916 });
23917 editor.update_in(cx, |editor, window, cx| {
23918 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
23919 });
23920 cx.executor().run_until_parked();
23921 editor.update(cx, |editor, cx| {
23922 assert_eq!(editor.text(cx), "<head></head>");
23923 });
23924}
23925
23926#[gpui::test]
23927async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
23928 init_test(cx, |_| {});
23929
23930 let fs = FakeFs::new(cx.executor());
23931 fs.insert_tree(
23932 path!("/root"),
23933 json!({
23934 "a": {
23935 "main.rs": "fn main() {}",
23936 },
23937 "foo": {
23938 "bar": {
23939 "external_file.rs": "pub mod external {}",
23940 }
23941 }
23942 }),
23943 )
23944 .await;
23945
23946 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
23947 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23948 language_registry.add(rust_lang());
23949 let _fake_servers = language_registry.register_fake_lsp(
23950 "Rust",
23951 FakeLspAdapter {
23952 ..FakeLspAdapter::default()
23953 },
23954 );
23955 let (workspace, cx) =
23956 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23957 let worktree_id = workspace.update(cx, |workspace, cx| {
23958 workspace.project().update(cx, |project, cx| {
23959 project.worktrees(cx).next().unwrap().read(cx).id()
23960 })
23961 });
23962
23963 let assert_language_servers_count =
23964 |expected: usize, context: &str, cx: &mut VisualTestContext| {
23965 project.update(cx, |project, cx| {
23966 let current = project
23967 .lsp_store()
23968 .read(cx)
23969 .as_local()
23970 .unwrap()
23971 .language_servers
23972 .len();
23973 assert_eq!(expected, current, "{context}");
23974 });
23975 };
23976
23977 assert_language_servers_count(
23978 0,
23979 "No servers should be running before any file is open",
23980 cx,
23981 );
23982 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23983 let main_editor = workspace
23984 .update_in(cx, |workspace, window, cx| {
23985 workspace.open_path(
23986 (worktree_id, "main.rs"),
23987 Some(pane.downgrade()),
23988 true,
23989 window,
23990 cx,
23991 )
23992 })
23993 .unwrap()
23994 .await
23995 .downcast::<Editor>()
23996 .unwrap();
23997 pane.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 "fn main() {}",
24003 "Original main.rs text on initial open",
24004 );
24005 });
24006 assert_eq!(open_editor, main_editor);
24007 });
24008 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
24009
24010 let external_editor = workspace
24011 .update_in(cx, |workspace, window, cx| {
24012 workspace.open_abs_path(
24013 PathBuf::from("/root/foo/bar/external_file.rs"),
24014 OpenOptions::default(),
24015 window,
24016 cx,
24017 )
24018 })
24019 .await
24020 .expect("opening external file")
24021 .downcast::<Editor>()
24022 .expect("downcasted external file's open element to editor");
24023 pane.update(cx, |pane, cx| {
24024 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24025 open_editor.update(cx, |editor, cx| {
24026 assert_eq!(
24027 editor.display_text(cx),
24028 "pub mod external {}",
24029 "External file is open now",
24030 );
24031 });
24032 assert_eq!(open_editor, external_editor);
24033 });
24034 assert_language_servers_count(
24035 1,
24036 "Second, external, *.rs file should join the existing server",
24037 cx,
24038 );
24039
24040 pane.update_in(cx, |pane, window, cx| {
24041 pane.close_active_item(&CloseActiveItem::default(), window, cx)
24042 })
24043 .await
24044 .unwrap();
24045 pane.update_in(cx, |pane, window, cx| {
24046 pane.navigate_backward(&Default::default(), window, cx);
24047 });
24048 cx.run_until_parked();
24049 pane.update(cx, |pane, cx| {
24050 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24051 open_editor.update(cx, |editor, cx| {
24052 assert_eq!(
24053 editor.display_text(cx),
24054 "pub mod external {}",
24055 "External file is open now",
24056 );
24057 });
24058 });
24059 assert_language_servers_count(
24060 1,
24061 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
24062 cx,
24063 );
24064
24065 cx.update(|_, cx| {
24066 workspace::reload(cx);
24067 });
24068 assert_language_servers_count(
24069 1,
24070 "After reloading the worktree with local and external files opened, only one project should be started",
24071 cx,
24072 );
24073}
24074
24075#[gpui::test]
24076async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
24077 init_test(cx, |_| {});
24078
24079 let mut cx = EditorTestContext::new(cx).await;
24080 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24081 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24082
24083 // test cursor move to start of each line on tab
24084 // for `if`, `elif`, `else`, `while`, `with` and `for`
24085 cx.set_state(indoc! {"
24086 def main():
24087 ˇ for item in items:
24088 ˇ while item.active:
24089 ˇ if item.value > 10:
24090 ˇ continue
24091 ˇ elif item.value < 0:
24092 ˇ break
24093 ˇ else:
24094 ˇ with item.context() as ctx:
24095 ˇ yield count
24096 ˇ else:
24097 ˇ log('while else')
24098 ˇ else:
24099 ˇ log('for else')
24100 "});
24101 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24102 cx.assert_editor_state(indoc! {"
24103 def main():
24104 ˇfor item in items:
24105 ˇwhile item.active:
24106 ˇif item.value > 10:
24107 ˇcontinue
24108 ˇelif item.value < 0:
24109 ˇbreak
24110 ˇelse:
24111 ˇwith item.context() as ctx:
24112 ˇyield count
24113 ˇelse:
24114 ˇlog('while else')
24115 ˇelse:
24116 ˇlog('for else')
24117 "});
24118 // test relative indent is preserved when tab
24119 // for `if`, `elif`, `else`, `while`, `with` and `for`
24120 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24121 cx.assert_editor_state(indoc! {"
24122 def main():
24123 ˇfor item in items:
24124 ˇwhile item.active:
24125 ˇif item.value > 10:
24126 ˇcontinue
24127 ˇelif item.value < 0:
24128 ˇbreak
24129 ˇelse:
24130 ˇwith item.context() as ctx:
24131 ˇyield count
24132 ˇelse:
24133 ˇlog('while else')
24134 ˇelse:
24135 ˇlog('for else')
24136 "});
24137
24138 // test cursor move to start of each line on tab
24139 // for `try`, `except`, `else`, `finally`, `match` and `def`
24140 cx.set_state(indoc! {"
24141 def main():
24142 ˇ try:
24143 ˇ fetch()
24144 ˇ except ValueError:
24145 ˇ handle_error()
24146 ˇ else:
24147 ˇ match value:
24148 ˇ case _:
24149 ˇ finally:
24150 ˇ def status():
24151 ˇ return 0
24152 "});
24153 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24154 cx.assert_editor_state(indoc! {"
24155 def main():
24156 ˇtry:
24157 ˇfetch()
24158 ˇexcept ValueError:
24159 ˇhandle_error()
24160 ˇelse:
24161 ˇmatch value:
24162 ˇcase _:
24163 ˇfinally:
24164 ˇdef status():
24165 ˇreturn 0
24166 "});
24167 // test relative indent is preserved when tab
24168 // for `try`, `except`, `else`, `finally`, `match` and `def`
24169 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24170 cx.assert_editor_state(indoc! {"
24171 def main():
24172 ˇtry:
24173 ˇfetch()
24174 ˇexcept ValueError:
24175 ˇhandle_error()
24176 ˇelse:
24177 ˇmatch value:
24178 ˇcase _:
24179 ˇfinally:
24180 ˇdef status():
24181 ˇreturn 0
24182 "});
24183}
24184
24185#[gpui::test]
24186async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
24187 init_test(cx, |_| {});
24188
24189 let mut cx = EditorTestContext::new(cx).await;
24190 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24191 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24192
24193 // test `else` auto outdents when typed inside `if` block
24194 cx.set_state(indoc! {"
24195 def main():
24196 if i == 2:
24197 return
24198 ˇ
24199 "});
24200 cx.update_editor(|editor, window, cx| {
24201 editor.handle_input("else:", window, cx);
24202 });
24203 cx.assert_editor_state(indoc! {"
24204 def main():
24205 if i == 2:
24206 return
24207 else:ˇ
24208 "});
24209
24210 // test `except` auto outdents when typed inside `try` block
24211 cx.set_state(indoc! {"
24212 def main():
24213 try:
24214 i = 2
24215 ˇ
24216 "});
24217 cx.update_editor(|editor, window, cx| {
24218 editor.handle_input("except:", window, cx);
24219 });
24220 cx.assert_editor_state(indoc! {"
24221 def main():
24222 try:
24223 i = 2
24224 except:ˇ
24225 "});
24226
24227 // test `else` auto outdents when typed inside `except` block
24228 cx.set_state(indoc! {"
24229 def main():
24230 try:
24231 i = 2
24232 except:
24233 j = 2
24234 ˇ
24235 "});
24236 cx.update_editor(|editor, window, cx| {
24237 editor.handle_input("else:", window, cx);
24238 });
24239 cx.assert_editor_state(indoc! {"
24240 def main():
24241 try:
24242 i = 2
24243 except:
24244 j = 2
24245 else:ˇ
24246 "});
24247
24248 // test `finally` auto outdents when typed inside `else` block
24249 cx.set_state(indoc! {"
24250 def main():
24251 try:
24252 i = 2
24253 except:
24254 j = 2
24255 else:
24256 k = 2
24257 ˇ
24258 "});
24259 cx.update_editor(|editor, window, cx| {
24260 editor.handle_input("finally:", window, cx);
24261 });
24262 cx.assert_editor_state(indoc! {"
24263 def main():
24264 try:
24265 i = 2
24266 except:
24267 j = 2
24268 else:
24269 k = 2
24270 finally:ˇ
24271 "});
24272
24273 // test `else` does not outdents when typed inside `except` block right after for block
24274 cx.set_state(indoc! {"
24275 def main():
24276 try:
24277 i = 2
24278 except:
24279 for i in range(n):
24280 pass
24281 ˇ
24282 "});
24283 cx.update_editor(|editor, window, cx| {
24284 editor.handle_input("else:", window, cx);
24285 });
24286 cx.assert_editor_state(indoc! {"
24287 def main():
24288 try:
24289 i = 2
24290 except:
24291 for i in range(n):
24292 pass
24293 else:ˇ
24294 "});
24295
24296 // test `finally` auto outdents when typed inside `else` block right after for block
24297 cx.set_state(indoc! {"
24298 def main():
24299 try:
24300 i = 2
24301 except:
24302 j = 2
24303 else:
24304 for i in range(n):
24305 pass
24306 ˇ
24307 "});
24308 cx.update_editor(|editor, window, cx| {
24309 editor.handle_input("finally:", window, cx);
24310 });
24311 cx.assert_editor_state(indoc! {"
24312 def main():
24313 try:
24314 i = 2
24315 except:
24316 j = 2
24317 else:
24318 for i in range(n):
24319 pass
24320 finally:ˇ
24321 "});
24322
24323 // test `except` outdents to inner "try" block
24324 cx.set_state(indoc! {"
24325 def main():
24326 try:
24327 i = 2
24328 if i == 2:
24329 try:
24330 i = 3
24331 ˇ
24332 "});
24333 cx.update_editor(|editor, window, cx| {
24334 editor.handle_input("except:", window, cx);
24335 });
24336 cx.assert_editor_state(indoc! {"
24337 def main():
24338 try:
24339 i = 2
24340 if i == 2:
24341 try:
24342 i = 3
24343 except:ˇ
24344 "});
24345
24346 // test `except` outdents to outer "try" block
24347 cx.set_state(indoc! {"
24348 def main():
24349 try:
24350 i = 2
24351 if i == 2:
24352 try:
24353 i = 3
24354 ˇ
24355 "});
24356 cx.update_editor(|editor, window, cx| {
24357 editor.handle_input("except:", window, cx);
24358 });
24359 cx.assert_editor_state(indoc! {"
24360 def main():
24361 try:
24362 i = 2
24363 if i == 2:
24364 try:
24365 i = 3
24366 except:ˇ
24367 "});
24368
24369 // test `else` stays at correct indent when typed after `for` block
24370 cx.set_state(indoc! {"
24371 def main():
24372 for i in range(10):
24373 if i == 3:
24374 break
24375 ˇ
24376 "});
24377 cx.update_editor(|editor, window, cx| {
24378 editor.handle_input("else:", window, cx);
24379 });
24380 cx.assert_editor_state(indoc! {"
24381 def main():
24382 for i in range(10):
24383 if i == 3:
24384 break
24385 else:ˇ
24386 "});
24387
24388 // test does not outdent on typing after line with square brackets
24389 cx.set_state(indoc! {"
24390 def f() -> list[str]:
24391 ˇ
24392 "});
24393 cx.update_editor(|editor, window, cx| {
24394 editor.handle_input("a", window, cx);
24395 });
24396 cx.assert_editor_state(indoc! {"
24397 def f() -> list[str]:
24398 aˇ
24399 "});
24400
24401 // test does not outdent on typing : after case keyword
24402 cx.set_state(indoc! {"
24403 match 1:
24404 caseˇ
24405 "});
24406 cx.update_editor(|editor, window, cx| {
24407 editor.handle_input(":", window, cx);
24408 });
24409 cx.assert_editor_state(indoc! {"
24410 match 1:
24411 case:ˇ
24412 "});
24413}
24414
24415#[gpui::test]
24416async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
24417 init_test(cx, |_| {});
24418 update_test_language_settings(cx, |settings| {
24419 settings.defaults.extend_comment_on_newline = Some(false);
24420 });
24421 let mut cx = EditorTestContext::new(cx).await;
24422 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24423 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24424
24425 // test correct indent after newline on comment
24426 cx.set_state(indoc! {"
24427 # COMMENT:ˇ
24428 "});
24429 cx.update_editor(|editor, window, cx| {
24430 editor.newline(&Newline, window, cx);
24431 });
24432 cx.assert_editor_state(indoc! {"
24433 # COMMENT:
24434 ˇ
24435 "});
24436
24437 // test correct indent after newline in brackets
24438 cx.set_state(indoc! {"
24439 {ˇ}
24440 "});
24441 cx.update_editor(|editor, window, cx| {
24442 editor.newline(&Newline, window, cx);
24443 });
24444 cx.run_until_parked();
24445 cx.assert_editor_state(indoc! {"
24446 {
24447 ˇ
24448 }
24449 "});
24450
24451 cx.set_state(indoc! {"
24452 (ˇ)
24453 "});
24454 cx.update_editor(|editor, window, cx| {
24455 editor.newline(&Newline, window, cx);
24456 });
24457 cx.run_until_parked();
24458 cx.assert_editor_state(indoc! {"
24459 (
24460 ˇ
24461 )
24462 "});
24463
24464 // do not indent after empty lists or dictionaries
24465 cx.set_state(indoc! {"
24466 a = []ˇ
24467 "});
24468 cx.update_editor(|editor, window, cx| {
24469 editor.newline(&Newline, window, cx);
24470 });
24471 cx.run_until_parked();
24472 cx.assert_editor_state(indoc! {"
24473 a = []
24474 ˇ
24475 "});
24476}
24477
24478#[gpui::test]
24479async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
24480 init_test(cx, |_| {});
24481
24482 let mut cx = EditorTestContext::new(cx).await;
24483 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24484 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24485
24486 // test cursor move to start of each line on tab
24487 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
24488 cx.set_state(indoc! {"
24489 function main() {
24490 ˇ for item in $items; do
24491 ˇ while [ -n \"$item\" ]; do
24492 ˇ if [ \"$value\" -gt 10 ]; then
24493 ˇ continue
24494 ˇ elif [ \"$value\" -lt 0 ]; then
24495 ˇ break
24496 ˇ else
24497 ˇ echo \"$item\"
24498 ˇ fi
24499 ˇ done
24500 ˇ done
24501 ˇ}
24502 "});
24503 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24504 cx.assert_editor_state(indoc! {"
24505 function main() {
24506 ˇfor item in $items; do
24507 ˇwhile [ -n \"$item\" ]; do
24508 ˇif [ \"$value\" -gt 10 ]; then
24509 ˇcontinue
24510 ˇelif [ \"$value\" -lt 0 ]; then
24511 ˇbreak
24512 ˇelse
24513 ˇecho \"$item\"
24514 ˇfi
24515 ˇdone
24516 ˇdone
24517 ˇ}
24518 "});
24519 // test relative indent is preserved when tab
24520 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24521 cx.assert_editor_state(indoc! {"
24522 function main() {
24523 ˇfor item in $items; do
24524 ˇwhile [ -n \"$item\" ]; do
24525 ˇif [ \"$value\" -gt 10 ]; then
24526 ˇcontinue
24527 ˇelif [ \"$value\" -lt 0 ]; then
24528 ˇbreak
24529 ˇelse
24530 ˇecho \"$item\"
24531 ˇfi
24532 ˇdone
24533 ˇdone
24534 ˇ}
24535 "});
24536
24537 // test cursor move to start of each line on tab
24538 // for `case` statement with patterns
24539 cx.set_state(indoc! {"
24540 function handle() {
24541 ˇ case \"$1\" in
24542 ˇ start)
24543 ˇ echo \"a\"
24544 ˇ ;;
24545 ˇ stop)
24546 ˇ echo \"b\"
24547 ˇ ;;
24548 ˇ *)
24549 ˇ echo \"c\"
24550 ˇ ;;
24551 ˇ esac
24552 ˇ}
24553 "});
24554 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24555 cx.assert_editor_state(indoc! {"
24556 function handle() {
24557 ˇcase \"$1\" in
24558 ˇstart)
24559 ˇecho \"a\"
24560 ˇ;;
24561 ˇstop)
24562 ˇecho \"b\"
24563 ˇ;;
24564 ˇ*)
24565 ˇecho \"c\"
24566 ˇ;;
24567 ˇesac
24568 ˇ}
24569 "});
24570}
24571
24572#[gpui::test]
24573async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
24574 init_test(cx, |_| {});
24575
24576 let mut cx = EditorTestContext::new(cx).await;
24577 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24578 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24579
24580 // test indents on comment insert
24581 cx.set_state(indoc! {"
24582 function main() {
24583 ˇ for item in $items; do
24584 ˇ while [ -n \"$item\" ]; do
24585 ˇ if [ \"$value\" -gt 10 ]; then
24586 ˇ continue
24587 ˇ elif [ \"$value\" -lt 0 ]; then
24588 ˇ break
24589 ˇ else
24590 ˇ echo \"$item\"
24591 ˇ fi
24592 ˇ done
24593 ˇ done
24594 ˇ}
24595 "});
24596 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
24597 cx.assert_editor_state(indoc! {"
24598 function main() {
24599 #ˇ for item in $items; do
24600 #ˇ while [ -n \"$item\" ]; do
24601 #ˇ if [ \"$value\" -gt 10 ]; then
24602 #ˇ continue
24603 #ˇ elif [ \"$value\" -lt 0 ]; then
24604 #ˇ break
24605 #ˇ else
24606 #ˇ echo \"$item\"
24607 #ˇ fi
24608 #ˇ done
24609 #ˇ done
24610 #ˇ}
24611 "});
24612}
24613
24614#[gpui::test]
24615async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
24616 init_test(cx, |_| {});
24617
24618 let mut cx = EditorTestContext::new(cx).await;
24619 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24620 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24621
24622 // test `else` auto outdents when typed inside `if` block
24623 cx.set_state(indoc! {"
24624 if [ \"$1\" = \"test\" ]; then
24625 echo \"foo bar\"
24626 ˇ
24627 "});
24628 cx.update_editor(|editor, window, cx| {
24629 editor.handle_input("else", window, cx);
24630 });
24631 cx.assert_editor_state(indoc! {"
24632 if [ \"$1\" = \"test\" ]; then
24633 echo \"foo bar\"
24634 elseˇ
24635 "});
24636
24637 // test `elif` auto outdents when typed inside `if` block
24638 cx.set_state(indoc! {"
24639 if [ \"$1\" = \"test\" ]; then
24640 echo \"foo bar\"
24641 ˇ
24642 "});
24643 cx.update_editor(|editor, window, cx| {
24644 editor.handle_input("elif", window, cx);
24645 });
24646 cx.assert_editor_state(indoc! {"
24647 if [ \"$1\" = \"test\" ]; then
24648 echo \"foo bar\"
24649 elifˇ
24650 "});
24651
24652 // test `fi` auto outdents when typed inside `else` block
24653 cx.set_state(indoc! {"
24654 if [ \"$1\" = \"test\" ]; then
24655 echo \"foo bar\"
24656 else
24657 echo \"bar baz\"
24658 ˇ
24659 "});
24660 cx.update_editor(|editor, window, cx| {
24661 editor.handle_input("fi", window, cx);
24662 });
24663 cx.assert_editor_state(indoc! {"
24664 if [ \"$1\" = \"test\" ]; then
24665 echo \"foo bar\"
24666 else
24667 echo \"bar baz\"
24668 fiˇ
24669 "});
24670
24671 // test `done` auto outdents when typed inside `while` block
24672 cx.set_state(indoc! {"
24673 while read line; do
24674 echo \"$line\"
24675 ˇ
24676 "});
24677 cx.update_editor(|editor, window, cx| {
24678 editor.handle_input("done", window, cx);
24679 });
24680 cx.assert_editor_state(indoc! {"
24681 while read line; do
24682 echo \"$line\"
24683 doneˇ
24684 "});
24685
24686 // test `done` auto outdents when typed inside `for` block
24687 cx.set_state(indoc! {"
24688 for file in *.txt; do
24689 cat \"$file\"
24690 ˇ
24691 "});
24692 cx.update_editor(|editor, window, cx| {
24693 editor.handle_input("done", window, cx);
24694 });
24695 cx.assert_editor_state(indoc! {"
24696 for file in *.txt; do
24697 cat \"$file\"
24698 doneˇ
24699 "});
24700
24701 // test `esac` auto outdents when typed inside `case` block
24702 cx.set_state(indoc! {"
24703 case \"$1\" in
24704 start)
24705 echo \"foo bar\"
24706 ;;
24707 stop)
24708 echo \"bar baz\"
24709 ;;
24710 ˇ
24711 "});
24712 cx.update_editor(|editor, window, cx| {
24713 editor.handle_input("esac", window, cx);
24714 });
24715 cx.assert_editor_state(indoc! {"
24716 case \"$1\" in
24717 start)
24718 echo \"foo bar\"
24719 ;;
24720 stop)
24721 echo \"bar baz\"
24722 ;;
24723 esacˇ
24724 "});
24725
24726 // test `*)` auto outdents when typed inside `case` block
24727 cx.set_state(indoc! {"
24728 case \"$1\" in
24729 start)
24730 echo \"foo bar\"
24731 ;;
24732 ˇ
24733 "});
24734 cx.update_editor(|editor, window, cx| {
24735 editor.handle_input("*)", window, cx);
24736 });
24737 cx.assert_editor_state(indoc! {"
24738 case \"$1\" in
24739 start)
24740 echo \"foo bar\"
24741 ;;
24742 *)ˇ
24743 "});
24744
24745 // test `fi` outdents to correct level with nested if blocks
24746 cx.set_state(indoc! {"
24747 if [ \"$1\" = \"test\" ]; then
24748 echo \"outer if\"
24749 if [ \"$2\" = \"debug\" ]; then
24750 echo \"inner if\"
24751 ˇ
24752 "});
24753 cx.update_editor(|editor, window, cx| {
24754 editor.handle_input("fi", window, cx);
24755 });
24756 cx.assert_editor_state(indoc! {"
24757 if [ \"$1\" = \"test\" ]; then
24758 echo \"outer if\"
24759 if [ \"$2\" = \"debug\" ]; then
24760 echo \"inner if\"
24761 fiˇ
24762 "});
24763}
24764
24765#[gpui::test]
24766async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
24767 init_test(cx, |_| {});
24768 update_test_language_settings(cx, |settings| {
24769 settings.defaults.extend_comment_on_newline = Some(false);
24770 });
24771 let mut cx = EditorTestContext::new(cx).await;
24772 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24773 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24774
24775 // test correct indent after newline on comment
24776 cx.set_state(indoc! {"
24777 # COMMENT:ˇ
24778 "});
24779 cx.update_editor(|editor, window, cx| {
24780 editor.newline(&Newline, window, cx);
24781 });
24782 cx.assert_editor_state(indoc! {"
24783 # COMMENT:
24784 ˇ
24785 "});
24786
24787 // test correct indent after newline after `then`
24788 cx.set_state(indoc! {"
24789
24790 if [ \"$1\" = \"test\" ]; thenˇ
24791 "});
24792 cx.update_editor(|editor, window, cx| {
24793 editor.newline(&Newline, window, cx);
24794 });
24795 cx.run_until_parked();
24796 cx.assert_editor_state(indoc! {"
24797
24798 if [ \"$1\" = \"test\" ]; then
24799 ˇ
24800 "});
24801
24802 // test correct indent after newline after `else`
24803 cx.set_state(indoc! {"
24804 if [ \"$1\" = \"test\" ]; then
24805 elseˇ
24806 "});
24807 cx.update_editor(|editor, window, cx| {
24808 editor.newline(&Newline, window, cx);
24809 });
24810 cx.run_until_parked();
24811 cx.assert_editor_state(indoc! {"
24812 if [ \"$1\" = \"test\" ]; then
24813 else
24814 ˇ
24815 "});
24816
24817 // test correct indent after newline after `elif`
24818 cx.set_state(indoc! {"
24819 if [ \"$1\" = \"test\" ]; then
24820 elifˇ
24821 "});
24822 cx.update_editor(|editor, window, cx| {
24823 editor.newline(&Newline, window, cx);
24824 });
24825 cx.run_until_parked();
24826 cx.assert_editor_state(indoc! {"
24827 if [ \"$1\" = \"test\" ]; then
24828 elif
24829 ˇ
24830 "});
24831
24832 // test correct indent after newline after `do`
24833 cx.set_state(indoc! {"
24834 for file in *.txt; doˇ
24835 "});
24836 cx.update_editor(|editor, window, cx| {
24837 editor.newline(&Newline, window, cx);
24838 });
24839 cx.run_until_parked();
24840 cx.assert_editor_state(indoc! {"
24841 for file in *.txt; do
24842 ˇ
24843 "});
24844
24845 // test correct indent after newline after case pattern
24846 cx.set_state(indoc! {"
24847 case \"$1\" in
24848 start)ˇ
24849 "});
24850 cx.update_editor(|editor, window, cx| {
24851 editor.newline(&Newline, window, cx);
24852 });
24853 cx.run_until_parked();
24854 cx.assert_editor_state(indoc! {"
24855 case \"$1\" in
24856 start)
24857 ˇ
24858 "});
24859
24860 // test correct indent after newline after case pattern
24861 cx.set_state(indoc! {"
24862 case \"$1\" in
24863 start)
24864 ;;
24865 *)ˇ
24866 "});
24867 cx.update_editor(|editor, window, cx| {
24868 editor.newline(&Newline, window, cx);
24869 });
24870 cx.run_until_parked();
24871 cx.assert_editor_state(indoc! {"
24872 case \"$1\" in
24873 start)
24874 ;;
24875 *)
24876 ˇ
24877 "});
24878
24879 // test correct indent after newline after function opening brace
24880 cx.set_state(indoc! {"
24881 function test() {ˇ}
24882 "});
24883 cx.update_editor(|editor, window, cx| {
24884 editor.newline(&Newline, window, cx);
24885 });
24886 cx.run_until_parked();
24887 cx.assert_editor_state(indoc! {"
24888 function test() {
24889 ˇ
24890 }
24891 "});
24892
24893 // test no extra indent after semicolon on same line
24894 cx.set_state(indoc! {"
24895 echo \"test\";ˇ
24896 "});
24897 cx.update_editor(|editor, window, cx| {
24898 editor.newline(&Newline, window, cx);
24899 });
24900 cx.run_until_parked();
24901 cx.assert_editor_state(indoc! {"
24902 echo \"test\";
24903 ˇ
24904 "});
24905}
24906
24907fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
24908 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
24909 point..point
24910}
24911
24912#[track_caller]
24913fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
24914 let (text, ranges) = marked_text_ranges(marked_text, true);
24915 assert_eq!(editor.text(cx), text);
24916 assert_eq!(
24917 editor.selections.ranges(cx),
24918 ranges,
24919 "Assert selections are {}",
24920 marked_text
24921 );
24922}
24923
24924pub fn handle_signature_help_request(
24925 cx: &mut EditorLspTestContext,
24926 mocked_response: lsp::SignatureHelp,
24927) -> impl Future<Output = ()> + use<> {
24928 let mut request =
24929 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
24930 let mocked_response = mocked_response.clone();
24931 async move { Ok(Some(mocked_response)) }
24932 });
24933
24934 async move {
24935 request.next().await;
24936 }
24937}
24938
24939#[track_caller]
24940pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
24941 cx.update_editor(|editor, _, _| {
24942 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
24943 let entries = menu.entries.borrow();
24944 let entries = entries
24945 .iter()
24946 .map(|entry| entry.string.as_str())
24947 .collect::<Vec<_>>();
24948 assert_eq!(entries, expected);
24949 } else {
24950 panic!("Expected completions menu");
24951 }
24952 });
24953}
24954
24955/// Handle completion request passing a marked string specifying where the completion
24956/// should be triggered from using '|' character, what range should be replaced, and what completions
24957/// should be returned using '<' and '>' to delimit the range.
24958///
24959/// Also see `handle_completion_request_with_insert_and_replace`.
24960#[track_caller]
24961pub fn handle_completion_request(
24962 marked_string: &str,
24963 completions: Vec<&'static str>,
24964 is_incomplete: bool,
24965 counter: Arc<AtomicUsize>,
24966 cx: &mut EditorLspTestContext,
24967) -> impl Future<Output = ()> {
24968 let complete_from_marker: TextRangeMarker = '|'.into();
24969 let replace_range_marker: TextRangeMarker = ('<', '>').into();
24970 let (_, mut marked_ranges) = marked_text_ranges_by(
24971 marked_string,
24972 vec![complete_from_marker.clone(), replace_range_marker.clone()],
24973 );
24974
24975 let complete_from_position =
24976 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
24977 let replace_range =
24978 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
24979
24980 let mut request =
24981 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
24982 let completions = completions.clone();
24983 counter.fetch_add(1, atomic::Ordering::Release);
24984 async move {
24985 assert_eq!(params.text_document_position.text_document.uri, url.clone());
24986 assert_eq!(
24987 params.text_document_position.position,
24988 complete_from_position
24989 );
24990 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
24991 is_incomplete,
24992 item_defaults: None,
24993 items: completions
24994 .iter()
24995 .map(|completion_text| lsp::CompletionItem {
24996 label: completion_text.to_string(),
24997 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
24998 range: replace_range,
24999 new_text: completion_text.to_string(),
25000 })),
25001 ..Default::default()
25002 })
25003 .collect(),
25004 })))
25005 }
25006 });
25007
25008 async move {
25009 request.next().await;
25010 }
25011}
25012
25013/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
25014/// given instead, which also contains an `insert` range.
25015///
25016/// This function uses markers to define ranges:
25017/// - `|` marks the cursor position
25018/// - `<>` marks the replace range
25019/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
25020pub fn handle_completion_request_with_insert_and_replace(
25021 cx: &mut EditorLspTestContext,
25022 marked_string: &str,
25023 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
25024 counter: Arc<AtomicUsize>,
25025) -> impl Future<Output = ()> {
25026 let complete_from_marker: TextRangeMarker = '|'.into();
25027 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25028 let insert_range_marker: TextRangeMarker = ('{', '}').into();
25029
25030 let (_, mut marked_ranges) = marked_text_ranges_by(
25031 marked_string,
25032 vec![
25033 complete_from_marker.clone(),
25034 replace_range_marker.clone(),
25035 insert_range_marker.clone(),
25036 ],
25037 );
25038
25039 let complete_from_position =
25040 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25041 let replace_range =
25042 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25043
25044 let insert_range = match marked_ranges.remove(&insert_range_marker) {
25045 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
25046 _ => lsp::Range {
25047 start: replace_range.start,
25048 end: complete_from_position,
25049 },
25050 };
25051
25052 let mut request =
25053 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25054 let completions = completions.clone();
25055 counter.fetch_add(1, atomic::Ordering::Release);
25056 async move {
25057 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25058 assert_eq!(
25059 params.text_document_position.position, complete_from_position,
25060 "marker `|` position doesn't match",
25061 );
25062 Ok(Some(lsp::CompletionResponse::Array(
25063 completions
25064 .iter()
25065 .map(|(label, new_text)| lsp::CompletionItem {
25066 label: label.to_string(),
25067 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
25068 lsp::InsertReplaceEdit {
25069 insert: insert_range,
25070 replace: replace_range,
25071 new_text: new_text.to_string(),
25072 },
25073 )),
25074 ..Default::default()
25075 })
25076 .collect(),
25077 )))
25078 }
25079 });
25080
25081 async move {
25082 request.next().await;
25083 }
25084}
25085
25086fn handle_resolve_completion_request(
25087 cx: &mut EditorLspTestContext,
25088 edits: Option<Vec<(&'static str, &'static str)>>,
25089) -> impl Future<Output = ()> {
25090 let edits = edits.map(|edits| {
25091 edits
25092 .iter()
25093 .map(|(marked_string, new_text)| {
25094 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
25095 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
25096 lsp::TextEdit::new(replace_range, new_text.to_string())
25097 })
25098 .collect::<Vec<_>>()
25099 });
25100
25101 let mut request =
25102 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
25103 let edits = edits.clone();
25104 async move {
25105 Ok(lsp::CompletionItem {
25106 additional_text_edits: edits,
25107 ..Default::default()
25108 })
25109 }
25110 });
25111
25112 async move {
25113 request.next().await;
25114 }
25115}
25116
25117pub(crate) fn update_test_language_settings(
25118 cx: &mut TestAppContext,
25119 f: impl Fn(&mut AllLanguageSettingsContent),
25120) {
25121 cx.update(|cx| {
25122 SettingsStore::update_global(cx, |store, cx| {
25123 store.update_user_settings::<AllLanguageSettings>(cx, f);
25124 });
25125 });
25126}
25127
25128pub(crate) fn update_test_project_settings(
25129 cx: &mut TestAppContext,
25130 f: impl Fn(&mut ProjectSettings),
25131) {
25132 cx.update(|cx| {
25133 SettingsStore::update_global(cx, |store, cx| {
25134 store.update_user_settings::<ProjectSettings>(cx, f);
25135 });
25136 });
25137}
25138
25139pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
25140 cx.update(|cx| {
25141 assets::Assets.load_test_fonts(cx);
25142 let store = SettingsStore::test(cx);
25143 cx.set_global(store);
25144 theme::init(theme::LoadThemes::JustBase, cx);
25145 release_channel::init(SemanticVersion::default(), cx);
25146 client::init_settings(cx);
25147 language::init(cx);
25148 Project::init_settings(cx);
25149 workspace::init_settings(cx);
25150 crate::init(cx);
25151 });
25152 zlog::init_test();
25153 update_test_language_settings(cx, f);
25154}
25155
25156#[track_caller]
25157fn assert_hunk_revert(
25158 not_reverted_text_with_selections: &str,
25159 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
25160 expected_reverted_text_with_selections: &str,
25161 base_text: &str,
25162 cx: &mut EditorLspTestContext,
25163) {
25164 cx.set_state(not_reverted_text_with_selections);
25165 cx.set_head_text(base_text);
25166 cx.executor().run_until_parked();
25167
25168 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
25169 let snapshot = editor.snapshot(window, cx);
25170 let reverted_hunk_statuses = snapshot
25171 .buffer_snapshot
25172 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
25173 .map(|hunk| hunk.status().kind)
25174 .collect::<Vec<_>>();
25175
25176 editor.git_restore(&Default::default(), window, cx);
25177 reverted_hunk_statuses
25178 });
25179 cx.executor().run_until_parked();
25180 cx.assert_editor_state(expected_reverted_text_with_selections);
25181 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
25182}
25183
25184#[gpui::test(iterations = 10)]
25185async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
25186 init_test(cx, |_| {});
25187
25188 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
25189 let counter = diagnostic_requests.clone();
25190
25191 let fs = FakeFs::new(cx.executor());
25192 fs.insert_tree(
25193 path!("/a"),
25194 json!({
25195 "first.rs": "fn main() { let a = 5; }",
25196 "second.rs": "// Test file",
25197 }),
25198 )
25199 .await;
25200
25201 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25202 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25203 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25204
25205 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25206 language_registry.add(rust_lang());
25207 let mut fake_servers = language_registry.register_fake_lsp(
25208 "Rust",
25209 FakeLspAdapter {
25210 capabilities: lsp::ServerCapabilities {
25211 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
25212 lsp::DiagnosticOptions {
25213 identifier: None,
25214 inter_file_dependencies: true,
25215 workspace_diagnostics: true,
25216 work_done_progress_options: Default::default(),
25217 },
25218 )),
25219 ..Default::default()
25220 },
25221 ..Default::default()
25222 },
25223 );
25224
25225 let editor = workspace
25226 .update(cx, |workspace, window, cx| {
25227 workspace.open_abs_path(
25228 PathBuf::from(path!("/a/first.rs")),
25229 OpenOptions::default(),
25230 window,
25231 cx,
25232 )
25233 })
25234 .unwrap()
25235 .await
25236 .unwrap()
25237 .downcast::<Editor>()
25238 .unwrap();
25239 let fake_server = fake_servers.next().await.unwrap();
25240 let server_id = fake_server.server.server_id();
25241 let mut first_request = fake_server
25242 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
25243 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
25244 let result_id = Some(new_result_id.to_string());
25245 assert_eq!(
25246 params.text_document.uri,
25247 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25248 );
25249 async move {
25250 Ok(lsp::DocumentDiagnosticReportResult::Report(
25251 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
25252 related_documents: None,
25253 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
25254 items: Vec::new(),
25255 result_id,
25256 },
25257 }),
25258 ))
25259 }
25260 });
25261
25262 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
25263 project.update(cx, |project, cx| {
25264 let buffer_id = editor
25265 .read(cx)
25266 .buffer()
25267 .read(cx)
25268 .as_singleton()
25269 .expect("created a singleton buffer")
25270 .read(cx)
25271 .remote_id();
25272 let buffer_result_id = project
25273 .lsp_store()
25274 .read(cx)
25275 .result_id(server_id, buffer_id, cx);
25276 assert_eq!(expected, buffer_result_id);
25277 });
25278 };
25279
25280 ensure_result_id(None, cx);
25281 cx.executor().advance_clock(Duration::from_millis(60));
25282 cx.executor().run_until_parked();
25283 assert_eq!(
25284 diagnostic_requests.load(atomic::Ordering::Acquire),
25285 1,
25286 "Opening file should trigger diagnostic request"
25287 );
25288 first_request
25289 .next()
25290 .await
25291 .expect("should have sent the first diagnostics pull request");
25292 ensure_result_id(Some("1".to_string()), cx);
25293
25294 // Editing should trigger diagnostics
25295 editor.update_in(cx, |editor, window, cx| {
25296 editor.handle_input("2", window, cx)
25297 });
25298 cx.executor().advance_clock(Duration::from_millis(60));
25299 cx.executor().run_until_parked();
25300 assert_eq!(
25301 diagnostic_requests.load(atomic::Ordering::Acquire),
25302 2,
25303 "Editing should trigger diagnostic request"
25304 );
25305 ensure_result_id(Some("2".to_string()), cx);
25306
25307 // Moving cursor should not trigger diagnostic request
25308 editor.update_in(cx, |editor, window, cx| {
25309 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25310 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
25311 });
25312 });
25313 cx.executor().advance_clock(Duration::from_millis(60));
25314 cx.executor().run_until_parked();
25315 assert_eq!(
25316 diagnostic_requests.load(atomic::Ordering::Acquire),
25317 2,
25318 "Cursor movement should not trigger diagnostic request"
25319 );
25320 ensure_result_id(Some("2".to_string()), cx);
25321 // Multiple rapid edits should be debounced
25322 for _ in 0..5 {
25323 editor.update_in(cx, |editor, window, cx| {
25324 editor.handle_input("x", window, cx)
25325 });
25326 }
25327 cx.executor().advance_clock(Duration::from_millis(60));
25328 cx.executor().run_until_parked();
25329
25330 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
25331 assert!(
25332 final_requests <= 4,
25333 "Multiple rapid edits should be debounced (got {final_requests} requests)",
25334 );
25335 ensure_result_id(Some(final_requests.to_string()), cx);
25336}
25337
25338#[gpui::test]
25339async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
25340 // Regression test for issue #11671
25341 // Previously, adding a cursor after moving multiple cursors would reset
25342 // the cursor count instead of adding to the existing cursors.
25343 init_test(cx, |_| {});
25344 let mut cx = EditorTestContext::new(cx).await;
25345
25346 // Create a simple buffer with cursor at start
25347 cx.set_state(indoc! {"
25348 ˇaaaa
25349 bbbb
25350 cccc
25351 dddd
25352 eeee
25353 ffff
25354 gggg
25355 hhhh"});
25356
25357 // Add 2 cursors below (so we have 3 total)
25358 cx.update_editor(|editor, window, cx| {
25359 editor.add_selection_below(&Default::default(), window, cx);
25360 editor.add_selection_below(&Default::default(), window, cx);
25361 });
25362
25363 // Verify we have 3 cursors
25364 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
25365 assert_eq!(
25366 initial_count, 3,
25367 "Should have 3 cursors after adding 2 below"
25368 );
25369
25370 // Move down one line
25371 cx.update_editor(|editor, window, cx| {
25372 editor.move_down(&MoveDown, window, cx);
25373 });
25374
25375 // Add another cursor below
25376 cx.update_editor(|editor, window, cx| {
25377 editor.add_selection_below(&Default::default(), window, cx);
25378 });
25379
25380 // Should now have 4 cursors (3 original + 1 new)
25381 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
25382 assert_eq!(
25383 final_count, 4,
25384 "Should have 4 cursors after moving and adding another"
25385 );
25386}
25387
25388#[gpui::test(iterations = 10)]
25389async fn test_document_colors(cx: &mut TestAppContext) {
25390 let expected_color = Rgba {
25391 r: 0.33,
25392 g: 0.33,
25393 b: 0.33,
25394 a: 0.33,
25395 };
25396
25397 init_test(cx, |_| {});
25398
25399 let fs = FakeFs::new(cx.executor());
25400 fs.insert_tree(
25401 path!("/a"),
25402 json!({
25403 "first.rs": "fn main() { let a = 5; }",
25404 }),
25405 )
25406 .await;
25407
25408 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25409 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25410 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25411
25412 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25413 language_registry.add(rust_lang());
25414 let mut fake_servers = language_registry.register_fake_lsp(
25415 "Rust",
25416 FakeLspAdapter {
25417 capabilities: lsp::ServerCapabilities {
25418 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
25419 ..lsp::ServerCapabilities::default()
25420 },
25421 name: "rust-analyzer",
25422 ..FakeLspAdapter::default()
25423 },
25424 );
25425 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
25426 "Rust",
25427 FakeLspAdapter {
25428 capabilities: lsp::ServerCapabilities {
25429 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
25430 ..lsp::ServerCapabilities::default()
25431 },
25432 name: "not-rust-analyzer",
25433 ..FakeLspAdapter::default()
25434 },
25435 );
25436
25437 let editor = workspace
25438 .update(cx, |workspace, window, cx| {
25439 workspace.open_abs_path(
25440 PathBuf::from(path!("/a/first.rs")),
25441 OpenOptions::default(),
25442 window,
25443 cx,
25444 )
25445 })
25446 .unwrap()
25447 .await
25448 .unwrap()
25449 .downcast::<Editor>()
25450 .unwrap();
25451 let fake_language_server = fake_servers.next().await.unwrap();
25452 let fake_language_server_without_capabilities =
25453 fake_servers_without_capabilities.next().await.unwrap();
25454 let requests_made = Arc::new(AtomicUsize::new(0));
25455 let closure_requests_made = Arc::clone(&requests_made);
25456 let mut color_request_handle = fake_language_server
25457 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
25458 let requests_made = Arc::clone(&closure_requests_made);
25459 async move {
25460 assert_eq!(
25461 params.text_document.uri,
25462 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25463 );
25464 requests_made.fetch_add(1, atomic::Ordering::Release);
25465 Ok(vec![
25466 lsp::ColorInformation {
25467 range: lsp::Range {
25468 start: lsp::Position {
25469 line: 0,
25470 character: 0,
25471 },
25472 end: lsp::Position {
25473 line: 0,
25474 character: 1,
25475 },
25476 },
25477 color: lsp::Color {
25478 red: 0.33,
25479 green: 0.33,
25480 blue: 0.33,
25481 alpha: 0.33,
25482 },
25483 },
25484 lsp::ColorInformation {
25485 range: lsp::Range {
25486 start: lsp::Position {
25487 line: 0,
25488 character: 0,
25489 },
25490 end: lsp::Position {
25491 line: 0,
25492 character: 1,
25493 },
25494 },
25495 color: lsp::Color {
25496 red: 0.33,
25497 green: 0.33,
25498 blue: 0.33,
25499 alpha: 0.33,
25500 },
25501 },
25502 ])
25503 }
25504 });
25505
25506 let _handle = fake_language_server_without_capabilities
25507 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
25508 panic!("Should not be called");
25509 });
25510 cx.executor().advance_clock(Duration::from_millis(100));
25511 color_request_handle.next().await.unwrap();
25512 cx.run_until_parked();
25513 assert_eq!(
25514 1,
25515 requests_made.load(atomic::Ordering::Acquire),
25516 "Should query for colors once per editor open"
25517 );
25518 editor.update_in(cx, |editor, _, cx| {
25519 assert_eq!(
25520 vec![expected_color],
25521 extract_color_inlays(editor, cx),
25522 "Should have an initial inlay"
25523 );
25524 });
25525
25526 // opening another file in a split should not influence the LSP query counter
25527 workspace
25528 .update(cx, |workspace, window, cx| {
25529 assert_eq!(
25530 workspace.panes().len(),
25531 1,
25532 "Should have one pane with one editor"
25533 );
25534 workspace.move_item_to_pane_in_direction(
25535 &MoveItemToPaneInDirection {
25536 direction: SplitDirection::Right,
25537 focus: false,
25538 clone: true,
25539 },
25540 window,
25541 cx,
25542 );
25543 })
25544 .unwrap();
25545 cx.run_until_parked();
25546 workspace
25547 .update(cx, |workspace, _, cx| {
25548 let panes = workspace.panes();
25549 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
25550 for pane in panes {
25551 let editor = pane
25552 .read(cx)
25553 .active_item()
25554 .and_then(|item| item.downcast::<Editor>())
25555 .expect("Should have opened an editor in each split");
25556 let editor_file = editor
25557 .read(cx)
25558 .buffer()
25559 .read(cx)
25560 .as_singleton()
25561 .expect("test deals with singleton buffers")
25562 .read(cx)
25563 .file()
25564 .expect("test buffese should have a file")
25565 .path();
25566 assert_eq!(
25567 editor_file.as_ref(),
25568 Path::new("first.rs"),
25569 "Both editors should be opened for the same file"
25570 )
25571 }
25572 })
25573 .unwrap();
25574
25575 cx.executor().advance_clock(Duration::from_millis(500));
25576 let save = editor.update_in(cx, |editor, window, cx| {
25577 editor.move_to_end(&MoveToEnd, window, cx);
25578 editor.handle_input("dirty", window, cx);
25579 editor.save(
25580 SaveOptions {
25581 format: true,
25582 autosave: true,
25583 },
25584 project.clone(),
25585 window,
25586 cx,
25587 )
25588 });
25589 save.await.unwrap();
25590
25591 color_request_handle.next().await.unwrap();
25592 cx.run_until_parked();
25593 assert_eq!(
25594 3,
25595 requests_made.load(atomic::Ordering::Acquire),
25596 "Should query for colors once per save and once per formatting after save"
25597 );
25598
25599 drop(editor);
25600 let close = workspace
25601 .update(cx, |workspace, window, cx| {
25602 workspace.active_pane().update(cx, |pane, cx| {
25603 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25604 })
25605 })
25606 .unwrap();
25607 close.await.unwrap();
25608 let close = workspace
25609 .update(cx, |workspace, window, cx| {
25610 workspace.active_pane().update(cx, |pane, cx| {
25611 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25612 })
25613 })
25614 .unwrap();
25615 close.await.unwrap();
25616 assert_eq!(
25617 3,
25618 requests_made.load(atomic::Ordering::Acquire),
25619 "After saving and closing all editors, no extra requests should be made"
25620 );
25621 workspace
25622 .update(cx, |workspace, _, cx| {
25623 assert!(
25624 workspace.active_item(cx).is_none(),
25625 "Should close all editors"
25626 )
25627 })
25628 .unwrap();
25629
25630 workspace
25631 .update(cx, |workspace, window, cx| {
25632 workspace.active_pane().update(cx, |pane, cx| {
25633 pane.navigate_backward(&workspace::GoBack, window, cx);
25634 })
25635 })
25636 .unwrap();
25637 cx.executor().advance_clock(Duration::from_millis(100));
25638 cx.run_until_parked();
25639 let editor = workspace
25640 .update(cx, |workspace, _, cx| {
25641 workspace
25642 .active_item(cx)
25643 .expect("Should have reopened the editor again after navigating back")
25644 .downcast::<Editor>()
25645 .expect("Should be an editor")
25646 })
25647 .unwrap();
25648 color_request_handle.next().await.unwrap();
25649 assert_eq!(
25650 3,
25651 requests_made.load(atomic::Ordering::Acquire),
25652 "Cache should be reused on buffer close and reopen"
25653 );
25654 editor.update(cx, |editor, cx| {
25655 assert_eq!(
25656 vec![expected_color],
25657 extract_color_inlays(editor, cx),
25658 "Should have an initial inlay"
25659 );
25660 });
25661
25662 drop(color_request_handle);
25663 let closure_requests_made = Arc::clone(&requests_made);
25664 let mut empty_color_request_handle = fake_language_server
25665 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
25666 let requests_made = Arc::clone(&closure_requests_made);
25667 async move {
25668 assert_eq!(
25669 params.text_document.uri,
25670 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25671 );
25672 requests_made.fetch_add(1, atomic::Ordering::Release);
25673 Ok(Vec::new())
25674 }
25675 });
25676 let save = editor.update_in(cx, |editor, window, cx| {
25677 editor.move_to_end(&MoveToEnd, window, cx);
25678 editor.handle_input("dirty_again", window, cx);
25679 editor.save(
25680 SaveOptions {
25681 format: false,
25682 autosave: true,
25683 },
25684 project.clone(),
25685 window,
25686 cx,
25687 )
25688 });
25689 save.await.unwrap();
25690
25691 empty_color_request_handle.next().await.unwrap();
25692 cx.run_until_parked();
25693 assert_eq!(
25694 4,
25695 requests_made.load(atomic::Ordering::Acquire),
25696 "Should query for colors once per save only, as formatting was not requested"
25697 );
25698 editor.update(cx, |editor, cx| {
25699 assert_eq!(
25700 Vec::<Rgba>::new(),
25701 extract_color_inlays(editor, cx),
25702 "Should clear all colors when the server returns an empty response"
25703 );
25704 });
25705}
25706
25707#[gpui::test]
25708async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
25709 init_test(cx, |_| {});
25710 let (editor, cx) = cx.add_window_view(Editor::single_line);
25711 editor.update_in(cx, |editor, window, cx| {
25712 editor.set_text("oops\n\nwow\n", window, cx)
25713 });
25714 cx.run_until_parked();
25715 editor.update(cx, |editor, cx| {
25716 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
25717 });
25718 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
25719 cx.run_until_parked();
25720 editor.update(cx, |editor, cx| {
25721 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
25722 });
25723}
25724
25725#[gpui::test]
25726async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
25727 init_test(cx, |_| {});
25728
25729 cx.update(|cx| {
25730 register_project_item::<Editor>(cx);
25731 });
25732
25733 let fs = FakeFs::new(cx.executor());
25734 fs.insert_tree("/root1", json!({})).await;
25735 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
25736 .await;
25737
25738 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
25739 let (workspace, cx) =
25740 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
25741
25742 let worktree_id = project.update(cx, |project, cx| {
25743 project.worktrees(cx).next().unwrap().read(cx).id()
25744 });
25745
25746 let handle = workspace
25747 .update_in(cx, |workspace, window, cx| {
25748 let project_path = (worktree_id, "one.pdf");
25749 workspace.open_path(project_path, None, true, window, cx)
25750 })
25751 .await
25752 .unwrap();
25753
25754 assert_eq!(
25755 handle.to_any().entity_type(),
25756 TypeId::of::<InvalidBufferView>()
25757 );
25758}
25759
25760#[gpui::test]
25761async fn test_select_next_prev_syntax_node(cx: &mut TestAppContext) {
25762 init_test(cx, |_| {});
25763
25764 let language = Arc::new(Language::new(
25765 LanguageConfig::default(),
25766 Some(tree_sitter_rust::LANGUAGE.into()),
25767 ));
25768
25769 // Test hierarchical sibling navigation
25770 let text = r#"
25771 fn outer() {
25772 if condition {
25773 let a = 1;
25774 }
25775 let b = 2;
25776 }
25777
25778 fn another() {
25779 let c = 3;
25780 }
25781 "#;
25782
25783 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
25784 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
25785 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
25786
25787 // Wait for parsing to complete
25788 editor
25789 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
25790 .await;
25791
25792 editor.update_in(cx, |editor, window, cx| {
25793 // Start by selecting "let a = 1;" inside the if block
25794 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25795 s.select_display_ranges([
25796 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 26)
25797 ]);
25798 });
25799
25800 let initial_selection = editor.selections.display_ranges(cx);
25801 assert_eq!(initial_selection.len(), 1, "Should have one selection");
25802
25803 // Test select next sibling - should move up levels to find the next sibling
25804 // Since "let a = 1;" has no siblings in the if block, it should move up
25805 // to find "let b = 2;" which is a sibling of the if block
25806 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
25807 let next_selection = editor.selections.display_ranges(cx);
25808
25809 // Should have a selection and it should be different from the initial
25810 assert_eq!(
25811 next_selection.len(),
25812 1,
25813 "Should have one selection after next"
25814 );
25815 assert_ne!(
25816 next_selection[0], initial_selection[0],
25817 "Next sibling selection should be different"
25818 );
25819
25820 // Test hierarchical navigation by going to the end of the current function
25821 // and trying to navigate to the next function
25822 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25823 s.select_display_ranges([
25824 DisplayPoint::new(DisplayRow(5), 12)..DisplayPoint::new(DisplayRow(5), 22)
25825 ]);
25826 });
25827
25828 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
25829 let function_next_selection = editor.selections.display_ranges(cx);
25830
25831 // Should move to the next function
25832 assert_eq!(
25833 function_next_selection.len(),
25834 1,
25835 "Should have one selection after function next"
25836 );
25837
25838 // Test select previous sibling navigation
25839 editor.select_prev_syntax_node(&SelectPreviousSyntaxNode, window, cx);
25840 let prev_selection = editor.selections.display_ranges(cx);
25841
25842 // Should have a selection and it should be different
25843 assert_eq!(
25844 prev_selection.len(),
25845 1,
25846 "Should have one selection after prev"
25847 );
25848 assert_ne!(
25849 prev_selection[0], function_next_selection[0],
25850 "Previous sibling selection should be different from next"
25851 );
25852 });
25853}
25854
25855#[gpui::test]
25856async fn test_next_prev_document_highlight(cx: &mut TestAppContext) {
25857 init_test(cx, |_| {});
25858
25859 let mut cx = EditorTestContext::new(cx).await;
25860 cx.set_state(
25861 "let ˇvariable = 42;
25862let another = variable + 1;
25863let result = variable * 2;",
25864 );
25865
25866 // Set up document highlights manually (simulating LSP response)
25867 cx.update_editor(|editor, _window, cx| {
25868 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
25869
25870 // Create highlights for "variable" occurrences
25871 let highlight_ranges = [
25872 Point::new(0, 4)..Point::new(0, 12), // First "variable"
25873 Point::new(1, 14)..Point::new(1, 22), // Second "variable"
25874 Point::new(2, 13)..Point::new(2, 21), // Third "variable"
25875 ];
25876
25877 let anchor_ranges: Vec<_> = highlight_ranges
25878 .iter()
25879 .map(|range| range.clone().to_anchors(&buffer_snapshot))
25880 .collect();
25881
25882 editor.highlight_background::<DocumentHighlightRead>(
25883 &anchor_ranges,
25884 |theme| theme.colors().editor_document_highlight_read_background,
25885 cx,
25886 );
25887 });
25888
25889 // Go to next highlight - should move to second "variable"
25890 cx.update_editor(|editor, window, cx| {
25891 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
25892 });
25893 cx.assert_editor_state(
25894 "let variable = 42;
25895let another = ˇvariable + 1;
25896let result = variable * 2;",
25897 );
25898
25899 // Go to next highlight - should move to third "variable"
25900 cx.update_editor(|editor, window, cx| {
25901 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
25902 });
25903 cx.assert_editor_state(
25904 "let variable = 42;
25905let another = variable + 1;
25906let result = ˇvariable * 2;",
25907 );
25908
25909 // Go to next highlight - should stay at third "variable" (no wrap-around)
25910 cx.update_editor(|editor, window, cx| {
25911 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
25912 });
25913 cx.assert_editor_state(
25914 "let variable = 42;
25915let another = variable + 1;
25916let result = ˇvariable * 2;",
25917 );
25918
25919 // Now test going backwards from third position
25920 cx.update_editor(|editor, window, cx| {
25921 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
25922 });
25923 cx.assert_editor_state(
25924 "let variable = 42;
25925let another = ˇvariable + 1;
25926let result = variable * 2;",
25927 );
25928
25929 // Go to previous highlight - should move to first "variable"
25930 cx.update_editor(|editor, window, cx| {
25931 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
25932 });
25933 cx.assert_editor_state(
25934 "let ˇvariable = 42;
25935let another = variable + 1;
25936let result = variable * 2;",
25937 );
25938
25939 // Go to previous highlight - should stay on first "variable"
25940 cx.update_editor(|editor, window, cx| {
25941 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
25942 });
25943 cx.assert_editor_state(
25944 "let ˇvariable = 42;
25945let another = variable + 1;
25946let result = variable * 2;",
25947 );
25948}
25949
25950#[track_caller]
25951fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
25952 editor
25953 .all_inlays(cx)
25954 .into_iter()
25955 .filter_map(|inlay| inlay.get_color())
25956 .map(Rgba::from)
25957 .collect()
25958}