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 collections::HashMap;
17use futures::{StreamExt, channel::oneshot};
18use gpui::{
19 BackgroundExecutor, DismissEvent, Rgba, SemanticVersion, TestAppContext, UpdateGlobal,
20 VisualTestContext, WindowBounds, WindowOptions, div,
21};
22use indoc::indoc;
23use language::{
24 BracketPairConfig,
25 Capability::ReadWrite,
26 DiagnosticSourceKind, FakeLspAdapter, IndentGuideSettings, LanguageConfig,
27 LanguageConfigOverride, LanguageMatcher, LanguageName, Override, Point,
28 language_settings::{
29 CompletionSettingsContent, FormatterList, LanguageSettingsContent, LspInsertMode,
30 },
31 tree_sitter_python,
32};
33use language_settings::Formatter;
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,
42};
43use serde_json::{self, json};
44use settings::{
45 AllLanguageSettingsContent, IndentGuideBackgroundColoring, IndentGuideColoring,
46 ProjectSettingsContent,
47};
48use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
49use std::{
50 iter,
51 sync::atomic::{self, AtomicUsize},
52};
53use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
54use text::ToPoint as _;
55use unindent::Unindent;
56use util::{
57 assert_set_eq, path,
58 rel_path::rel_path,
59 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
60 uri,
61};
62use workspace::{
63 CloseActiveItem, CloseAllItems, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry,
64 OpenOptions, ViewId,
65 invalid_buffer_view::InvalidBufferView,
66 item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
67 register_project_item,
68};
69
70#[gpui::test]
71fn test_edit_events(cx: &mut TestAppContext) {
72 init_test(cx, |_| {});
73
74 let buffer = cx.new(|cx| {
75 let mut buffer = language::Buffer::local("123456", cx);
76 buffer.set_group_interval(Duration::from_secs(1));
77 buffer
78 });
79
80 let events = Rc::new(RefCell::new(Vec::new()));
81 let editor1 = cx.add_window({
82 let events = events.clone();
83 |window, cx| {
84 let entity = cx.entity();
85 cx.subscribe_in(
86 &entity,
87 window,
88 move |_, _, event: &EditorEvent, _, _| match event {
89 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
90 EditorEvent::BufferEdited => {
91 events.borrow_mut().push(("editor1", "buffer edited"))
92 }
93 _ => {}
94 },
95 )
96 .detach();
97 Editor::for_buffer(buffer.clone(), None, window, cx)
98 }
99 });
100
101 let editor2 = cx.add_window({
102 let events = events.clone();
103 |window, cx| {
104 cx.subscribe_in(
105 &cx.entity(),
106 window,
107 move |_, _, event: &EditorEvent, _, _| match event {
108 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
109 EditorEvent::BufferEdited => {
110 events.borrow_mut().push(("editor2", "buffer edited"))
111 }
112 _ => {}
113 },
114 )
115 .detach();
116 Editor::for_buffer(buffer.clone(), None, window, cx)
117 }
118 });
119
120 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
121
122 // Mutating editor 1 will emit an `Edited` event only for that editor.
123 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
124 assert_eq!(
125 mem::take(&mut *events.borrow_mut()),
126 [
127 ("editor1", "edited"),
128 ("editor1", "buffer edited"),
129 ("editor2", "buffer edited"),
130 ]
131 );
132
133 // Mutating editor 2 will emit an `Edited` event only for that editor.
134 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
135 assert_eq!(
136 mem::take(&mut *events.borrow_mut()),
137 [
138 ("editor2", "edited"),
139 ("editor1", "buffer edited"),
140 ("editor2", "buffer edited"),
141 ]
142 );
143
144 // Undoing on editor 1 will emit an `Edited` event only for that editor.
145 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
146 assert_eq!(
147 mem::take(&mut *events.borrow_mut()),
148 [
149 ("editor1", "edited"),
150 ("editor1", "buffer edited"),
151 ("editor2", "buffer edited"),
152 ]
153 );
154
155 // Redoing on editor 1 will emit an `Edited` event only for that editor.
156 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
157 assert_eq!(
158 mem::take(&mut *events.borrow_mut()),
159 [
160 ("editor1", "edited"),
161 ("editor1", "buffer edited"),
162 ("editor2", "buffer edited"),
163 ]
164 );
165
166 // Undoing on editor 2 will emit an `Edited` event only for that editor.
167 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
168 assert_eq!(
169 mem::take(&mut *events.borrow_mut()),
170 [
171 ("editor2", "edited"),
172 ("editor1", "buffer edited"),
173 ("editor2", "buffer edited"),
174 ]
175 );
176
177 // Redoing on editor 2 will emit an `Edited` event only for that editor.
178 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
179 assert_eq!(
180 mem::take(&mut *events.borrow_mut()),
181 [
182 ("editor2", "edited"),
183 ("editor1", "buffer edited"),
184 ("editor2", "buffer edited"),
185 ]
186 );
187
188 // No event is emitted when the mutation is a no-op.
189 _ = editor2.update(cx, |editor, window, cx| {
190 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
191 s.select_ranges([0..0])
192 });
193
194 editor.backspace(&Backspace, window, cx);
195 });
196 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
197}
198
199#[gpui::test]
200fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
201 init_test(cx, |_| {});
202
203 let mut now = Instant::now();
204 let group_interval = Duration::from_millis(1);
205 let buffer = cx.new(|cx| {
206 let mut buf = language::Buffer::local("123456", cx);
207 buf.set_group_interval(group_interval);
208 buf
209 });
210 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
211 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
212
213 _ = editor.update(cx, |editor, window, cx| {
214 editor.start_transaction_at(now, window, cx);
215 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
216 s.select_ranges([2..4])
217 });
218
219 editor.insert("cd", window, cx);
220 editor.end_transaction_at(now, cx);
221 assert_eq!(editor.text(cx), "12cd56");
222 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
223
224 editor.start_transaction_at(now, window, cx);
225 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
226 s.select_ranges([4..5])
227 });
228 editor.insert("e", window, cx);
229 editor.end_transaction_at(now, cx);
230 assert_eq!(editor.text(cx), "12cde6");
231 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
232
233 now += group_interval + Duration::from_millis(1);
234 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
235 s.select_ranges([2..2])
236 });
237
238 // Simulate an edit in another editor
239 buffer.update(cx, |buffer, cx| {
240 buffer.start_transaction_at(now, cx);
241 buffer.edit([(0..1, "a")], None, cx);
242 buffer.edit([(1..1, "b")], None, cx);
243 buffer.end_transaction_at(now, cx);
244 });
245
246 assert_eq!(editor.text(cx), "ab2cde6");
247 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
248
249 // Last transaction happened past the group interval in a different editor.
250 // Undo it individually and don't restore selections.
251 editor.undo(&Undo, window, cx);
252 assert_eq!(editor.text(cx), "12cde6");
253 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
254
255 // First two transactions happened within the group interval in this editor.
256 // Undo them together and restore selections.
257 editor.undo(&Undo, window, cx);
258 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
259 assert_eq!(editor.text(cx), "123456");
260 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
261
262 // Redo the first two transactions together.
263 editor.redo(&Redo, window, cx);
264 assert_eq!(editor.text(cx), "12cde6");
265 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
266
267 // Redo the last transaction on its own.
268 editor.redo(&Redo, window, cx);
269 assert_eq!(editor.text(cx), "ab2cde6");
270 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
271
272 // Test empty transactions.
273 editor.start_transaction_at(now, window, cx);
274 editor.end_transaction_at(now, cx);
275 editor.undo(&Undo, window, cx);
276 assert_eq!(editor.text(cx), "12cde6");
277 });
278}
279
280#[gpui::test]
281fn test_ime_composition(cx: &mut TestAppContext) {
282 init_test(cx, |_| {});
283
284 let buffer = cx.new(|cx| {
285 let mut buffer = language::Buffer::local("abcde", cx);
286 // Ensure automatic grouping doesn't occur.
287 buffer.set_group_interval(Duration::ZERO);
288 buffer
289 });
290
291 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
292 cx.add_window(|window, cx| {
293 let mut editor = build_editor(buffer.clone(), window, cx);
294
295 // Start a new IME composition.
296 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
297 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
298 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
299 assert_eq!(editor.text(cx), "äbcde");
300 assert_eq!(
301 editor.marked_text_ranges(cx),
302 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
303 );
304
305 // Finalize IME composition.
306 editor.replace_text_in_range(None, "ā", window, cx);
307 assert_eq!(editor.text(cx), "ābcde");
308 assert_eq!(editor.marked_text_ranges(cx), None);
309
310 // IME composition edits are grouped and are undone/redone at once.
311 editor.undo(&Default::default(), window, cx);
312 assert_eq!(editor.text(cx), "abcde");
313 assert_eq!(editor.marked_text_ranges(cx), None);
314 editor.redo(&Default::default(), window, cx);
315 assert_eq!(editor.text(cx), "ābcde");
316 assert_eq!(editor.marked_text_ranges(cx), None);
317
318 // Start a new IME composition.
319 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
320 assert_eq!(
321 editor.marked_text_ranges(cx),
322 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
323 );
324
325 // Undoing during an IME composition cancels it.
326 editor.undo(&Default::default(), window, cx);
327 assert_eq!(editor.text(cx), "ābcde");
328 assert_eq!(editor.marked_text_ranges(cx), None);
329
330 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
331 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
332 assert_eq!(editor.text(cx), "ābcdè");
333 assert_eq!(
334 editor.marked_text_ranges(cx),
335 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
336 );
337
338 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
339 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
340 assert_eq!(editor.text(cx), "ābcdę");
341 assert_eq!(editor.marked_text_ranges(cx), None);
342
343 // Start a new IME composition with multiple cursors.
344 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
345 s.select_ranges([
346 OffsetUtf16(1)..OffsetUtf16(1),
347 OffsetUtf16(3)..OffsetUtf16(3),
348 OffsetUtf16(5)..OffsetUtf16(5),
349 ])
350 });
351 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
352 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
353 assert_eq!(
354 editor.marked_text_ranges(cx),
355 Some(vec![
356 OffsetUtf16(0)..OffsetUtf16(3),
357 OffsetUtf16(4)..OffsetUtf16(7),
358 OffsetUtf16(8)..OffsetUtf16(11)
359 ])
360 );
361
362 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
363 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
364 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
365 assert_eq!(
366 editor.marked_text_ranges(cx),
367 Some(vec![
368 OffsetUtf16(1)..OffsetUtf16(2),
369 OffsetUtf16(5)..OffsetUtf16(6),
370 OffsetUtf16(9)..OffsetUtf16(10)
371 ])
372 );
373
374 // Finalize IME composition with multiple cursors.
375 editor.replace_text_in_range(Some(9..10), "2", window, cx);
376 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
377 assert_eq!(editor.marked_text_ranges(cx), None);
378
379 editor
380 });
381}
382
383#[gpui::test]
384fn test_selection_with_mouse(cx: &mut TestAppContext) {
385 init_test(cx, |_| {});
386
387 let editor = cx.add_window(|window, cx| {
388 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
389 build_editor(buffer, window, cx)
390 });
391
392 _ = editor.update(cx, |editor, window, cx| {
393 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
394 });
395 assert_eq!(
396 editor
397 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
398 .unwrap(),
399 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
400 );
401
402 _ = editor.update(cx, |editor, window, cx| {
403 editor.update_selection(
404 DisplayPoint::new(DisplayRow(3), 3),
405 0,
406 gpui::Point::<f32>::default(),
407 window,
408 cx,
409 );
410 });
411
412 assert_eq!(
413 editor
414 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
415 .unwrap(),
416 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
417 );
418
419 _ = editor.update(cx, |editor, window, cx| {
420 editor.update_selection(
421 DisplayPoint::new(DisplayRow(1), 1),
422 0,
423 gpui::Point::<f32>::default(),
424 window,
425 cx,
426 );
427 });
428
429 assert_eq!(
430 editor
431 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
432 .unwrap(),
433 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
434 );
435
436 _ = editor.update(cx, |editor, window, cx| {
437 editor.end_selection(window, cx);
438 editor.update_selection(
439 DisplayPoint::new(DisplayRow(3), 3),
440 0,
441 gpui::Point::<f32>::default(),
442 window,
443 cx,
444 );
445 });
446
447 assert_eq!(
448 editor
449 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
450 .unwrap(),
451 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
452 );
453
454 _ = editor.update(cx, |editor, window, cx| {
455 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
456 editor.update_selection(
457 DisplayPoint::new(DisplayRow(0), 0),
458 0,
459 gpui::Point::<f32>::default(),
460 window,
461 cx,
462 );
463 });
464
465 assert_eq!(
466 editor
467 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
468 .unwrap(),
469 [
470 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
471 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
472 ]
473 );
474
475 _ = editor.update(cx, |editor, window, cx| {
476 editor.end_selection(window, cx);
477 });
478
479 assert_eq!(
480 editor
481 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
482 .unwrap(),
483 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
484 );
485}
486
487#[gpui::test]
488fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
489 init_test(cx, |_| {});
490
491 let editor = cx.add_window(|window, cx| {
492 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
493 build_editor(buffer, window, cx)
494 });
495
496 _ = editor.update(cx, |editor, window, cx| {
497 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
498 });
499
500 _ = editor.update(cx, |editor, window, cx| {
501 editor.end_selection(window, cx);
502 });
503
504 _ = editor.update(cx, |editor, window, cx| {
505 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
506 });
507
508 _ = editor.update(cx, |editor, window, cx| {
509 editor.end_selection(window, cx);
510 });
511
512 assert_eq!(
513 editor
514 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
515 .unwrap(),
516 [
517 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
518 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
519 ]
520 );
521
522 _ = editor.update(cx, |editor, window, cx| {
523 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
524 });
525
526 _ = editor.update(cx, |editor, window, cx| {
527 editor.end_selection(window, cx);
528 });
529
530 assert_eq!(
531 editor
532 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
533 .unwrap(),
534 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
535 );
536}
537
538#[gpui::test]
539fn test_canceling_pending_selection(cx: &mut TestAppContext) {
540 init_test(cx, |_| {});
541
542 let editor = cx.add_window(|window, cx| {
543 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
544 build_editor(buffer, window, cx)
545 });
546
547 _ = editor.update(cx, |editor, window, cx| {
548 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
549 assert_eq!(
550 editor.selections.display_ranges(cx),
551 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
552 );
553 });
554
555 _ = editor.update(cx, |editor, window, cx| {
556 editor.update_selection(
557 DisplayPoint::new(DisplayRow(3), 3),
558 0,
559 gpui::Point::<f32>::default(),
560 window,
561 cx,
562 );
563 assert_eq!(
564 editor.selections.display_ranges(cx),
565 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
566 );
567 });
568
569 _ = editor.update(cx, |editor, window, cx| {
570 editor.cancel(&Cancel, window, cx);
571 editor.update_selection(
572 DisplayPoint::new(DisplayRow(1), 1),
573 0,
574 gpui::Point::<f32>::default(),
575 window,
576 cx,
577 );
578 assert_eq!(
579 editor.selections.display_ranges(cx),
580 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
581 );
582 });
583}
584
585#[gpui::test]
586fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
587 init_test(cx, |_| {});
588
589 let editor = cx.add_window(|window, cx| {
590 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
591 build_editor(buffer, window, cx)
592 });
593
594 _ = editor.update(cx, |editor, window, cx| {
595 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
596 assert_eq!(
597 editor.selections.display_ranges(cx),
598 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
599 );
600
601 editor.move_down(&Default::default(), window, cx);
602 assert_eq!(
603 editor.selections.display_ranges(cx),
604 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
605 );
606
607 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
608 assert_eq!(
609 editor.selections.display_ranges(cx),
610 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
611 );
612
613 editor.move_up(&Default::default(), window, cx);
614 assert_eq!(
615 editor.selections.display_ranges(cx),
616 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
617 );
618 });
619}
620
621#[gpui::test]
622fn test_clone(cx: &mut TestAppContext) {
623 init_test(cx, |_| {});
624
625 let (text, selection_ranges) = marked_text_ranges(
626 indoc! {"
627 one
628 two
629 threeˇ
630 four
631 fiveˇ
632 "},
633 true,
634 );
635
636 let editor = cx.add_window(|window, cx| {
637 let buffer = MultiBuffer::build_simple(&text, cx);
638 build_editor(buffer, window, cx)
639 });
640
641 _ = editor.update(cx, |editor, window, cx| {
642 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
643 s.select_ranges(selection_ranges.clone())
644 });
645 editor.fold_creases(
646 vec![
647 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
648 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
649 ],
650 true,
651 window,
652 cx,
653 );
654 });
655
656 let cloned_editor = editor
657 .update(cx, |editor, _, cx| {
658 cx.open_window(Default::default(), |window, cx| {
659 cx.new(|cx| editor.clone(window, cx))
660 })
661 })
662 .unwrap()
663 .unwrap();
664
665 let snapshot = editor
666 .update(cx, |e, window, cx| e.snapshot(window, cx))
667 .unwrap();
668 let cloned_snapshot = cloned_editor
669 .update(cx, |e, window, cx| e.snapshot(window, cx))
670 .unwrap();
671
672 assert_eq!(
673 cloned_editor
674 .update(cx, |e, _, cx| e.display_text(cx))
675 .unwrap(),
676 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
677 );
678 assert_eq!(
679 cloned_snapshot
680 .folds_in_range(0..text.len())
681 .collect::<Vec<_>>(),
682 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
683 );
684 assert_set_eq!(
685 cloned_editor
686 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
687 .unwrap(),
688 editor
689 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
690 .unwrap()
691 );
692 assert_set_eq!(
693 cloned_editor
694 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
695 .unwrap(),
696 editor
697 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
698 .unwrap()
699 );
700}
701
702#[gpui::test]
703async fn test_navigation_history(cx: &mut TestAppContext) {
704 init_test(cx, |_| {});
705
706 use workspace::item::Item;
707
708 let fs = FakeFs::new(cx.executor());
709 let project = Project::test(fs, [], cx).await;
710 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
711 let pane = workspace
712 .update(cx, |workspace, _, _| workspace.active_pane().clone())
713 .unwrap();
714
715 _ = workspace.update(cx, |_v, window, cx| {
716 cx.new(|cx| {
717 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
718 let mut editor = build_editor(buffer, window, cx);
719 let handle = cx.entity();
720 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
721
722 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
723 editor.nav_history.as_mut().unwrap().pop_backward(cx)
724 }
725
726 // Move the cursor a small distance.
727 // Nothing is added to the navigation history.
728 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
729 s.select_display_ranges([
730 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
731 ])
732 });
733 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
734 s.select_display_ranges([
735 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
736 ])
737 });
738 assert!(pop_history(&mut editor, cx).is_none());
739
740 // Move the cursor a large distance.
741 // The history can jump back to the previous position.
742 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
743 s.select_display_ranges([
744 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
745 ])
746 });
747 let nav_entry = pop_history(&mut editor, cx).unwrap();
748 editor.navigate(nav_entry.data.unwrap(), window, cx);
749 assert_eq!(nav_entry.item.id(), cx.entity_id());
750 assert_eq!(
751 editor.selections.display_ranges(cx),
752 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
753 );
754 assert!(pop_history(&mut editor, cx).is_none());
755
756 // Move the cursor a small distance via the mouse.
757 // Nothing is added to the navigation history.
758 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
759 editor.end_selection(window, cx);
760 assert_eq!(
761 editor.selections.display_ranges(cx),
762 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
763 );
764 assert!(pop_history(&mut editor, cx).is_none());
765
766 // Move the cursor a large distance via the mouse.
767 // The history can jump back to the previous position.
768 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
769 editor.end_selection(window, cx);
770 assert_eq!(
771 editor.selections.display_ranges(cx),
772 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
773 );
774 let nav_entry = pop_history(&mut editor, cx).unwrap();
775 editor.navigate(nav_entry.data.unwrap(), window, cx);
776 assert_eq!(nav_entry.item.id(), cx.entity_id());
777 assert_eq!(
778 editor.selections.display_ranges(cx),
779 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
780 );
781 assert!(pop_history(&mut editor, cx).is_none());
782
783 // Set scroll position to check later
784 editor.set_scroll_position(gpui::Point::<f64>::new(5.5, 5.5), window, cx);
785 let original_scroll_position = editor.scroll_manager.anchor();
786
787 // Jump to the end of the document and adjust scroll
788 editor.move_to_end(&MoveToEnd, window, cx);
789 editor.set_scroll_position(gpui::Point::<f64>::new(-2.5, -0.5), window, cx);
790 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
791
792 let nav_entry = pop_history(&mut editor, cx).unwrap();
793 editor.navigate(nav_entry.data.unwrap(), window, cx);
794 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
795
796 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
797 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
798 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
799 let invalid_point = Point::new(9999, 0);
800 editor.navigate(
801 Box::new(NavigationData {
802 cursor_anchor: invalid_anchor,
803 cursor_position: invalid_point,
804 scroll_anchor: ScrollAnchor {
805 anchor: invalid_anchor,
806 offset: Default::default(),
807 },
808 scroll_top_row: invalid_point.row,
809 }),
810 window,
811 cx,
812 );
813 assert_eq!(
814 editor.selections.display_ranges(cx),
815 &[editor.max_point(cx)..editor.max_point(cx)]
816 );
817 assert_eq!(
818 editor.scroll_position(cx),
819 gpui::Point::new(0., editor.max_point(cx).row().as_f64())
820 );
821
822 editor
823 })
824 });
825}
826
827#[gpui::test]
828fn test_cancel(cx: &mut TestAppContext) {
829 init_test(cx, |_| {});
830
831 let editor = cx.add_window(|window, cx| {
832 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
833 build_editor(buffer, window, cx)
834 });
835
836 _ = editor.update(cx, |editor, window, cx| {
837 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
838 editor.update_selection(
839 DisplayPoint::new(DisplayRow(1), 1),
840 0,
841 gpui::Point::<f32>::default(),
842 window,
843 cx,
844 );
845 editor.end_selection(window, cx);
846
847 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
848 editor.update_selection(
849 DisplayPoint::new(DisplayRow(0), 3),
850 0,
851 gpui::Point::<f32>::default(),
852 window,
853 cx,
854 );
855 editor.end_selection(window, cx);
856 assert_eq!(
857 editor.selections.display_ranges(cx),
858 [
859 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
860 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
861 ]
862 );
863 });
864
865 _ = editor.update(cx, |editor, window, cx| {
866 editor.cancel(&Cancel, window, cx);
867 assert_eq!(
868 editor.selections.display_ranges(cx),
869 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
870 );
871 });
872
873 _ = editor.update(cx, |editor, window, cx| {
874 editor.cancel(&Cancel, window, cx);
875 assert_eq!(
876 editor.selections.display_ranges(cx),
877 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
878 );
879 });
880}
881
882#[gpui::test]
883fn test_fold_action(cx: &mut TestAppContext) {
884 init_test(cx, |_| {});
885
886 let editor = cx.add_window(|window, cx| {
887 let buffer = MultiBuffer::build_simple(
888 &"
889 impl Foo {
890 // Hello!
891
892 fn a() {
893 1
894 }
895
896 fn b() {
897 2
898 }
899
900 fn c() {
901 3
902 }
903 }
904 "
905 .unindent(),
906 cx,
907 );
908 build_editor(buffer, window, cx)
909 });
910
911 _ = editor.update(cx, |editor, window, cx| {
912 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
913 s.select_display_ranges([
914 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
915 ]);
916 });
917 editor.fold(&Fold, window, cx);
918 assert_eq!(
919 editor.display_text(cx),
920 "
921 impl Foo {
922 // Hello!
923
924 fn a() {
925 1
926 }
927
928 fn b() {⋯
929 }
930
931 fn c() {⋯
932 }
933 }
934 "
935 .unindent(),
936 );
937
938 editor.fold(&Fold, window, cx);
939 assert_eq!(
940 editor.display_text(cx),
941 "
942 impl Foo {⋯
943 }
944 "
945 .unindent(),
946 );
947
948 editor.unfold_lines(&UnfoldLines, window, cx);
949 assert_eq!(
950 editor.display_text(cx),
951 "
952 impl Foo {
953 // Hello!
954
955 fn a() {
956 1
957 }
958
959 fn b() {⋯
960 }
961
962 fn c() {⋯
963 }
964 }
965 "
966 .unindent(),
967 );
968
969 editor.unfold_lines(&UnfoldLines, window, cx);
970 assert_eq!(
971 editor.display_text(cx),
972 editor.buffer.read(cx).read(cx).text()
973 );
974 });
975}
976
977#[gpui::test]
978fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
979 init_test(cx, |_| {});
980
981 let editor = cx.add_window(|window, cx| {
982 let buffer = MultiBuffer::build_simple(
983 &"
984 class Foo:
985 # Hello!
986
987 def a():
988 print(1)
989
990 def b():
991 print(2)
992
993 def c():
994 print(3)
995 "
996 .unindent(),
997 cx,
998 );
999 build_editor(buffer, window, cx)
1000 });
1001
1002 _ = editor.update(cx, |editor, window, cx| {
1003 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1004 s.select_display_ranges([
1005 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
1006 ]);
1007 });
1008 editor.fold(&Fold, window, cx);
1009 assert_eq!(
1010 editor.display_text(cx),
1011 "
1012 class Foo:
1013 # Hello!
1014
1015 def a():
1016 print(1)
1017
1018 def b():⋯
1019
1020 def c():⋯
1021 "
1022 .unindent(),
1023 );
1024
1025 editor.fold(&Fold, window, cx);
1026 assert_eq!(
1027 editor.display_text(cx),
1028 "
1029 class Foo:⋯
1030 "
1031 .unindent(),
1032 );
1033
1034 editor.unfold_lines(&UnfoldLines, window, cx);
1035 assert_eq!(
1036 editor.display_text(cx),
1037 "
1038 class Foo:
1039 # Hello!
1040
1041 def a():
1042 print(1)
1043
1044 def b():⋯
1045
1046 def c():⋯
1047 "
1048 .unindent(),
1049 );
1050
1051 editor.unfold_lines(&UnfoldLines, window, cx);
1052 assert_eq!(
1053 editor.display_text(cx),
1054 editor.buffer.read(cx).read(cx).text()
1055 );
1056 });
1057}
1058
1059#[gpui::test]
1060fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1061 init_test(cx, |_| {});
1062
1063 let editor = cx.add_window(|window, cx| {
1064 let buffer = MultiBuffer::build_simple(
1065 &"
1066 class Foo:
1067 # Hello!
1068
1069 def a():
1070 print(1)
1071
1072 def b():
1073 print(2)
1074
1075
1076 def c():
1077 print(3)
1078
1079
1080 "
1081 .unindent(),
1082 cx,
1083 );
1084 build_editor(buffer, window, cx)
1085 });
1086
1087 _ = editor.update(cx, |editor, window, cx| {
1088 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1089 s.select_display_ranges([
1090 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1091 ]);
1092 });
1093 editor.fold(&Fold, window, cx);
1094 assert_eq!(
1095 editor.display_text(cx),
1096 "
1097 class Foo:
1098 # Hello!
1099
1100 def a():
1101 print(1)
1102
1103 def b():⋯
1104
1105
1106 def c():⋯
1107
1108
1109 "
1110 .unindent(),
1111 );
1112
1113 editor.fold(&Fold, window, cx);
1114 assert_eq!(
1115 editor.display_text(cx),
1116 "
1117 class Foo:⋯
1118
1119
1120 "
1121 .unindent(),
1122 );
1123
1124 editor.unfold_lines(&UnfoldLines, window, cx);
1125 assert_eq!(
1126 editor.display_text(cx),
1127 "
1128 class Foo:
1129 # Hello!
1130
1131 def a():
1132 print(1)
1133
1134 def b():⋯
1135
1136
1137 def c():⋯
1138
1139
1140 "
1141 .unindent(),
1142 );
1143
1144 editor.unfold_lines(&UnfoldLines, window, cx);
1145 assert_eq!(
1146 editor.display_text(cx),
1147 editor.buffer.read(cx).read(cx).text()
1148 );
1149 });
1150}
1151
1152#[gpui::test]
1153fn test_fold_at_level(cx: &mut TestAppContext) {
1154 init_test(cx, |_| {});
1155
1156 let editor = cx.add_window(|window, cx| {
1157 let buffer = MultiBuffer::build_simple(
1158 &"
1159 class Foo:
1160 # Hello!
1161
1162 def a():
1163 print(1)
1164
1165 def b():
1166 print(2)
1167
1168
1169 class Bar:
1170 # World!
1171
1172 def a():
1173 print(1)
1174
1175 def b():
1176 print(2)
1177
1178
1179 "
1180 .unindent(),
1181 cx,
1182 );
1183 build_editor(buffer, window, cx)
1184 });
1185
1186 _ = editor.update(cx, |editor, window, cx| {
1187 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1188 assert_eq!(
1189 editor.display_text(cx),
1190 "
1191 class Foo:
1192 # Hello!
1193
1194 def a():⋯
1195
1196 def b():⋯
1197
1198
1199 class Bar:
1200 # World!
1201
1202 def a():⋯
1203
1204 def b():⋯
1205
1206
1207 "
1208 .unindent(),
1209 );
1210
1211 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1212 assert_eq!(
1213 editor.display_text(cx),
1214 "
1215 class Foo:⋯
1216
1217
1218 class Bar:⋯
1219
1220
1221 "
1222 .unindent(),
1223 );
1224
1225 editor.unfold_all(&UnfoldAll, window, cx);
1226 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1227 assert_eq!(
1228 editor.display_text(cx),
1229 "
1230 class Foo:
1231 # Hello!
1232
1233 def a():
1234 print(1)
1235
1236 def b():
1237 print(2)
1238
1239
1240 class Bar:
1241 # World!
1242
1243 def a():
1244 print(1)
1245
1246 def b():
1247 print(2)
1248
1249
1250 "
1251 .unindent(),
1252 );
1253
1254 assert_eq!(
1255 editor.display_text(cx),
1256 editor.buffer.read(cx).read(cx).text()
1257 );
1258 let (_, positions) = marked_text_ranges(
1259 &"
1260 class Foo:
1261 # Hello!
1262
1263 def a():
1264 print(1)
1265
1266 def b():
1267 p«riˇ»nt(2)
1268
1269
1270 class Bar:
1271 # World!
1272
1273 def a():
1274 «ˇprint(1)
1275
1276 def b():
1277 print(2)»
1278
1279
1280 "
1281 .unindent(),
1282 true,
1283 );
1284
1285 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
1286 s.select_ranges(positions)
1287 });
1288
1289 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1290 assert_eq!(
1291 editor.display_text(cx),
1292 "
1293 class Foo:
1294 # Hello!
1295
1296 def a():⋯
1297
1298 def b():
1299 print(2)
1300
1301
1302 class Bar:
1303 # World!
1304
1305 def a():
1306 print(1)
1307
1308 def b():
1309 print(2)
1310
1311
1312 "
1313 .unindent(),
1314 );
1315 });
1316}
1317
1318#[gpui::test]
1319fn test_move_cursor(cx: &mut TestAppContext) {
1320 init_test(cx, |_| {});
1321
1322 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1323 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1324
1325 buffer.update(cx, |buffer, cx| {
1326 buffer.edit(
1327 vec![
1328 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1329 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1330 ],
1331 None,
1332 cx,
1333 );
1334 });
1335 _ = editor.update(cx, |editor, window, cx| {
1336 assert_eq!(
1337 editor.selections.display_ranges(cx),
1338 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1339 );
1340
1341 editor.move_down(&MoveDown, window, cx);
1342 assert_eq!(
1343 editor.selections.display_ranges(cx),
1344 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1345 );
1346
1347 editor.move_right(&MoveRight, window, cx);
1348 assert_eq!(
1349 editor.selections.display_ranges(cx),
1350 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1351 );
1352
1353 editor.move_left(&MoveLeft, window, cx);
1354 assert_eq!(
1355 editor.selections.display_ranges(cx),
1356 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1357 );
1358
1359 editor.move_up(&MoveUp, window, cx);
1360 assert_eq!(
1361 editor.selections.display_ranges(cx),
1362 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1363 );
1364
1365 editor.move_to_end(&MoveToEnd, window, cx);
1366 assert_eq!(
1367 editor.selections.display_ranges(cx),
1368 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1369 );
1370
1371 editor.move_to_beginning(&MoveToBeginning, window, cx);
1372 assert_eq!(
1373 editor.selections.display_ranges(cx),
1374 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1375 );
1376
1377 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1378 s.select_display_ranges([
1379 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1380 ]);
1381 });
1382 editor.select_to_beginning(&SelectToBeginning, window, cx);
1383 assert_eq!(
1384 editor.selections.display_ranges(cx),
1385 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1386 );
1387
1388 editor.select_to_end(&SelectToEnd, window, cx);
1389 assert_eq!(
1390 editor.selections.display_ranges(cx),
1391 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1392 );
1393 });
1394}
1395
1396#[gpui::test]
1397fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1398 init_test(cx, |_| {});
1399
1400 let editor = cx.add_window(|window, cx| {
1401 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1402 build_editor(buffer, window, cx)
1403 });
1404
1405 assert_eq!('🟥'.len_utf8(), 4);
1406 assert_eq!('α'.len_utf8(), 2);
1407
1408 _ = editor.update(cx, |editor, window, cx| {
1409 editor.fold_creases(
1410 vec![
1411 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1412 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1413 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1414 ],
1415 true,
1416 window,
1417 cx,
1418 );
1419 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1420
1421 editor.move_right(&MoveRight, window, cx);
1422 assert_eq!(
1423 editor.selections.display_ranges(cx),
1424 &[empty_range(0, "🟥".len())]
1425 );
1426 editor.move_right(&MoveRight, window, cx);
1427 assert_eq!(
1428 editor.selections.display_ranges(cx),
1429 &[empty_range(0, "🟥🟧".len())]
1430 );
1431 editor.move_right(&MoveRight, window, cx);
1432 assert_eq!(
1433 editor.selections.display_ranges(cx),
1434 &[empty_range(0, "🟥🟧⋯".len())]
1435 );
1436
1437 editor.move_down(&MoveDown, window, cx);
1438 assert_eq!(
1439 editor.selections.display_ranges(cx),
1440 &[empty_range(1, "ab⋯e".len())]
1441 );
1442 editor.move_left(&MoveLeft, window, cx);
1443 assert_eq!(
1444 editor.selections.display_ranges(cx),
1445 &[empty_range(1, "ab⋯".len())]
1446 );
1447 editor.move_left(&MoveLeft, window, cx);
1448 assert_eq!(
1449 editor.selections.display_ranges(cx),
1450 &[empty_range(1, "ab".len())]
1451 );
1452 editor.move_left(&MoveLeft, window, cx);
1453 assert_eq!(
1454 editor.selections.display_ranges(cx),
1455 &[empty_range(1, "a".len())]
1456 );
1457
1458 editor.move_down(&MoveDown, window, cx);
1459 assert_eq!(
1460 editor.selections.display_ranges(cx),
1461 &[empty_range(2, "α".len())]
1462 );
1463 editor.move_right(&MoveRight, window, cx);
1464 assert_eq!(
1465 editor.selections.display_ranges(cx),
1466 &[empty_range(2, "αβ".len())]
1467 );
1468 editor.move_right(&MoveRight, window, cx);
1469 assert_eq!(
1470 editor.selections.display_ranges(cx),
1471 &[empty_range(2, "αβ⋯".len())]
1472 );
1473 editor.move_right(&MoveRight, window, cx);
1474 assert_eq!(
1475 editor.selections.display_ranges(cx),
1476 &[empty_range(2, "αβ⋯ε".len())]
1477 );
1478
1479 editor.move_up(&MoveUp, window, cx);
1480 assert_eq!(
1481 editor.selections.display_ranges(cx),
1482 &[empty_range(1, "ab⋯e".len())]
1483 );
1484 editor.move_down(&MoveDown, window, cx);
1485 assert_eq!(
1486 editor.selections.display_ranges(cx),
1487 &[empty_range(2, "αβ⋯ε".len())]
1488 );
1489 editor.move_up(&MoveUp, window, cx);
1490 assert_eq!(
1491 editor.selections.display_ranges(cx),
1492 &[empty_range(1, "ab⋯e".len())]
1493 );
1494
1495 editor.move_up(&MoveUp, window, cx);
1496 assert_eq!(
1497 editor.selections.display_ranges(cx),
1498 &[empty_range(0, "🟥🟧".len())]
1499 );
1500 editor.move_left(&MoveLeft, window, cx);
1501 assert_eq!(
1502 editor.selections.display_ranges(cx),
1503 &[empty_range(0, "🟥".len())]
1504 );
1505 editor.move_left(&MoveLeft, window, cx);
1506 assert_eq!(
1507 editor.selections.display_ranges(cx),
1508 &[empty_range(0, "".len())]
1509 );
1510 });
1511}
1512
1513#[gpui::test]
1514fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1515 init_test(cx, |_| {});
1516
1517 let editor = cx.add_window(|window, cx| {
1518 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1519 build_editor(buffer, window, cx)
1520 });
1521 _ = editor.update(cx, |editor, window, cx| {
1522 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1523 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1524 });
1525
1526 // moving above start of document should move selection to start of document,
1527 // but the next move down should still be at the original goal_x
1528 editor.move_up(&MoveUp, window, cx);
1529 assert_eq!(
1530 editor.selections.display_ranges(cx),
1531 &[empty_range(0, "".len())]
1532 );
1533
1534 editor.move_down(&MoveDown, window, cx);
1535 assert_eq!(
1536 editor.selections.display_ranges(cx),
1537 &[empty_range(1, "abcd".len())]
1538 );
1539
1540 editor.move_down(&MoveDown, window, cx);
1541 assert_eq!(
1542 editor.selections.display_ranges(cx),
1543 &[empty_range(2, "αβγ".len())]
1544 );
1545
1546 editor.move_down(&MoveDown, window, cx);
1547 assert_eq!(
1548 editor.selections.display_ranges(cx),
1549 &[empty_range(3, "abcd".len())]
1550 );
1551
1552 editor.move_down(&MoveDown, window, cx);
1553 assert_eq!(
1554 editor.selections.display_ranges(cx),
1555 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1556 );
1557
1558 // moving past end of document should not change goal_x
1559 editor.move_down(&MoveDown, window, cx);
1560 assert_eq!(
1561 editor.selections.display_ranges(cx),
1562 &[empty_range(5, "".len())]
1563 );
1564
1565 editor.move_down(&MoveDown, window, cx);
1566 assert_eq!(
1567 editor.selections.display_ranges(cx),
1568 &[empty_range(5, "".len())]
1569 );
1570
1571 editor.move_up(&MoveUp, window, cx);
1572 assert_eq!(
1573 editor.selections.display_ranges(cx),
1574 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1575 );
1576
1577 editor.move_up(&MoveUp, window, cx);
1578 assert_eq!(
1579 editor.selections.display_ranges(cx),
1580 &[empty_range(3, "abcd".len())]
1581 );
1582
1583 editor.move_up(&MoveUp, window, cx);
1584 assert_eq!(
1585 editor.selections.display_ranges(cx),
1586 &[empty_range(2, "αβγ".len())]
1587 );
1588 });
1589}
1590
1591#[gpui::test]
1592fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1593 init_test(cx, |_| {});
1594 let move_to_beg = MoveToBeginningOfLine {
1595 stop_at_soft_wraps: true,
1596 stop_at_indent: true,
1597 };
1598
1599 let delete_to_beg = DeleteToBeginningOfLine {
1600 stop_at_indent: false,
1601 };
1602
1603 let move_to_end = MoveToEndOfLine {
1604 stop_at_soft_wraps: true,
1605 };
1606
1607 let editor = cx.add_window(|window, cx| {
1608 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1609 build_editor(buffer, window, cx)
1610 });
1611 _ = editor.update(cx, |editor, window, cx| {
1612 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1613 s.select_display_ranges([
1614 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1615 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1616 ]);
1617 });
1618 });
1619
1620 _ = editor.update(cx, |editor, window, cx| {
1621 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1622 assert_eq!(
1623 editor.selections.display_ranges(cx),
1624 &[
1625 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1626 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1627 ]
1628 );
1629 });
1630
1631 _ = editor.update(cx, |editor, window, cx| {
1632 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1633 assert_eq!(
1634 editor.selections.display_ranges(cx),
1635 &[
1636 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1637 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1638 ]
1639 );
1640 });
1641
1642 _ = editor.update(cx, |editor, window, cx| {
1643 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1644 assert_eq!(
1645 editor.selections.display_ranges(cx),
1646 &[
1647 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1648 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1649 ]
1650 );
1651 });
1652
1653 _ = editor.update(cx, |editor, window, cx| {
1654 editor.move_to_end_of_line(&move_to_end, window, cx);
1655 assert_eq!(
1656 editor.selections.display_ranges(cx),
1657 &[
1658 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1659 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1660 ]
1661 );
1662 });
1663
1664 // Moving to the end of line again is a no-op.
1665 _ = editor.update(cx, |editor, window, cx| {
1666 editor.move_to_end_of_line(&move_to_end, window, cx);
1667 assert_eq!(
1668 editor.selections.display_ranges(cx),
1669 &[
1670 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1671 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1672 ]
1673 );
1674 });
1675
1676 _ = editor.update(cx, |editor, window, cx| {
1677 editor.move_left(&MoveLeft, window, cx);
1678 editor.select_to_beginning_of_line(
1679 &SelectToBeginningOfLine {
1680 stop_at_soft_wraps: true,
1681 stop_at_indent: true,
1682 },
1683 window,
1684 cx,
1685 );
1686 assert_eq!(
1687 editor.selections.display_ranges(cx),
1688 &[
1689 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1690 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1691 ]
1692 );
1693 });
1694
1695 _ = editor.update(cx, |editor, window, cx| {
1696 editor.select_to_beginning_of_line(
1697 &SelectToBeginningOfLine {
1698 stop_at_soft_wraps: true,
1699 stop_at_indent: true,
1700 },
1701 window,
1702 cx,
1703 );
1704 assert_eq!(
1705 editor.selections.display_ranges(cx),
1706 &[
1707 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1708 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1709 ]
1710 );
1711 });
1712
1713 _ = editor.update(cx, |editor, window, cx| {
1714 editor.select_to_beginning_of_line(
1715 &SelectToBeginningOfLine {
1716 stop_at_soft_wraps: true,
1717 stop_at_indent: true,
1718 },
1719 window,
1720 cx,
1721 );
1722 assert_eq!(
1723 editor.selections.display_ranges(cx),
1724 &[
1725 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1726 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1727 ]
1728 );
1729 });
1730
1731 _ = editor.update(cx, |editor, window, cx| {
1732 editor.select_to_end_of_line(
1733 &SelectToEndOfLine {
1734 stop_at_soft_wraps: true,
1735 },
1736 window,
1737 cx,
1738 );
1739 assert_eq!(
1740 editor.selections.display_ranges(cx),
1741 &[
1742 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1743 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1744 ]
1745 );
1746 });
1747
1748 _ = editor.update(cx, |editor, window, cx| {
1749 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1750 assert_eq!(editor.display_text(cx), "ab\n de");
1751 assert_eq!(
1752 editor.selections.display_ranges(cx),
1753 &[
1754 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1755 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1756 ]
1757 );
1758 });
1759
1760 _ = editor.update(cx, |editor, window, cx| {
1761 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1762 assert_eq!(editor.display_text(cx), "\n");
1763 assert_eq!(
1764 editor.selections.display_ranges(cx),
1765 &[
1766 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1767 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1768 ]
1769 );
1770 });
1771}
1772
1773#[gpui::test]
1774fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1775 init_test(cx, |_| {});
1776 let move_to_beg = MoveToBeginningOfLine {
1777 stop_at_soft_wraps: false,
1778 stop_at_indent: false,
1779 };
1780
1781 let move_to_end = MoveToEndOfLine {
1782 stop_at_soft_wraps: false,
1783 };
1784
1785 let editor = cx.add_window(|window, cx| {
1786 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1787 build_editor(buffer, window, cx)
1788 });
1789
1790 _ = editor.update(cx, |editor, window, cx| {
1791 editor.set_wrap_width(Some(140.0.into()), cx);
1792
1793 // We expect the following lines after wrapping
1794 // ```
1795 // thequickbrownfox
1796 // jumpedoverthelazydo
1797 // gs
1798 // ```
1799 // The final `gs` was soft-wrapped onto a new line.
1800 assert_eq!(
1801 "thequickbrownfox\njumpedoverthelaz\nydogs",
1802 editor.display_text(cx),
1803 );
1804
1805 // First, let's assert behavior on the first line, that was not soft-wrapped.
1806 // Start the cursor at the `k` on the first line
1807 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1808 s.select_display_ranges([
1809 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1810 ]);
1811 });
1812
1813 // Moving to the beginning of the line should put us at the beginning of the line.
1814 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1815 assert_eq!(
1816 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1817 editor.selections.display_ranges(cx)
1818 );
1819
1820 // Moving to the end of the line should put us at the end of the line.
1821 editor.move_to_end_of_line(&move_to_end, window, cx);
1822 assert_eq!(
1823 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1824 editor.selections.display_ranges(cx)
1825 );
1826
1827 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1828 // Start the cursor at the last line (`y` that was wrapped to a new line)
1829 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1830 s.select_display_ranges([
1831 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1832 ]);
1833 });
1834
1835 // Moving to the beginning of the line should put us at the start of the second line of
1836 // display text, i.e., the `j`.
1837 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1838 assert_eq!(
1839 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1840 editor.selections.display_ranges(cx)
1841 );
1842
1843 // Moving to the beginning of the line again should be a no-op.
1844 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1845 assert_eq!(
1846 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1847 editor.selections.display_ranges(cx)
1848 );
1849
1850 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1851 // next display line.
1852 editor.move_to_end_of_line(&move_to_end, window, cx);
1853 assert_eq!(
1854 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1855 editor.selections.display_ranges(cx)
1856 );
1857
1858 // Moving to the end of the line again should be a no-op.
1859 editor.move_to_end_of_line(&move_to_end, window, cx);
1860 assert_eq!(
1861 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1862 editor.selections.display_ranges(cx)
1863 );
1864 });
1865}
1866
1867#[gpui::test]
1868fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1869 init_test(cx, |_| {});
1870
1871 let move_to_beg = MoveToBeginningOfLine {
1872 stop_at_soft_wraps: true,
1873 stop_at_indent: true,
1874 };
1875
1876 let select_to_beg = SelectToBeginningOfLine {
1877 stop_at_soft_wraps: true,
1878 stop_at_indent: true,
1879 };
1880
1881 let delete_to_beg = DeleteToBeginningOfLine {
1882 stop_at_indent: true,
1883 };
1884
1885 let move_to_end = MoveToEndOfLine {
1886 stop_at_soft_wraps: false,
1887 };
1888
1889 let editor = cx.add_window(|window, cx| {
1890 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1891 build_editor(buffer, window, cx)
1892 });
1893
1894 _ = editor.update(cx, |editor, window, cx| {
1895 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1896 s.select_display_ranges([
1897 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1898 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1899 ]);
1900 });
1901
1902 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1903 // and the second cursor at the first non-whitespace character in the line.
1904 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1905 assert_eq!(
1906 editor.selections.display_ranges(cx),
1907 &[
1908 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1909 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1910 ]
1911 );
1912
1913 // Moving to the beginning of the line again should be a no-op for the first cursor,
1914 // and should move the second cursor to the beginning of the line.
1915 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1916 assert_eq!(
1917 editor.selections.display_ranges(cx),
1918 &[
1919 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1920 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1921 ]
1922 );
1923
1924 // Moving to the beginning of the line again should still be a no-op for the first cursor,
1925 // and should move the second cursor back to the first non-whitespace character in the line.
1926 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1927 assert_eq!(
1928 editor.selections.display_ranges(cx),
1929 &[
1930 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1931 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1932 ]
1933 );
1934
1935 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
1936 // and to the first non-whitespace character in the line for the second cursor.
1937 editor.move_to_end_of_line(&move_to_end, window, cx);
1938 editor.move_left(&MoveLeft, window, cx);
1939 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1940 assert_eq!(
1941 editor.selections.display_ranges(cx),
1942 &[
1943 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1944 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1945 ]
1946 );
1947
1948 // Selecting to the beginning of the line again should be a no-op for the first cursor,
1949 // and should select to the beginning of the line for the second cursor.
1950 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1951 assert_eq!(
1952 editor.selections.display_ranges(cx),
1953 &[
1954 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1955 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1956 ]
1957 );
1958
1959 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
1960 // and should delete to the first non-whitespace character in the line for the second cursor.
1961 editor.move_to_end_of_line(&move_to_end, window, cx);
1962 editor.move_left(&MoveLeft, window, cx);
1963 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1964 assert_eq!(editor.text(cx), "c\n f");
1965 });
1966}
1967
1968#[gpui::test]
1969fn test_beginning_of_line_with_cursor_between_line_start_and_indent(cx: &mut TestAppContext) {
1970 init_test(cx, |_| {});
1971
1972 let move_to_beg = MoveToBeginningOfLine {
1973 stop_at_soft_wraps: true,
1974 stop_at_indent: true,
1975 };
1976
1977 let editor = cx.add_window(|window, cx| {
1978 let buffer = MultiBuffer::build_simple(" hello\nworld", cx);
1979 build_editor(buffer, window, cx)
1980 });
1981
1982 _ = editor.update(cx, |editor, window, cx| {
1983 // test cursor between line_start and indent_start
1984 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1985 s.select_display_ranges([
1986 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3)
1987 ]);
1988 });
1989
1990 // cursor should move to line_start
1991 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1992 assert_eq!(
1993 editor.selections.display_ranges(cx),
1994 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1995 );
1996
1997 // cursor should move to indent_start
1998 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1999 assert_eq!(
2000 editor.selections.display_ranges(cx),
2001 &[DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 4)]
2002 );
2003
2004 // cursor should move to back to line_start
2005 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2006 assert_eq!(
2007 editor.selections.display_ranges(cx),
2008 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
2009 );
2010 });
2011}
2012
2013#[gpui::test]
2014fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
2015 init_test(cx, |_| {});
2016
2017 let editor = cx.add_window(|window, cx| {
2018 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
2019 build_editor(buffer, window, cx)
2020 });
2021 _ = editor.update(cx, |editor, window, cx| {
2022 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2023 s.select_display_ranges([
2024 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
2025 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
2026 ])
2027 });
2028 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2029 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
2030
2031 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2032 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
2033
2034 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2035 assert_selection_ranges("use ˇstd::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
2036
2037 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2038 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
2039
2040 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2041 assert_selection_ranges("ˇuse std::str::{foo, ˇbar}\n\n {baz.qux()}", editor, cx);
2042
2043 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2044 assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
2045
2046 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2047 assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
2048
2049 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2050 assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
2051
2052 editor.move_right(&MoveRight, window, cx);
2053 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2054 assert_selection_ranges(
2055 "use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}",
2056 editor,
2057 cx,
2058 );
2059
2060 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2061 assert_selection_ranges(
2062 "use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
2063 editor,
2064 cx,
2065 );
2066
2067 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
2068 assert_selection_ranges(
2069 "use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
2070 editor,
2071 cx,
2072 );
2073 });
2074}
2075
2076#[gpui::test]
2077fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
2078 init_test(cx, |_| {});
2079
2080 let editor = cx.add_window(|window, cx| {
2081 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
2082 build_editor(buffer, window, cx)
2083 });
2084
2085 _ = editor.update(cx, |editor, window, cx| {
2086 editor.set_wrap_width(Some(140.0.into()), cx);
2087 assert_eq!(
2088 editor.display_text(cx),
2089 "use one::{\n two::three::\n four::five\n};"
2090 );
2091
2092 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2093 s.select_display_ranges([
2094 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
2095 ]);
2096 });
2097
2098 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2099 assert_eq!(
2100 editor.selections.display_ranges(cx),
2101 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
2102 );
2103
2104 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2105 assert_eq!(
2106 editor.selections.display_ranges(cx),
2107 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2108 );
2109
2110 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2111 assert_eq!(
2112 editor.selections.display_ranges(cx),
2113 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2114 );
2115
2116 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2117 assert_eq!(
2118 editor.selections.display_ranges(cx),
2119 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2120 );
2121
2122 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2123 assert_eq!(
2124 editor.selections.display_ranges(cx),
2125 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2126 );
2127
2128 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2129 assert_eq!(
2130 editor.selections.display_ranges(cx),
2131 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2132 );
2133 });
2134}
2135
2136#[gpui::test]
2137async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2138 init_test(cx, |_| {});
2139 let mut cx = EditorTestContext::new(cx).await;
2140
2141 let line_height = cx.editor(|editor, window, _| {
2142 editor
2143 .style()
2144 .unwrap()
2145 .text
2146 .line_height_in_pixels(window.rem_size())
2147 });
2148 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2149
2150 cx.set_state(
2151 &r#"ˇone
2152 two
2153
2154 three
2155 fourˇ
2156 five
2157
2158 six"#
2159 .unindent(),
2160 );
2161
2162 cx.update_editor(|editor, window, cx| {
2163 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2164 });
2165 cx.assert_editor_state(
2166 &r#"one
2167 two
2168 ˇ
2169 three
2170 four
2171 five
2172 ˇ
2173 six"#
2174 .unindent(),
2175 );
2176
2177 cx.update_editor(|editor, window, cx| {
2178 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2179 });
2180 cx.assert_editor_state(
2181 &r#"one
2182 two
2183
2184 three
2185 four
2186 five
2187 ˇ
2188 sixˇ"#
2189 .unindent(),
2190 );
2191
2192 cx.update_editor(|editor, window, cx| {
2193 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2194 });
2195 cx.assert_editor_state(
2196 &r#"one
2197 two
2198
2199 three
2200 four
2201 five
2202
2203 sixˇ"#
2204 .unindent(),
2205 );
2206
2207 cx.update_editor(|editor, window, cx| {
2208 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2209 });
2210 cx.assert_editor_state(
2211 &r#"one
2212 two
2213
2214 three
2215 four
2216 five
2217 ˇ
2218 six"#
2219 .unindent(),
2220 );
2221
2222 cx.update_editor(|editor, window, cx| {
2223 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2224 });
2225 cx.assert_editor_state(
2226 &r#"one
2227 two
2228 ˇ
2229 three
2230 four
2231 five
2232
2233 six"#
2234 .unindent(),
2235 );
2236
2237 cx.update_editor(|editor, window, cx| {
2238 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2239 });
2240 cx.assert_editor_state(
2241 &r#"ˇone
2242 two
2243
2244 three
2245 four
2246 five
2247
2248 six"#
2249 .unindent(),
2250 );
2251}
2252
2253#[gpui::test]
2254async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2255 init_test(cx, |_| {});
2256 let mut cx = EditorTestContext::new(cx).await;
2257 let line_height = cx.editor(|editor, window, _| {
2258 editor
2259 .style()
2260 .unwrap()
2261 .text
2262 .line_height_in_pixels(window.rem_size())
2263 });
2264 let window = cx.window;
2265 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2266
2267 cx.set_state(
2268 r#"ˇone
2269 two
2270 three
2271 four
2272 five
2273 six
2274 seven
2275 eight
2276 nine
2277 ten
2278 "#,
2279 );
2280
2281 cx.update_editor(|editor, window, cx| {
2282 assert_eq!(
2283 editor.snapshot(window, cx).scroll_position(),
2284 gpui::Point::new(0., 0.)
2285 );
2286 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2287 assert_eq!(
2288 editor.snapshot(window, cx).scroll_position(),
2289 gpui::Point::new(0., 3.)
2290 );
2291 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2292 assert_eq!(
2293 editor.snapshot(window, cx).scroll_position(),
2294 gpui::Point::new(0., 6.)
2295 );
2296 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2297 assert_eq!(
2298 editor.snapshot(window, cx).scroll_position(),
2299 gpui::Point::new(0., 3.)
2300 );
2301
2302 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2303 assert_eq!(
2304 editor.snapshot(window, cx).scroll_position(),
2305 gpui::Point::new(0., 1.)
2306 );
2307 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2308 assert_eq!(
2309 editor.snapshot(window, cx).scroll_position(),
2310 gpui::Point::new(0., 3.)
2311 );
2312 });
2313}
2314
2315#[gpui::test]
2316async fn test_autoscroll(cx: &mut TestAppContext) {
2317 init_test(cx, |_| {});
2318 let mut cx = EditorTestContext::new(cx).await;
2319
2320 let line_height = cx.update_editor(|editor, window, cx| {
2321 editor.set_vertical_scroll_margin(2, cx);
2322 editor
2323 .style()
2324 .unwrap()
2325 .text
2326 .line_height_in_pixels(window.rem_size())
2327 });
2328 let window = cx.window;
2329 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2330
2331 cx.set_state(
2332 r#"ˇone
2333 two
2334 three
2335 four
2336 five
2337 six
2338 seven
2339 eight
2340 nine
2341 ten
2342 "#,
2343 );
2344 cx.update_editor(|editor, window, cx| {
2345 assert_eq!(
2346 editor.snapshot(window, cx).scroll_position(),
2347 gpui::Point::new(0., 0.0)
2348 );
2349 });
2350
2351 // Add a cursor below the visible area. Since both cursors cannot fit
2352 // on screen, the editor autoscrolls to reveal the newest cursor, and
2353 // allows the vertical scroll margin below that cursor.
2354 cx.update_editor(|editor, window, cx| {
2355 editor.change_selections(Default::default(), window, cx, |selections| {
2356 selections.select_ranges([
2357 Point::new(0, 0)..Point::new(0, 0),
2358 Point::new(6, 0)..Point::new(6, 0),
2359 ]);
2360 })
2361 });
2362 cx.update_editor(|editor, window, cx| {
2363 assert_eq!(
2364 editor.snapshot(window, cx).scroll_position(),
2365 gpui::Point::new(0., 3.0)
2366 );
2367 });
2368
2369 // Move down. The editor cursor scrolls down to track the newest cursor.
2370 cx.update_editor(|editor, window, cx| {
2371 editor.move_down(&Default::default(), window, cx);
2372 });
2373 cx.update_editor(|editor, window, cx| {
2374 assert_eq!(
2375 editor.snapshot(window, cx).scroll_position(),
2376 gpui::Point::new(0., 4.0)
2377 );
2378 });
2379
2380 // Add a cursor above the visible area. Since both cursors fit on screen,
2381 // the editor scrolls to show both.
2382 cx.update_editor(|editor, window, cx| {
2383 editor.change_selections(Default::default(), window, cx, |selections| {
2384 selections.select_ranges([
2385 Point::new(1, 0)..Point::new(1, 0),
2386 Point::new(6, 0)..Point::new(6, 0),
2387 ]);
2388 })
2389 });
2390 cx.update_editor(|editor, window, cx| {
2391 assert_eq!(
2392 editor.snapshot(window, cx).scroll_position(),
2393 gpui::Point::new(0., 1.0)
2394 );
2395 });
2396}
2397
2398#[gpui::test]
2399async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2400 init_test(cx, |_| {});
2401 let mut cx = EditorTestContext::new(cx).await;
2402
2403 let line_height = cx.editor(|editor, window, _cx| {
2404 editor
2405 .style()
2406 .unwrap()
2407 .text
2408 .line_height_in_pixels(window.rem_size())
2409 });
2410 let window = cx.window;
2411 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2412 cx.set_state(
2413 &r#"
2414 ˇone
2415 two
2416 threeˇ
2417 four
2418 five
2419 six
2420 seven
2421 eight
2422 nine
2423 ten
2424 "#
2425 .unindent(),
2426 );
2427
2428 cx.update_editor(|editor, window, cx| {
2429 editor.move_page_down(&MovePageDown::default(), window, cx)
2430 });
2431 cx.assert_editor_state(
2432 &r#"
2433 one
2434 two
2435 three
2436 ˇfour
2437 five
2438 sixˇ
2439 seven
2440 eight
2441 nine
2442 ten
2443 "#
2444 .unindent(),
2445 );
2446
2447 cx.update_editor(|editor, window, cx| {
2448 editor.move_page_down(&MovePageDown::default(), window, cx)
2449 });
2450 cx.assert_editor_state(
2451 &r#"
2452 one
2453 two
2454 three
2455 four
2456 five
2457 six
2458 ˇseven
2459 eight
2460 nineˇ
2461 ten
2462 "#
2463 .unindent(),
2464 );
2465
2466 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2467 cx.assert_editor_state(
2468 &r#"
2469 one
2470 two
2471 three
2472 ˇfour
2473 five
2474 sixˇ
2475 seven
2476 eight
2477 nine
2478 ten
2479 "#
2480 .unindent(),
2481 );
2482
2483 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2484 cx.assert_editor_state(
2485 &r#"
2486 ˇone
2487 two
2488 threeˇ
2489 four
2490 five
2491 six
2492 seven
2493 eight
2494 nine
2495 ten
2496 "#
2497 .unindent(),
2498 );
2499
2500 // Test select collapsing
2501 cx.update_editor(|editor, window, cx| {
2502 editor.move_page_down(&MovePageDown::default(), window, cx);
2503 editor.move_page_down(&MovePageDown::default(), window, cx);
2504 editor.move_page_down(&MovePageDown::default(), window, cx);
2505 });
2506 cx.assert_editor_state(
2507 &r#"
2508 one
2509 two
2510 three
2511 four
2512 five
2513 six
2514 seven
2515 eight
2516 nine
2517 ˇten
2518 ˇ"#
2519 .unindent(),
2520 );
2521}
2522
2523#[gpui::test]
2524async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2525 init_test(cx, |_| {});
2526 let mut cx = EditorTestContext::new(cx).await;
2527 cx.set_state("one «two threeˇ» four");
2528 cx.update_editor(|editor, window, cx| {
2529 editor.delete_to_beginning_of_line(
2530 &DeleteToBeginningOfLine {
2531 stop_at_indent: false,
2532 },
2533 window,
2534 cx,
2535 );
2536 assert_eq!(editor.text(cx), " four");
2537 });
2538}
2539
2540#[gpui::test]
2541async fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2542 init_test(cx, |_| {});
2543
2544 let mut cx = EditorTestContext::new(cx).await;
2545
2546 // For an empty selection, the preceding word fragment is deleted.
2547 // For non-empty selections, only selected characters are deleted.
2548 cx.set_state("onˇe two t«hreˇ»e four");
2549 cx.update_editor(|editor, window, cx| {
2550 editor.delete_to_previous_word_start(
2551 &DeleteToPreviousWordStart {
2552 ignore_newlines: false,
2553 ignore_brackets: false,
2554 },
2555 window,
2556 cx,
2557 );
2558 });
2559 cx.assert_editor_state("ˇe two tˇe four");
2560
2561 cx.set_state("e tˇwo te «fˇ»our");
2562 cx.update_editor(|editor, window, cx| {
2563 editor.delete_to_next_word_end(
2564 &DeleteToNextWordEnd {
2565 ignore_newlines: false,
2566 ignore_brackets: false,
2567 },
2568 window,
2569 cx,
2570 );
2571 });
2572 cx.assert_editor_state("e tˇ te ˇour");
2573}
2574
2575#[gpui::test]
2576async fn test_delete_whitespaces(cx: &mut TestAppContext) {
2577 init_test(cx, |_| {});
2578
2579 let mut cx = EditorTestContext::new(cx).await;
2580
2581 cx.set_state("here is some text ˇwith a space");
2582 cx.update_editor(|editor, window, cx| {
2583 editor.delete_to_previous_word_start(
2584 &DeleteToPreviousWordStart {
2585 ignore_newlines: false,
2586 ignore_brackets: true,
2587 },
2588 window,
2589 cx,
2590 );
2591 });
2592 // Continuous whitespace sequences are removed entirely, words behind them are not affected by the deletion action.
2593 cx.assert_editor_state("here is some textˇwith a space");
2594
2595 cx.set_state("here is some text ˇwith a space");
2596 cx.update_editor(|editor, window, cx| {
2597 editor.delete_to_previous_word_start(
2598 &DeleteToPreviousWordStart {
2599 ignore_newlines: false,
2600 ignore_brackets: false,
2601 },
2602 window,
2603 cx,
2604 );
2605 });
2606 cx.assert_editor_state("here is some textˇwith a space");
2607
2608 cx.set_state("here is some textˇ with a space");
2609 cx.update_editor(|editor, window, cx| {
2610 editor.delete_to_next_word_end(
2611 &DeleteToNextWordEnd {
2612 ignore_newlines: false,
2613 ignore_brackets: true,
2614 },
2615 window,
2616 cx,
2617 );
2618 });
2619 // Same happens in the other direction.
2620 cx.assert_editor_state("here is some textˇwith a space");
2621
2622 cx.set_state("here is some textˇ with a space");
2623 cx.update_editor(|editor, window, cx| {
2624 editor.delete_to_next_word_end(
2625 &DeleteToNextWordEnd {
2626 ignore_newlines: false,
2627 ignore_brackets: false,
2628 },
2629 window,
2630 cx,
2631 );
2632 });
2633 cx.assert_editor_state("here is some textˇwith a space");
2634
2635 cx.set_state("here is some textˇ with a space");
2636 cx.update_editor(|editor, window, cx| {
2637 editor.delete_to_next_word_end(
2638 &DeleteToNextWordEnd {
2639 ignore_newlines: true,
2640 ignore_brackets: false,
2641 },
2642 window,
2643 cx,
2644 );
2645 });
2646 cx.assert_editor_state("here is some textˇwith a space");
2647 cx.update_editor(|editor, window, cx| {
2648 editor.delete_to_previous_word_start(
2649 &DeleteToPreviousWordStart {
2650 ignore_newlines: true,
2651 ignore_brackets: false,
2652 },
2653 window,
2654 cx,
2655 );
2656 });
2657 cx.assert_editor_state("here is some ˇwith a space");
2658 cx.update_editor(|editor, window, cx| {
2659 editor.delete_to_previous_word_start(
2660 &DeleteToPreviousWordStart {
2661 ignore_newlines: true,
2662 ignore_brackets: false,
2663 },
2664 window,
2665 cx,
2666 );
2667 });
2668 // Single whitespaces are removed with the word behind them.
2669 cx.assert_editor_state("here is ˇwith a space");
2670 cx.update_editor(|editor, window, cx| {
2671 editor.delete_to_previous_word_start(
2672 &DeleteToPreviousWordStart {
2673 ignore_newlines: true,
2674 ignore_brackets: false,
2675 },
2676 window,
2677 cx,
2678 );
2679 });
2680 cx.assert_editor_state("here ˇwith a space");
2681 cx.update_editor(|editor, window, cx| {
2682 editor.delete_to_previous_word_start(
2683 &DeleteToPreviousWordStart {
2684 ignore_newlines: true,
2685 ignore_brackets: false,
2686 },
2687 window,
2688 cx,
2689 );
2690 });
2691 cx.assert_editor_state("ˇwith a space");
2692 cx.update_editor(|editor, window, cx| {
2693 editor.delete_to_previous_word_start(
2694 &DeleteToPreviousWordStart {
2695 ignore_newlines: true,
2696 ignore_brackets: false,
2697 },
2698 window,
2699 cx,
2700 );
2701 });
2702 cx.assert_editor_state("ˇwith a space");
2703 cx.update_editor(|editor, window, cx| {
2704 editor.delete_to_next_word_end(
2705 &DeleteToNextWordEnd {
2706 ignore_newlines: true,
2707 ignore_brackets: false,
2708 },
2709 window,
2710 cx,
2711 );
2712 });
2713 // Same happens in the other direction.
2714 cx.assert_editor_state("ˇ a space");
2715 cx.update_editor(|editor, window, cx| {
2716 editor.delete_to_next_word_end(
2717 &DeleteToNextWordEnd {
2718 ignore_newlines: true,
2719 ignore_brackets: false,
2720 },
2721 window,
2722 cx,
2723 );
2724 });
2725 cx.assert_editor_state("ˇ space");
2726 cx.update_editor(|editor, window, cx| {
2727 editor.delete_to_next_word_end(
2728 &DeleteToNextWordEnd {
2729 ignore_newlines: true,
2730 ignore_brackets: false,
2731 },
2732 window,
2733 cx,
2734 );
2735 });
2736 cx.assert_editor_state("ˇ");
2737 cx.update_editor(|editor, window, cx| {
2738 editor.delete_to_next_word_end(
2739 &DeleteToNextWordEnd {
2740 ignore_newlines: true,
2741 ignore_brackets: false,
2742 },
2743 window,
2744 cx,
2745 );
2746 });
2747 cx.assert_editor_state("ˇ");
2748 cx.update_editor(|editor, window, cx| {
2749 editor.delete_to_previous_word_start(
2750 &DeleteToPreviousWordStart {
2751 ignore_newlines: true,
2752 ignore_brackets: false,
2753 },
2754 window,
2755 cx,
2756 );
2757 });
2758 cx.assert_editor_state("ˇ");
2759}
2760
2761#[gpui::test]
2762async fn test_delete_to_bracket(cx: &mut TestAppContext) {
2763 init_test(cx, |_| {});
2764
2765 let language = Arc::new(
2766 Language::new(
2767 LanguageConfig {
2768 brackets: BracketPairConfig {
2769 pairs: vec![
2770 BracketPair {
2771 start: "\"".to_string(),
2772 end: "\"".to_string(),
2773 close: true,
2774 surround: true,
2775 newline: false,
2776 },
2777 BracketPair {
2778 start: "(".to_string(),
2779 end: ")".to_string(),
2780 close: true,
2781 surround: true,
2782 newline: true,
2783 },
2784 ],
2785 ..BracketPairConfig::default()
2786 },
2787 ..LanguageConfig::default()
2788 },
2789 Some(tree_sitter_rust::LANGUAGE.into()),
2790 )
2791 .with_brackets_query(
2792 r#"
2793 ("(" @open ")" @close)
2794 ("\"" @open "\"" @close)
2795 "#,
2796 )
2797 .unwrap(),
2798 );
2799
2800 let mut cx = EditorTestContext::new(cx).await;
2801 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2802
2803 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2804 cx.update_editor(|editor, window, cx| {
2805 editor.delete_to_previous_word_start(
2806 &DeleteToPreviousWordStart {
2807 ignore_newlines: true,
2808 ignore_brackets: false,
2809 },
2810 window,
2811 cx,
2812 );
2813 });
2814 // Deletion stops before brackets if asked to not ignore them.
2815 cx.assert_editor_state(r#"macro!("ˇCOMMENT");"#);
2816 cx.update_editor(|editor, window, cx| {
2817 editor.delete_to_previous_word_start(
2818 &DeleteToPreviousWordStart {
2819 ignore_newlines: true,
2820 ignore_brackets: false,
2821 },
2822 window,
2823 cx,
2824 );
2825 });
2826 // Deletion has to remove a single bracket and then stop again.
2827 cx.assert_editor_state(r#"macro!(ˇCOMMENT");"#);
2828
2829 cx.update_editor(|editor, window, cx| {
2830 editor.delete_to_previous_word_start(
2831 &DeleteToPreviousWordStart {
2832 ignore_newlines: true,
2833 ignore_brackets: false,
2834 },
2835 window,
2836 cx,
2837 );
2838 });
2839 cx.assert_editor_state(r#"macro!ˇCOMMENT");"#);
2840
2841 cx.update_editor(|editor, window, cx| {
2842 editor.delete_to_previous_word_start(
2843 &DeleteToPreviousWordStart {
2844 ignore_newlines: true,
2845 ignore_brackets: false,
2846 },
2847 window,
2848 cx,
2849 );
2850 });
2851 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2852
2853 cx.update_editor(|editor, window, cx| {
2854 editor.delete_to_previous_word_start(
2855 &DeleteToPreviousWordStart {
2856 ignore_newlines: true,
2857 ignore_brackets: false,
2858 },
2859 window,
2860 cx,
2861 );
2862 });
2863 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2864
2865 cx.update_editor(|editor, window, cx| {
2866 editor.delete_to_next_word_end(
2867 &DeleteToNextWordEnd {
2868 ignore_newlines: true,
2869 ignore_brackets: false,
2870 },
2871 window,
2872 cx,
2873 );
2874 });
2875 // Brackets on the right are not paired anymore, hence deletion does not stop at them
2876 cx.assert_editor_state(r#"ˇ");"#);
2877
2878 cx.update_editor(|editor, window, cx| {
2879 editor.delete_to_next_word_end(
2880 &DeleteToNextWordEnd {
2881 ignore_newlines: true,
2882 ignore_brackets: false,
2883 },
2884 window,
2885 cx,
2886 );
2887 });
2888 cx.assert_editor_state(r#"ˇ"#);
2889
2890 cx.update_editor(|editor, window, cx| {
2891 editor.delete_to_next_word_end(
2892 &DeleteToNextWordEnd {
2893 ignore_newlines: true,
2894 ignore_brackets: false,
2895 },
2896 window,
2897 cx,
2898 );
2899 });
2900 cx.assert_editor_state(r#"ˇ"#);
2901
2902 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2903 cx.update_editor(|editor, window, cx| {
2904 editor.delete_to_previous_word_start(
2905 &DeleteToPreviousWordStart {
2906 ignore_newlines: true,
2907 ignore_brackets: true,
2908 },
2909 window,
2910 cx,
2911 );
2912 });
2913 cx.assert_editor_state(r#"macroˇCOMMENT");"#);
2914}
2915
2916#[gpui::test]
2917fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2918 init_test(cx, |_| {});
2919
2920 let editor = cx.add_window(|window, cx| {
2921 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2922 build_editor(buffer, window, cx)
2923 });
2924 let del_to_prev_word_start = DeleteToPreviousWordStart {
2925 ignore_newlines: false,
2926 ignore_brackets: false,
2927 };
2928 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2929 ignore_newlines: true,
2930 ignore_brackets: false,
2931 };
2932
2933 _ = editor.update(cx, |editor, window, cx| {
2934 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2935 s.select_display_ranges([
2936 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2937 ])
2938 });
2939 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2940 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2941 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2942 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2943 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2944 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2945 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2946 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
2947 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2948 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
2949 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2950 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2951 });
2952}
2953
2954#[gpui::test]
2955fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2956 init_test(cx, |_| {});
2957
2958 let editor = cx.add_window(|window, cx| {
2959 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2960 build_editor(buffer, window, cx)
2961 });
2962 let del_to_next_word_end = DeleteToNextWordEnd {
2963 ignore_newlines: false,
2964 ignore_brackets: false,
2965 };
2966 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2967 ignore_newlines: true,
2968 ignore_brackets: false,
2969 };
2970
2971 _ = editor.update(cx, |editor, window, cx| {
2972 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2973 s.select_display_ranges([
2974 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2975 ])
2976 });
2977 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2978 assert_eq!(
2979 editor.buffer.read(cx).read(cx).text(),
2980 "one\n two\nthree\n four"
2981 );
2982 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2983 assert_eq!(
2984 editor.buffer.read(cx).read(cx).text(),
2985 "\n two\nthree\n four"
2986 );
2987 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2988 assert_eq!(
2989 editor.buffer.read(cx).read(cx).text(),
2990 "two\nthree\n four"
2991 );
2992 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2993 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2994 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2995 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
2996 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2997 assert_eq!(editor.buffer.read(cx).read(cx).text(), "four");
2998 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2999 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3000 });
3001}
3002
3003#[gpui::test]
3004fn test_newline(cx: &mut TestAppContext) {
3005 init_test(cx, |_| {});
3006
3007 let editor = cx.add_window(|window, cx| {
3008 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
3009 build_editor(buffer, window, cx)
3010 });
3011
3012 _ = editor.update(cx, |editor, window, cx| {
3013 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3014 s.select_display_ranges([
3015 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
3016 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
3017 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
3018 ])
3019 });
3020
3021 editor.newline(&Newline, window, cx);
3022 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
3023 });
3024}
3025
3026#[gpui::test]
3027fn test_newline_with_old_selections(cx: &mut TestAppContext) {
3028 init_test(cx, |_| {});
3029
3030 let editor = cx.add_window(|window, cx| {
3031 let buffer = MultiBuffer::build_simple(
3032 "
3033 a
3034 b(
3035 X
3036 )
3037 c(
3038 X
3039 )
3040 "
3041 .unindent()
3042 .as_str(),
3043 cx,
3044 );
3045 let mut editor = build_editor(buffer, window, cx);
3046 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3047 s.select_ranges([
3048 Point::new(2, 4)..Point::new(2, 5),
3049 Point::new(5, 4)..Point::new(5, 5),
3050 ])
3051 });
3052 editor
3053 });
3054
3055 _ = editor.update(cx, |editor, window, cx| {
3056 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3057 editor.buffer.update(cx, |buffer, cx| {
3058 buffer.edit(
3059 [
3060 (Point::new(1, 2)..Point::new(3, 0), ""),
3061 (Point::new(4, 2)..Point::new(6, 0), ""),
3062 ],
3063 None,
3064 cx,
3065 );
3066 assert_eq!(
3067 buffer.read(cx).text(),
3068 "
3069 a
3070 b()
3071 c()
3072 "
3073 .unindent()
3074 );
3075 });
3076 assert_eq!(
3077 editor.selections.ranges(cx),
3078 &[
3079 Point::new(1, 2)..Point::new(1, 2),
3080 Point::new(2, 2)..Point::new(2, 2),
3081 ],
3082 );
3083
3084 editor.newline(&Newline, window, cx);
3085 assert_eq!(
3086 editor.text(cx),
3087 "
3088 a
3089 b(
3090 )
3091 c(
3092 )
3093 "
3094 .unindent()
3095 );
3096
3097 // The selections are moved after the inserted newlines
3098 assert_eq!(
3099 editor.selections.ranges(cx),
3100 &[
3101 Point::new(2, 0)..Point::new(2, 0),
3102 Point::new(4, 0)..Point::new(4, 0),
3103 ],
3104 );
3105 });
3106}
3107
3108#[gpui::test]
3109async fn test_newline_above(cx: &mut TestAppContext) {
3110 init_test(cx, |settings| {
3111 settings.defaults.tab_size = NonZeroU32::new(4)
3112 });
3113
3114 let language = Arc::new(
3115 Language::new(
3116 LanguageConfig::default(),
3117 Some(tree_sitter_rust::LANGUAGE.into()),
3118 )
3119 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3120 .unwrap(),
3121 );
3122
3123 let mut cx = EditorTestContext::new(cx).await;
3124 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3125 cx.set_state(indoc! {"
3126 const a: ˇA = (
3127 (ˇ
3128 «const_functionˇ»(ˇ),
3129 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3130 )ˇ
3131 ˇ);ˇ
3132 "});
3133
3134 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
3135 cx.assert_editor_state(indoc! {"
3136 ˇ
3137 const a: A = (
3138 ˇ
3139 (
3140 ˇ
3141 ˇ
3142 const_function(),
3143 ˇ
3144 ˇ
3145 ˇ
3146 ˇ
3147 something_else,
3148 ˇ
3149 )
3150 ˇ
3151 ˇ
3152 );
3153 "});
3154}
3155
3156#[gpui::test]
3157async fn test_newline_below(cx: &mut TestAppContext) {
3158 init_test(cx, |settings| {
3159 settings.defaults.tab_size = NonZeroU32::new(4)
3160 });
3161
3162 let language = Arc::new(
3163 Language::new(
3164 LanguageConfig::default(),
3165 Some(tree_sitter_rust::LANGUAGE.into()),
3166 )
3167 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3168 .unwrap(),
3169 );
3170
3171 let mut cx = EditorTestContext::new(cx).await;
3172 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3173 cx.set_state(indoc! {"
3174 const a: ˇA = (
3175 (ˇ
3176 «const_functionˇ»(ˇ),
3177 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3178 )ˇ
3179 ˇ);ˇ
3180 "});
3181
3182 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
3183 cx.assert_editor_state(indoc! {"
3184 const a: A = (
3185 ˇ
3186 (
3187 ˇ
3188 const_function(),
3189 ˇ
3190 ˇ
3191 something_else,
3192 ˇ
3193 ˇ
3194 ˇ
3195 ˇ
3196 )
3197 ˇ
3198 );
3199 ˇ
3200 ˇ
3201 "});
3202}
3203
3204#[gpui::test]
3205async fn test_newline_comments(cx: &mut TestAppContext) {
3206 init_test(cx, |settings| {
3207 settings.defaults.tab_size = NonZeroU32::new(4)
3208 });
3209
3210 let language = Arc::new(Language::new(
3211 LanguageConfig {
3212 line_comments: vec!["// ".into()],
3213 ..LanguageConfig::default()
3214 },
3215 None,
3216 ));
3217 {
3218 let mut cx = EditorTestContext::new(cx).await;
3219 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3220 cx.set_state(indoc! {"
3221 // Fooˇ
3222 "});
3223
3224 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3225 cx.assert_editor_state(indoc! {"
3226 // Foo
3227 // ˇ
3228 "});
3229 // Ensure that we add comment prefix when existing line contains space
3230 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3231 cx.assert_editor_state(
3232 indoc! {"
3233 // Foo
3234 //s
3235 // ˇ
3236 "}
3237 .replace("s", " ") // s is used as space placeholder to prevent format on save
3238 .as_str(),
3239 );
3240 // Ensure that we add comment prefix when existing line does not contain space
3241 cx.set_state(indoc! {"
3242 // Foo
3243 //ˇ
3244 "});
3245 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3246 cx.assert_editor_state(indoc! {"
3247 // Foo
3248 //
3249 // ˇ
3250 "});
3251 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
3252 cx.set_state(indoc! {"
3253 ˇ// Foo
3254 "});
3255 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3256 cx.assert_editor_state(indoc! {"
3257
3258 ˇ// Foo
3259 "});
3260 }
3261 // Ensure that comment continuations can be disabled.
3262 update_test_language_settings(cx, |settings| {
3263 settings.defaults.extend_comment_on_newline = Some(false);
3264 });
3265 let mut cx = EditorTestContext::new(cx).await;
3266 cx.set_state(indoc! {"
3267 // Fooˇ
3268 "});
3269 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3270 cx.assert_editor_state(indoc! {"
3271 // Foo
3272 ˇ
3273 "});
3274}
3275
3276#[gpui::test]
3277async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
3278 init_test(cx, |settings| {
3279 settings.defaults.tab_size = NonZeroU32::new(4)
3280 });
3281
3282 let language = Arc::new(Language::new(
3283 LanguageConfig {
3284 line_comments: vec!["// ".into(), "/// ".into()],
3285 ..LanguageConfig::default()
3286 },
3287 None,
3288 ));
3289 {
3290 let mut cx = EditorTestContext::new(cx).await;
3291 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3292 cx.set_state(indoc! {"
3293 //ˇ
3294 "});
3295 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3296 cx.assert_editor_state(indoc! {"
3297 //
3298 // ˇ
3299 "});
3300
3301 cx.set_state(indoc! {"
3302 ///ˇ
3303 "});
3304 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3305 cx.assert_editor_state(indoc! {"
3306 ///
3307 /// ˇ
3308 "});
3309 }
3310}
3311
3312#[gpui::test]
3313async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
3314 init_test(cx, |settings| {
3315 settings.defaults.tab_size = NonZeroU32::new(4)
3316 });
3317
3318 let language = Arc::new(
3319 Language::new(
3320 LanguageConfig {
3321 documentation_comment: Some(language::BlockCommentConfig {
3322 start: "/**".into(),
3323 end: "*/".into(),
3324 prefix: "* ".into(),
3325 tab_size: 1,
3326 }),
3327
3328 ..LanguageConfig::default()
3329 },
3330 Some(tree_sitter_rust::LANGUAGE.into()),
3331 )
3332 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
3333 .unwrap(),
3334 );
3335
3336 {
3337 let mut cx = EditorTestContext::new(cx).await;
3338 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3339 cx.set_state(indoc! {"
3340 /**ˇ
3341 "});
3342
3343 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3344 cx.assert_editor_state(indoc! {"
3345 /**
3346 * ˇ
3347 "});
3348 // Ensure that if cursor is before the comment start,
3349 // we do not actually insert a comment prefix.
3350 cx.set_state(indoc! {"
3351 ˇ/**
3352 "});
3353 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3354 cx.assert_editor_state(indoc! {"
3355
3356 ˇ/**
3357 "});
3358 // Ensure that if cursor is between it doesn't add comment prefix.
3359 cx.set_state(indoc! {"
3360 /*ˇ*
3361 "});
3362 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3363 cx.assert_editor_state(indoc! {"
3364 /*
3365 ˇ*
3366 "});
3367 // Ensure that if suffix exists on same line after cursor it adds new line.
3368 cx.set_state(indoc! {"
3369 /**ˇ*/
3370 "});
3371 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3372 cx.assert_editor_state(indoc! {"
3373 /**
3374 * ˇ
3375 */
3376 "});
3377 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3378 cx.set_state(indoc! {"
3379 /**ˇ */
3380 "});
3381 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3382 cx.assert_editor_state(indoc! {"
3383 /**
3384 * ˇ
3385 */
3386 "});
3387 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3388 cx.set_state(indoc! {"
3389 /** ˇ*/
3390 "});
3391 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3392 cx.assert_editor_state(
3393 indoc! {"
3394 /**s
3395 * ˇ
3396 */
3397 "}
3398 .replace("s", " ") // s is used as space placeholder to prevent format on save
3399 .as_str(),
3400 );
3401 // Ensure that delimiter space is preserved when newline on already
3402 // spaced delimiter.
3403 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3404 cx.assert_editor_state(
3405 indoc! {"
3406 /**s
3407 *s
3408 * ˇ
3409 */
3410 "}
3411 .replace("s", " ") // s is used as space placeholder to prevent format on save
3412 .as_str(),
3413 );
3414 // Ensure that delimiter space is preserved when space is not
3415 // on existing delimiter.
3416 cx.set_state(indoc! {"
3417 /**
3418 *ˇ
3419 */
3420 "});
3421 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3422 cx.assert_editor_state(indoc! {"
3423 /**
3424 *
3425 * ˇ
3426 */
3427 "});
3428 // Ensure that if suffix exists on same line after cursor it
3429 // doesn't add extra new line if prefix is not on same line.
3430 cx.set_state(indoc! {"
3431 /**
3432 ˇ*/
3433 "});
3434 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3435 cx.assert_editor_state(indoc! {"
3436 /**
3437
3438 ˇ*/
3439 "});
3440 // Ensure that it detects suffix after existing prefix.
3441 cx.set_state(indoc! {"
3442 /**ˇ/
3443 "});
3444 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3445 cx.assert_editor_state(indoc! {"
3446 /**
3447 ˇ/
3448 "});
3449 // Ensure that if suffix exists on same line before
3450 // cursor it does not add comment prefix.
3451 cx.set_state(indoc! {"
3452 /** */ˇ
3453 "});
3454 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3455 cx.assert_editor_state(indoc! {"
3456 /** */
3457 ˇ
3458 "});
3459 // Ensure that if suffix exists on same line before
3460 // cursor it does not add comment prefix.
3461 cx.set_state(indoc! {"
3462 /**
3463 *
3464 */ˇ
3465 "});
3466 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3467 cx.assert_editor_state(indoc! {"
3468 /**
3469 *
3470 */
3471 ˇ
3472 "});
3473
3474 // Ensure that inline comment followed by code
3475 // doesn't add comment prefix on newline
3476 cx.set_state(indoc! {"
3477 /** */ textˇ
3478 "});
3479 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3480 cx.assert_editor_state(indoc! {"
3481 /** */ text
3482 ˇ
3483 "});
3484
3485 // Ensure that text after comment end tag
3486 // doesn't add comment prefix on newline
3487 cx.set_state(indoc! {"
3488 /**
3489 *
3490 */ˇtext
3491 "});
3492 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3493 cx.assert_editor_state(indoc! {"
3494 /**
3495 *
3496 */
3497 ˇtext
3498 "});
3499
3500 // Ensure if not comment block it doesn't
3501 // add comment prefix on newline
3502 cx.set_state(indoc! {"
3503 * textˇ
3504 "});
3505 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3506 cx.assert_editor_state(indoc! {"
3507 * text
3508 ˇ
3509 "});
3510 }
3511 // Ensure that comment continuations can be disabled.
3512 update_test_language_settings(cx, |settings| {
3513 settings.defaults.extend_comment_on_newline = Some(false);
3514 });
3515 let mut cx = EditorTestContext::new(cx).await;
3516 cx.set_state(indoc! {"
3517 /**ˇ
3518 "});
3519 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3520 cx.assert_editor_state(indoc! {"
3521 /**
3522 ˇ
3523 "});
3524}
3525
3526#[gpui::test]
3527async fn test_newline_comments_with_block_comment(cx: &mut TestAppContext) {
3528 init_test(cx, |settings| {
3529 settings.defaults.tab_size = NonZeroU32::new(4)
3530 });
3531
3532 let lua_language = Arc::new(Language::new(
3533 LanguageConfig {
3534 line_comments: vec!["--".into()],
3535 block_comment: Some(language::BlockCommentConfig {
3536 start: "--[[".into(),
3537 prefix: "".into(),
3538 end: "]]".into(),
3539 tab_size: 0,
3540 }),
3541 ..LanguageConfig::default()
3542 },
3543 None,
3544 ));
3545
3546 let mut cx = EditorTestContext::new(cx).await;
3547 cx.update_buffer(|buffer, cx| buffer.set_language(Some(lua_language), cx));
3548
3549 // Line with line comment should extend
3550 cx.set_state(indoc! {"
3551 --ˇ
3552 "});
3553 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3554 cx.assert_editor_state(indoc! {"
3555 --
3556 --ˇ
3557 "});
3558
3559 // Line with block comment that matches line comment should not extend
3560 cx.set_state(indoc! {"
3561 --[[ˇ
3562 "});
3563 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3564 cx.assert_editor_state(indoc! {"
3565 --[[
3566 ˇ
3567 "});
3568}
3569
3570#[gpui::test]
3571fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3572 init_test(cx, |_| {});
3573
3574 let editor = cx.add_window(|window, cx| {
3575 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3576 let mut editor = build_editor(buffer, window, cx);
3577 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3578 s.select_ranges([3..4, 11..12, 19..20])
3579 });
3580 editor
3581 });
3582
3583 _ = editor.update(cx, |editor, window, cx| {
3584 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3585 editor.buffer.update(cx, |buffer, cx| {
3586 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3587 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3588 });
3589 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
3590
3591 editor.insert("Z", window, cx);
3592 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3593
3594 // The selections are moved after the inserted characters
3595 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
3596 });
3597}
3598
3599#[gpui::test]
3600async fn test_tab(cx: &mut TestAppContext) {
3601 init_test(cx, |settings| {
3602 settings.defaults.tab_size = NonZeroU32::new(3)
3603 });
3604
3605 let mut cx = EditorTestContext::new(cx).await;
3606 cx.set_state(indoc! {"
3607 ˇabˇc
3608 ˇ🏀ˇ🏀ˇefg
3609 dˇ
3610 "});
3611 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3612 cx.assert_editor_state(indoc! {"
3613 ˇab ˇc
3614 ˇ🏀 ˇ🏀 ˇefg
3615 d ˇ
3616 "});
3617
3618 cx.set_state(indoc! {"
3619 a
3620 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3621 "});
3622 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3623 cx.assert_editor_state(indoc! {"
3624 a
3625 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3626 "});
3627}
3628
3629#[gpui::test]
3630async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3631 init_test(cx, |_| {});
3632
3633 let mut cx = EditorTestContext::new(cx).await;
3634 let language = Arc::new(
3635 Language::new(
3636 LanguageConfig::default(),
3637 Some(tree_sitter_rust::LANGUAGE.into()),
3638 )
3639 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3640 .unwrap(),
3641 );
3642 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3643
3644 // test when all cursors are not at suggested indent
3645 // then simply move to their suggested indent location
3646 cx.set_state(indoc! {"
3647 const a: B = (
3648 c(
3649 ˇ
3650 ˇ )
3651 );
3652 "});
3653 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3654 cx.assert_editor_state(indoc! {"
3655 const a: B = (
3656 c(
3657 ˇ
3658 ˇ)
3659 );
3660 "});
3661
3662 // test cursor already at suggested indent not moving when
3663 // other cursors are yet to reach their suggested indents
3664 cx.set_state(indoc! {"
3665 ˇ
3666 const a: B = (
3667 c(
3668 d(
3669 ˇ
3670 )
3671 ˇ
3672 ˇ )
3673 );
3674 "});
3675 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3676 cx.assert_editor_state(indoc! {"
3677 ˇ
3678 const a: B = (
3679 c(
3680 d(
3681 ˇ
3682 )
3683 ˇ
3684 ˇ)
3685 );
3686 "});
3687 // test when all cursors are at suggested indent then tab is inserted
3688 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3689 cx.assert_editor_state(indoc! {"
3690 ˇ
3691 const a: B = (
3692 c(
3693 d(
3694 ˇ
3695 )
3696 ˇ
3697 ˇ)
3698 );
3699 "});
3700
3701 // test when current indent is less than suggested indent,
3702 // we adjust line to match suggested indent and move cursor to it
3703 //
3704 // when no other cursor is at word boundary, all of them should move
3705 cx.set_state(indoc! {"
3706 const a: B = (
3707 c(
3708 d(
3709 ˇ
3710 ˇ )
3711 ˇ )
3712 );
3713 "});
3714 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3715 cx.assert_editor_state(indoc! {"
3716 const a: B = (
3717 c(
3718 d(
3719 ˇ
3720 ˇ)
3721 ˇ)
3722 );
3723 "});
3724
3725 // test when current indent is less than suggested indent,
3726 // we adjust line to match suggested indent and move cursor to it
3727 //
3728 // when some other cursor is at word boundary, it should not move
3729 cx.set_state(indoc! {"
3730 const a: B = (
3731 c(
3732 d(
3733 ˇ
3734 ˇ )
3735 ˇ)
3736 );
3737 "});
3738 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3739 cx.assert_editor_state(indoc! {"
3740 const a: B = (
3741 c(
3742 d(
3743 ˇ
3744 ˇ)
3745 ˇ)
3746 );
3747 "});
3748
3749 // test when current indent is more than suggested indent,
3750 // we just move cursor to current indent instead of suggested indent
3751 //
3752 // when no other cursor is at word boundary, all of them should move
3753 cx.set_state(indoc! {"
3754 const a: B = (
3755 c(
3756 d(
3757 ˇ
3758 ˇ )
3759 ˇ )
3760 );
3761 "});
3762 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3763 cx.assert_editor_state(indoc! {"
3764 const a: B = (
3765 c(
3766 d(
3767 ˇ
3768 ˇ)
3769 ˇ)
3770 );
3771 "});
3772 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3773 cx.assert_editor_state(indoc! {"
3774 const a: B = (
3775 c(
3776 d(
3777 ˇ
3778 ˇ)
3779 ˇ)
3780 );
3781 "});
3782
3783 // test when current indent is more than suggested indent,
3784 // we just move cursor to current indent instead of suggested indent
3785 //
3786 // when some other cursor is at word boundary, it doesn't move
3787 cx.set_state(indoc! {"
3788 const a: B = (
3789 c(
3790 d(
3791 ˇ
3792 ˇ )
3793 ˇ)
3794 );
3795 "});
3796 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3797 cx.assert_editor_state(indoc! {"
3798 const a: B = (
3799 c(
3800 d(
3801 ˇ
3802 ˇ)
3803 ˇ)
3804 );
3805 "});
3806
3807 // handle auto-indent when there are multiple cursors on the same line
3808 cx.set_state(indoc! {"
3809 const a: B = (
3810 c(
3811 ˇ ˇ
3812 ˇ )
3813 );
3814 "});
3815 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3816 cx.assert_editor_state(indoc! {"
3817 const a: B = (
3818 c(
3819 ˇ
3820 ˇ)
3821 );
3822 "});
3823}
3824
3825#[gpui::test]
3826async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3827 init_test(cx, |settings| {
3828 settings.defaults.tab_size = NonZeroU32::new(3)
3829 });
3830
3831 let mut cx = EditorTestContext::new(cx).await;
3832 cx.set_state(indoc! {"
3833 ˇ
3834 \t ˇ
3835 \t ˇ
3836 \t ˇ
3837 \t \t\t \t \t\t \t\t \t \t ˇ
3838 "});
3839
3840 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3841 cx.assert_editor_state(indoc! {"
3842 ˇ
3843 \t ˇ
3844 \t ˇ
3845 \t ˇ
3846 \t \t\t \t \t\t \t\t \t \t ˇ
3847 "});
3848}
3849
3850#[gpui::test]
3851async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3852 init_test(cx, |settings| {
3853 settings.defaults.tab_size = NonZeroU32::new(4)
3854 });
3855
3856 let language = Arc::new(
3857 Language::new(
3858 LanguageConfig::default(),
3859 Some(tree_sitter_rust::LANGUAGE.into()),
3860 )
3861 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3862 .unwrap(),
3863 );
3864
3865 let mut cx = EditorTestContext::new(cx).await;
3866 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3867 cx.set_state(indoc! {"
3868 fn a() {
3869 if b {
3870 \t ˇc
3871 }
3872 }
3873 "});
3874
3875 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3876 cx.assert_editor_state(indoc! {"
3877 fn a() {
3878 if b {
3879 ˇc
3880 }
3881 }
3882 "});
3883}
3884
3885#[gpui::test]
3886async fn test_indent_outdent(cx: &mut TestAppContext) {
3887 init_test(cx, |settings| {
3888 settings.defaults.tab_size = NonZeroU32::new(4);
3889 });
3890
3891 let mut cx = EditorTestContext::new(cx).await;
3892
3893 cx.set_state(indoc! {"
3894 «oneˇ» «twoˇ»
3895 three
3896 four
3897 "});
3898 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3899 cx.assert_editor_state(indoc! {"
3900 «oneˇ» «twoˇ»
3901 three
3902 four
3903 "});
3904
3905 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3906 cx.assert_editor_state(indoc! {"
3907 «oneˇ» «twoˇ»
3908 three
3909 four
3910 "});
3911
3912 // select across line ending
3913 cx.set_state(indoc! {"
3914 one two
3915 t«hree
3916 ˇ» four
3917 "});
3918 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3919 cx.assert_editor_state(indoc! {"
3920 one two
3921 t«hree
3922 ˇ» four
3923 "});
3924
3925 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3926 cx.assert_editor_state(indoc! {"
3927 one two
3928 t«hree
3929 ˇ» four
3930 "});
3931
3932 // Ensure that indenting/outdenting works when the cursor is at column 0.
3933 cx.set_state(indoc! {"
3934 one two
3935 ˇthree
3936 four
3937 "});
3938 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3939 cx.assert_editor_state(indoc! {"
3940 one two
3941 ˇthree
3942 four
3943 "});
3944
3945 cx.set_state(indoc! {"
3946 one two
3947 ˇ three
3948 four
3949 "});
3950 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3951 cx.assert_editor_state(indoc! {"
3952 one two
3953 ˇthree
3954 four
3955 "});
3956}
3957
3958#[gpui::test]
3959async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) {
3960 // This is a regression test for issue #33761
3961 init_test(cx, |_| {});
3962
3963 let mut cx = EditorTestContext::new(cx).await;
3964 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3965 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3966
3967 cx.set_state(
3968 r#"ˇ# ingress:
3969ˇ# api:
3970ˇ# enabled: false
3971ˇ# pathType: Prefix
3972ˇ# console:
3973ˇ# enabled: false
3974ˇ# pathType: Prefix
3975"#,
3976 );
3977
3978 // Press tab to indent all lines
3979 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3980
3981 cx.assert_editor_state(
3982 r#" ˇ# ingress:
3983 ˇ# api:
3984 ˇ# enabled: false
3985 ˇ# pathType: Prefix
3986 ˇ# console:
3987 ˇ# enabled: false
3988 ˇ# pathType: Prefix
3989"#,
3990 );
3991}
3992
3993#[gpui::test]
3994async fn test_indent_yaml_non_comments_with_multiple_cursors(cx: &mut TestAppContext) {
3995 // This is a test to make sure our fix for issue #33761 didn't break anything
3996 init_test(cx, |_| {});
3997
3998 let mut cx = EditorTestContext::new(cx).await;
3999 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
4000 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
4001
4002 cx.set_state(
4003 r#"ˇingress:
4004ˇ api:
4005ˇ enabled: false
4006ˇ pathType: Prefix
4007"#,
4008 );
4009
4010 // Press tab to indent all lines
4011 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4012
4013 cx.assert_editor_state(
4014 r#"ˇingress:
4015 ˇapi:
4016 ˇenabled: false
4017 ˇpathType: Prefix
4018"#,
4019 );
4020}
4021
4022#[gpui::test]
4023async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
4024 init_test(cx, |settings| {
4025 settings.defaults.hard_tabs = Some(true);
4026 });
4027
4028 let mut cx = EditorTestContext::new(cx).await;
4029
4030 // select two ranges on one line
4031 cx.set_state(indoc! {"
4032 «oneˇ» «twoˇ»
4033 three
4034 four
4035 "});
4036 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4037 cx.assert_editor_state(indoc! {"
4038 \t«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 \t\t«oneˇ» «twoˇ»
4045 three
4046 four
4047 "});
4048 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4049 cx.assert_editor_state(indoc! {"
4050 \t«oneˇ» «twoˇ»
4051 three
4052 four
4053 "});
4054 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4055 cx.assert_editor_state(indoc! {"
4056 «oneˇ» «twoˇ»
4057 three
4058 four
4059 "});
4060
4061 // select across a line ending
4062 cx.set_state(indoc! {"
4063 one two
4064 t«hree
4065 ˇ»four
4066 "});
4067 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4068 cx.assert_editor_state(indoc! {"
4069 one two
4070 \tt«hree
4071 ˇ»four
4072 "});
4073 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4074 cx.assert_editor_state(indoc! {"
4075 one two
4076 \t\tt«hree
4077 ˇ»four
4078 "});
4079 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4080 cx.assert_editor_state(indoc! {"
4081 one two
4082 \tt«hree
4083 ˇ»four
4084 "});
4085 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4086 cx.assert_editor_state(indoc! {"
4087 one two
4088 t«hree
4089 ˇ»four
4090 "});
4091
4092 // Ensure that indenting/outdenting works when the cursor is at column 0.
4093 cx.set_state(indoc! {"
4094 one two
4095 ˇthree
4096 four
4097 "});
4098 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4099 cx.assert_editor_state(indoc! {"
4100 one two
4101 ˇthree
4102 four
4103 "});
4104 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4105 cx.assert_editor_state(indoc! {"
4106 one two
4107 \tˇthree
4108 four
4109 "});
4110 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4111 cx.assert_editor_state(indoc! {"
4112 one two
4113 ˇthree
4114 four
4115 "});
4116}
4117
4118#[gpui::test]
4119fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
4120 init_test(cx, |settings| {
4121 settings.languages.0.extend([
4122 (
4123 "TOML".into(),
4124 LanguageSettingsContent {
4125 tab_size: NonZeroU32::new(2),
4126 ..Default::default()
4127 },
4128 ),
4129 (
4130 "Rust".into(),
4131 LanguageSettingsContent {
4132 tab_size: NonZeroU32::new(4),
4133 ..Default::default()
4134 },
4135 ),
4136 ]);
4137 });
4138
4139 let toml_language = Arc::new(Language::new(
4140 LanguageConfig {
4141 name: "TOML".into(),
4142 ..Default::default()
4143 },
4144 None,
4145 ));
4146 let rust_language = Arc::new(Language::new(
4147 LanguageConfig {
4148 name: "Rust".into(),
4149 ..Default::default()
4150 },
4151 None,
4152 ));
4153
4154 let toml_buffer =
4155 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
4156 let rust_buffer =
4157 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
4158 let multibuffer = cx.new(|cx| {
4159 let mut multibuffer = MultiBuffer::new(ReadWrite);
4160 multibuffer.push_excerpts(
4161 toml_buffer.clone(),
4162 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
4163 cx,
4164 );
4165 multibuffer.push_excerpts(
4166 rust_buffer.clone(),
4167 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
4168 cx,
4169 );
4170 multibuffer
4171 });
4172
4173 cx.add_window(|window, cx| {
4174 let mut editor = build_editor(multibuffer, window, cx);
4175
4176 assert_eq!(
4177 editor.text(cx),
4178 indoc! {"
4179 a = 1
4180 b = 2
4181
4182 const c: usize = 3;
4183 "}
4184 );
4185
4186 select_ranges(
4187 &mut editor,
4188 indoc! {"
4189 «aˇ» = 1
4190 b = 2
4191
4192 «const c:ˇ» usize = 3;
4193 "},
4194 window,
4195 cx,
4196 );
4197
4198 editor.tab(&Tab, window, cx);
4199 assert_text_with_selections(
4200 &mut editor,
4201 indoc! {"
4202 «aˇ» = 1
4203 b = 2
4204
4205 «const c:ˇ» usize = 3;
4206 "},
4207 cx,
4208 );
4209 editor.backtab(&Backtab, window, cx);
4210 assert_text_with_selections(
4211 &mut editor,
4212 indoc! {"
4213 «aˇ» = 1
4214 b = 2
4215
4216 «const c:ˇ» usize = 3;
4217 "},
4218 cx,
4219 );
4220
4221 editor
4222 });
4223}
4224
4225#[gpui::test]
4226async fn test_backspace(cx: &mut TestAppContext) {
4227 init_test(cx, |_| {});
4228
4229 let mut cx = EditorTestContext::new(cx).await;
4230
4231 // Basic backspace
4232 cx.set_state(indoc! {"
4233 onˇe two three
4234 fou«rˇ» five six
4235 seven «ˇeight nine
4236 »ten
4237 "});
4238 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4239 cx.assert_editor_state(indoc! {"
4240 oˇe two three
4241 fouˇ five six
4242 seven ˇten
4243 "});
4244
4245 // Test backspace inside and around indents
4246 cx.set_state(indoc! {"
4247 zero
4248 ˇone
4249 ˇtwo
4250 ˇ ˇ ˇ three
4251 ˇ ˇ four
4252 "});
4253 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4254 cx.assert_editor_state(indoc! {"
4255 zero
4256 ˇone
4257 ˇtwo
4258 ˇ threeˇ four
4259 "});
4260}
4261
4262#[gpui::test]
4263async fn test_delete(cx: &mut TestAppContext) {
4264 init_test(cx, |_| {});
4265
4266 let mut cx = EditorTestContext::new(cx).await;
4267 cx.set_state(indoc! {"
4268 onˇe two three
4269 fou«rˇ» five six
4270 seven «ˇeight nine
4271 »ten
4272 "});
4273 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
4274 cx.assert_editor_state(indoc! {"
4275 onˇ two three
4276 fouˇ five six
4277 seven ˇten
4278 "});
4279}
4280
4281#[gpui::test]
4282fn test_delete_line(cx: &mut TestAppContext) {
4283 init_test(cx, |_| {});
4284
4285 let editor = cx.add_window(|window, cx| {
4286 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4287 build_editor(buffer, window, cx)
4288 });
4289 _ = editor.update(cx, |editor, window, cx| {
4290 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4291 s.select_display_ranges([
4292 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4293 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4294 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4295 ])
4296 });
4297 editor.delete_line(&DeleteLine, window, cx);
4298 assert_eq!(editor.display_text(cx), "ghi");
4299 assert_eq!(
4300 editor.selections.display_ranges(cx),
4301 vec![
4302 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
4303 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4304 ]
4305 );
4306 });
4307
4308 let editor = cx.add_window(|window, cx| {
4309 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4310 build_editor(buffer, window, cx)
4311 });
4312 _ = editor.update(cx, |editor, window, cx| {
4313 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4314 s.select_display_ranges([
4315 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
4316 ])
4317 });
4318 editor.delete_line(&DeleteLine, window, cx);
4319 assert_eq!(editor.display_text(cx), "ghi\n");
4320 assert_eq!(
4321 editor.selections.display_ranges(cx),
4322 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
4323 );
4324 });
4325
4326 let editor = cx.add_window(|window, cx| {
4327 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n\njkl\nmno", cx);
4328 build_editor(buffer, window, cx)
4329 });
4330 _ = editor.update(cx, |editor, window, cx| {
4331 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4332 s.select_display_ranges([
4333 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(2), 1)
4334 ])
4335 });
4336 editor.delete_line(&DeleteLine, window, cx);
4337 assert_eq!(editor.display_text(cx), "\njkl\nmno");
4338 assert_eq!(
4339 editor.selections.display_ranges(cx),
4340 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
4341 );
4342 });
4343}
4344
4345#[gpui::test]
4346fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
4347 init_test(cx, |_| {});
4348
4349 cx.add_window(|window, cx| {
4350 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4351 let mut editor = build_editor(buffer.clone(), window, cx);
4352 let buffer = buffer.read(cx).as_singleton().unwrap();
4353
4354 assert_eq!(
4355 editor.selections.ranges::<Point>(cx),
4356 &[Point::new(0, 0)..Point::new(0, 0)]
4357 );
4358
4359 // When on single line, replace newline at end by space
4360 editor.join_lines(&JoinLines, window, cx);
4361 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4362 assert_eq!(
4363 editor.selections.ranges::<Point>(cx),
4364 &[Point::new(0, 3)..Point::new(0, 3)]
4365 );
4366
4367 // When multiple lines are selected, remove newlines that are spanned by the selection
4368 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4369 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
4370 });
4371 editor.join_lines(&JoinLines, window, cx);
4372 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
4373 assert_eq!(
4374 editor.selections.ranges::<Point>(cx),
4375 &[Point::new(0, 11)..Point::new(0, 11)]
4376 );
4377
4378 // Undo should be transactional
4379 editor.undo(&Undo, window, cx);
4380 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4381 assert_eq!(
4382 editor.selections.ranges::<Point>(cx),
4383 &[Point::new(0, 5)..Point::new(2, 2)]
4384 );
4385
4386 // When joining an empty line don't insert a space
4387 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4388 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
4389 });
4390 editor.join_lines(&JoinLines, window, cx);
4391 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
4392 assert_eq!(
4393 editor.selections.ranges::<Point>(cx),
4394 [Point::new(2, 3)..Point::new(2, 3)]
4395 );
4396
4397 // We can remove trailing newlines
4398 editor.join_lines(&JoinLines, window, cx);
4399 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4400 assert_eq!(
4401 editor.selections.ranges::<Point>(cx),
4402 [Point::new(2, 3)..Point::new(2, 3)]
4403 );
4404
4405 // We don't blow up on the last line
4406 editor.join_lines(&JoinLines, window, cx);
4407 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4408 assert_eq!(
4409 editor.selections.ranges::<Point>(cx),
4410 [Point::new(2, 3)..Point::new(2, 3)]
4411 );
4412
4413 // reset to test indentation
4414 editor.buffer.update(cx, |buffer, cx| {
4415 buffer.edit(
4416 [
4417 (Point::new(1, 0)..Point::new(1, 2), " "),
4418 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
4419 ],
4420 None,
4421 cx,
4422 )
4423 });
4424
4425 // We remove any leading spaces
4426 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
4427 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4428 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
4429 });
4430 editor.join_lines(&JoinLines, window, cx);
4431 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
4432
4433 // We don't insert a space for a line containing only spaces
4434 editor.join_lines(&JoinLines, window, cx);
4435 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
4436
4437 // We ignore any leading tabs
4438 editor.join_lines(&JoinLines, window, cx);
4439 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
4440
4441 editor
4442 });
4443}
4444
4445#[gpui::test]
4446fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
4447 init_test(cx, |_| {});
4448
4449 cx.add_window(|window, cx| {
4450 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4451 let mut editor = build_editor(buffer.clone(), window, cx);
4452 let buffer = buffer.read(cx).as_singleton().unwrap();
4453
4454 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4455 s.select_ranges([
4456 Point::new(0, 2)..Point::new(1, 1),
4457 Point::new(1, 2)..Point::new(1, 2),
4458 Point::new(3, 1)..Point::new(3, 2),
4459 ])
4460 });
4461
4462 editor.join_lines(&JoinLines, window, cx);
4463 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
4464
4465 assert_eq!(
4466 editor.selections.ranges::<Point>(cx),
4467 [
4468 Point::new(0, 7)..Point::new(0, 7),
4469 Point::new(1, 3)..Point::new(1, 3)
4470 ]
4471 );
4472 editor
4473 });
4474}
4475
4476#[gpui::test]
4477async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
4478 init_test(cx, |_| {});
4479
4480 let mut cx = EditorTestContext::new(cx).await;
4481
4482 let diff_base = r#"
4483 Line 0
4484 Line 1
4485 Line 2
4486 Line 3
4487 "#
4488 .unindent();
4489
4490 cx.set_state(
4491 &r#"
4492 ˇLine 0
4493 Line 1
4494 Line 2
4495 Line 3
4496 "#
4497 .unindent(),
4498 );
4499
4500 cx.set_head_text(&diff_base);
4501 executor.run_until_parked();
4502
4503 // Join lines
4504 cx.update_editor(|editor, window, cx| {
4505 editor.join_lines(&JoinLines, window, cx);
4506 });
4507 executor.run_until_parked();
4508
4509 cx.assert_editor_state(
4510 &r#"
4511 Line 0ˇ Line 1
4512 Line 2
4513 Line 3
4514 "#
4515 .unindent(),
4516 );
4517 // Join again
4518 cx.update_editor(|editor, window, cx| {
4519 editor.join_lines(&JoinLines, window, cx);
4520 });
4521 executor.run_until_parked();
4522
4523 cx.assert_editor_state(
4524 &r#"
4525 Line 0 Line 1ˇ Line 2
4526 Line 3
4527 "#
4528 .unindent(),
4529 );
4530}
4531
4532#[gpui::test]
4533async fn test_custom_newlines_cause_no_false_positive_diffs(
4534 executor: BackgroundExecutor,
4535 cx: &mut TestAppContext,
4536) {
4537 init_test(cx, |_| {});
4538 let mut cx = EditorTestContext::new(cx).await;
4539 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
4540 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
4541 executor.run_until_parked();
4542
4543 cx.update_editor(|editor, window, cx| {
4544 let snapshot = editor.snapshot(window, cx);
4545 assert_eq!(
4546 snapshot
4547 .buffer_snapshot()
4548 .diff_hunks_in_range(0..snapshot.buffer_snapshot().len())
4549 .collect::<Vec<_>>(),
4550 Vec::new(),
4551 "Should not have any diffs for files with custom newlines"
4552 );
4553 });
4554}
4555
4556#[gpui::test]
4557async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
4558 init_test(cx, |_| {});
4559
4560 let mut cx = EditorTestContext::new(cx).await;
4561
4562 // Test sort_lines_case_insensitive()
4563 cx.set_state(indoc! {"
4564 «z
4565 y
4566 x
4567 Z
4568 Y
4569 Xˇ»
4570 "});
4571 cx.update_editor(|e, window, cx| {
4572 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
4573 });
4574 cx.assert_editor_state(indoc! {"
4575 «x
4576 X
4577 y
4578 Y
4579 z
4580 Zˇ»
4581 "});
4582
4583 // Test sort_lines_by_length()
4584 //
4585 // Demonstrates:
4586 // - ∞ is 3 bytes UTF-8, but sorted by its char count (1)
4587 // - sort is stable
4588 cx.set_state(indoc! {"
4589 «123
4590 æ
4591 12
4592 ∞
4593 1
4594 æˇ»
4595 "});
4596 cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
4597 cx.assert_editor_state(indoc! {"
4598 «æ
4599 ∞
4600 1
4601 æ
4602 12
4603 123ˇ»
4604 "});
4605
4606 // Test reverse_lines()
4607 cx.set_state(indoc! {"
4608 «5
4609 4
4610 3
4611 2
4612 1ˇ»
4613 "});
4614 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4615 cx.assert_editor_state(indoc! {"
4616 «1
4617 2
4618 3
4619 4
4620 5ˇ»
4621 "});
4622
4623 // Skip testing shuffle_line()
4624
4625 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4626 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4627
4628 // Don't manipulate when cursor is on single line, but expand the selection
4629 cx.set_state(indoc! {"
4630 ddˇdd
4631 ccc
4632 bb
4633 a
4634 "});
4635 cx.update_editor(|e, window, cx| {
4636 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4637 });
4638 cx.assert_editor_state(indoc! {"
4639 «ddddˇ»
4640 ccc
4641 bb
4642 a
4643 "});
4644
4645 // Basic manipulate case
4646 // Start selection moves to column 0
4647 // End of selection shrinks to fit shorter line
4648 cx.set_state(indoc! {"
4649 dd«d
4650 ccc
4651 bb
4652 aaaaaˇ»
4653 "});
4654 cx.update_editor(|e, window, cx| {
4655 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4656 });
4657 cx.assert_editor_state(indoc! {"
4658 «aaaaa
4659 bb
4660 ccc
4661 dddˇ»
4662 "});
4663
4664 // Manipulate case with newlines
4665 cx.set_state(indoc! {"
4666 dd«d
4667 ccc
4668
4669 bb
4670 aaaaa
4671
4672 ˇ»
4673 "});
4674 cx.update_editor(|e, window, cx| {
4675 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4676 });
4677 cx.assert_editor_state(indoc! {"
4678 «
4679
4680 aaaaa
4681 bb
4682 ccc
4683 dddˇ»
4684
4685 "});
4686
4687 // Adding new line
4688 cx.set_state(indoc! {"
4689 aa«a
4690 bbˇ»b
4691 "});
4692 cx.update_editor(|e, window, cx| {
4693 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4694 });
4695 cx.assert_editor_state(indoc! {"
4696 «aaa
4697 bbb
4698 added_lineˇ»
4699 "});
4700
4701 // Removing line
4702 cx.set_state(indoc! {"
4703 aa«a
4704 bbbˇ»
4705 "});
4706 cx.update_editor(|e, window, cx| {
4707 e.manipulate_immutable_lines(window, cx, |lines| {
4708 lines.pop();
4709 })
4710 });
4711 cx.assert_editor_state(indoc! {"
4712 «aaaˇ»
4713 "});
4714
4715 // Removing all lines
4716 cx.set_state(indoc! {"
4717 aa«a
4718 bbbˇ»
4719 "});
4720 cx.update_editor(|e, window, cx| {
4721 e.manipulate_immutable_lines(window, cx, |lines| {
4722 lines.drain(..);
4723 })
4724 });
4725 cx.assert_editor_state(indoc! {"
4726 ˇ
4727 "});
4728}
4729
4730#[gpui::test]
4731async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4732 init_test(cx, |_| {});
4733
4734 let mut cx = EditorTestContext::new(cx).await;
4735
4736 // Consider continuous selection as single selection
4737 cx.set_state(indoc! {"
4738 Aaa«aa
4739 cˇ»c«c
4740 bb
4741 aaaˇ»aa
4742 "});
4743 cx.update_editor(|e, window, cx| {
4744 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4745 });
4746 cx.assert_editor_state(indoc! {"
4747 «Aaaaa
4748 ccc
4749 bb
4750 aaaaaˇ»
4751 "});
4752
4753 cx.set_state(indoc! {"
4754 Aaa«aa
4755 cˇ»c«c
4756 bb
4757 aaaˇ»aa
4758 "});
4759 cx.update_editor(|e, window, cx| {
4760 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4761 });
4762 cx.assert_editor_state(indoc! {"
4763 «Aaaaa
4764 ccc
4765 bbˇ»
4766 "});
4767
4768 // Consider non continuous selection as distinct dedup operations
4769 cx.set_state(indoc! {"
4770 «aaaaa
4771 bb
4772 aaaaa
4773 aaaaaˇ»
4774
4775 aaa«aaˇ»
4776 "});
4777 cx.update_editor(|e, window, cx| {
4778 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4779 });
4780 cx.assert_editor_state(indoc! {"
4781 «aaaaa
4782 bbˇ»
4783
4784 «aaaaaˇ»
4785 "});
4786}
4787
4788#[gpui::test]
4789async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4790 init_test(cx, |_| {});
4791
4792 let mut cx = EditorTestContext::new(cx).await;
4793
4794 cx.set_state(indoc! {"
4795 «Aaa
4796 aAa
4797 Aaaˇ»
4798 "});
4799 cx.update_editor(|e, window, cx| {
4800 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4801 });
4802 cx.assert_editor_state(indoc! {"
4803 «Aaa
4804 aAaˇ»
4805 "});
4806
4807 cx.set_state(indoc! {"
4808 «Aaa
4809 aAa
4810 aaAˇ»
4811 "});
4812 cx.update_editor(|e, window, cx| {
4813 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4814 });
4815 cx.assert_editor_state(indoc! {"
4816 «Aaaˇ»
4817 "});
4818}
4819
4820#[gpui::test]
4821async fn test_wrap_in_tag_single_selection(cx: &mut TestAppContext) {
4822 init_test(cx, |_| {});
4823
4824 let mut cx = EditorTestContext::new(cx).await;
4825
4826 let js_language = Arc::new(Language::new(
4827 LanguageConfig {
4828 name: "JavaScript".into(),
4829 wrap_characters: Some(language::WrapCharactersConfig {
4830 start_prefix: "<".into(),
4831 start_suffix: ">".into(),
4832 end_prefix: "</".into(),
4833 end_suffix: ">".into(),
4834 }),
4835 ..LanguageConfig::default()
4836 },
4837 None,
4838 ));
4839
4840 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
4841
4842 cx.set_state(indoc! {"
4843 «testˇ»
4844 "});
4845 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4846 cx.assert_editor_state(indoc! {"
4847 <«ˇ»>test</«ˇ»>
4848 "});
4849
4850 cx.set_state(indoc! {"
4851 «test
4852 testˇ»
4853 "});
4854 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4855 cx.assert_editor_state(indoc! {"
4856 <«ˇ»>test
4857 test</«ˇ»>
4858 "});
4859
4860 cx.set_state(indoc! {"
4861 teˇst
4862 "});
4863 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4864 cx.assert_editor_state(indoc! {"
4865 te<«ˇ»></«ˇ»>st
4866 "});
4867}
4868
4869#[gpui::test]
4870async fn test_wrap_in_tag_multi_selection(cx: &mut TestAppContext) {
4871 init_test(cx, |_| {});
4872
4873 let mut cx = EditorTestContext::new(cx).await;
4874
4875 let js_language = Arc::new(Language::new(
4876 LanguageConfig {
4877 name: "JavaScript".into(),
4878 wrap_characters: Some(language::WrapCharactersConfig {
4879 start_prefix: "<".into(),
4880 start_suffix: ">".into(),
4881 end_prefix: "</".into(),
4882 end_suffix: ">".into(),
4883 }),
4884 ..LanguageConfig::default()
4885 },
4886 None,
4887 ));
4888
4889 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
4890
4891 cx.set_state(indoc! {"
4892 «testˇ»
4893 «testˇ» «testˇ»
4894 «testˇ»
4895 "});
4896 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4897 cx.assert_editor_state(indoc! {"
4898 <«ˇ»>test</«ˇ»>
4899 <«ˇ»>test</«ˇ»> <«ˇ»>test</«ˇ»>
4900 <«ˇ»>test</«ˇ»>
4901 "});
4902
4903 cx.set_state(indoc! {"
4904 «test
4905 testˇ»
4906 «test
4907 testˇ»
4908 "});
4909 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4910 cx.assert_editor_state(indoc! {"
4911 <«ˇ»>test
4912 test</«ˇ»>
4913 <«ˇ»>test
4914 test</«ˇ»>
4915 "});
4916}
4917
4918#[gpui::test]
4919async fn test_wrap_in_tag_does_nothing_in_unsupported_languages(cx: &mut TestAppContext) {
4920 init_test(cx, |_| {});
4921
4922 let mut cx = EditorTestContext::new(cx).await;
4923
4924 let plaintext_language = Arc::new(Language::new(
4925 LanguageConfig {
4926 name: "Plain Text".into(),
4927 ..LanguageConfig::default()
4928 },
4929 None,
4930 ));
4931
4932 cx.update_buffer(|buffer, cx| buffer.set_language(Some(plaintext_language), cx));
4933
4934 cx.set_state(indoc! {"
4935 «testˇ»
4936 "});
4937 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4938 cx.assert_editor_state(indoc! {"
4939 «testˇ»
4940 "});
4941}
4942
4943#[gpui::test]
4944async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
4945 init_test(cx, |_| {});
4946
4947 let mut cx = EditorTestContext::new(cx).await;
4948
4949 // Manipulate with multiple selections on a single line
4950 cx.set_state(indoc! {"
4951 dd«dd
4952 cˇ»c«c
4953 bb
4954 aaaˇ»aa
4955 "});
4956 cx.update_editor(|e, window, cx| {
4957 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4958 });
4959 cx.assert_editor_state(indoc! {"
4960 «aaaaa
4961 bb
4962 ccc
4963 ddddˇ»
4964 "});
4965
4966 // Manipulate with multiple disjoin selections
4967 cx.set_state(indoc! {"
4968 5«
4969 4
4970 3
4971 2
4972 1ˇ»
4973
4974 dd«dd
4975 ccc
4976 bb
4977 aaaˇ»aa
4978 "});
4979 cx.update_editor(|e, window, cx| {
4980 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4981 });
4982 cx.assert_editor_state(indoc! {"
4983 «1
4984 2
4985 3
4986 4
4987 5ˇ»
4988
4989 «aaaaa
4990 bb
4991 ccc
4992 ddddˇ»
4993 "});
4994
4995 // Adding lines on each selection
4996 cx.set_state(indoc! {"
4997 2«
4998 1ˇ»
4999
5000 bb«bb
5001 aaaˇ»aa
5002 "});
5003 cx.update_editor(|e, window, cx| {
5004 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
5005 });
5006 cx.assert_editor_state(indoc! {"
5007 «2
5008 1
5009 added lineˇ»
5010
5011 «bbbb
5012 aaaaa
5013 added lineˇ»
5014 "});
5015
5016 // Removing lines on each selection
5017 cx.set_state(indoc! {"
5018 2«
5019 1ˇ»
5020
5021 bb«bb
5022 aaaˇ»aa
5023 "});
5024 cx.update_editor(|e, window, cx| {
5025 e.manipulate_immutable_lines(window, cx, |lines| {
5026 lines.pop();
5027 })
5028 });
5029 cx.assert_editor_state(indoc! {"
5030 «2ˇ»
5031
5032 «bbbbˇ»
5033 "});
5034}
5035
5036#[gpui::test]
5037async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
5038 init_test(cx, |settings| {
5039 settings.defaults.tab_size = NonZeroU32::new(3)
5040 });
5041
5042 let mut cx = EditorTestContext::new(cx).await;
5043
5044 // MULTI SELECTION
5045 // Ln.1 "«" tests empty lines
5046 // Ln.9 tests just leading whitespace
5047 cx.set_state(indoc! {"
5048 «
5049 abc // No indentationˇ»
5050 «\tabc // 1 tabˇ»
5051 \t\tabc « ˇ» // 2 tabs
5052 \t ab«c // Tab followed by space
5053 \tabc // Space followed by tab (3 spaces should be the result)
5054 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5055 abˇ»ˇc ˇ ˇ // Already space indented«
5056 \t
5057 \tabc\tdef // Only the leading tab is manipulatedˇ»
5058 "});
5059 cx.update_editor(|e, window, cx| {
5060 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5061 });
5062 cx.assert_editor_state(
5063 indoc! {"
5064 «
5065 abc // No indentation
5066 abc // 1 tab
5067 abc // 2 tabs
5068 abc // Tab followed by space
5069 abc // Space followed by tab (3 spaces should be the result)
5070 abc // Mixed indentation (tab conversion depends on the column)
5071 abc // Already space indented
5072 ·
5073 abc\tdef // Only the leading tab is manipulatedˇ»
5074 "}
5075 .replace("·", "")
5076 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5077 );
5078
5079 // Test on just a few lines, the others should remain unchanged
5080 // Only lines (3, 5, 10, 11) should change
5081 cx.set_state(
5082 indoc! {"
5083 ·
5084 abc // No indentation
5085 \tabcˇ // 1 tab
5086 \t\tabc // 2 tabs
5087 \t abcˇ // Tab followed by space
5088 \tabc // Space followed by tab (3 spaces should be the result)
5089 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5090 abc // Already space indented
5091 «\t
5092 \tabc\tdef // Only the leading tab is manipulatedˇ»
5093 "}
5094 .replace("·", "")
5095 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5096 );
5097 cx.update_editor(|e, window, cx| {
5098 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5099 });
5100 cx.assert_editor_state(
5101 indoc! {"
5102 ·
5103 abc // No indentation
5104 « abc // 1 tabˇ»
5105 \t\tabc // 2 tabs
5106 « abc // Tab followed by spaceˇ»
5107 \tabc // Space followed by tab (3 spaces should be the result)
5108 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5109 abc // Already space indented
5110 « ·
5111 abc\tdef // Only the leading tab is manipulatedˇ»
5112 "}
5113 .replace("·", "")
5114 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5115 );
5116
5117 // SINGLE SELECTION
5118 // Ln.1 "«" tests empty lines
5119 // Ln.9 tests just leading whitespace
5120 cx.set_state(indoc! {"
5121 «
5122 abc // No indentation
5123 \tabc // 1 tab
5124 \t\tabc // 2 tabs
5125 \t abc // Tab followed by space
5126 \tabc // Space followed by tab (3 spaces should be the result)
5127 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5128 abc // Already space indented
5129 \t
5130 \tabc\tdef // Only the leading tab is manipulatedˇ»
5131 "});
5132 cx.update_editor(|e, window, cx| {
5133 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5134 });
5135 cx.assert_editor_state(
5136 indoc! {"
5137 «
5138 abc // No indentation
5139 abc // 1 tab
5140 abc // 2 tabs
5141 abc // Tab followed by space
5142 abc // Space followed by tab (3 spaces should be the result)
5143 abc // Mixed indentation (tab conversion depends on the column)
5144 abc // Already space indented
5145 ·
5146 abc\tdef // Only the leading tab is manipulatedˇ»
5147 "}
5148 .replace("·", "")
5149 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5150 );
5151}
5152
5153#[gpui::test]
5154async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
5155 init_test(cx, |settings| {
5156 settings.defaults.tab_size = NonZeroU32::new(3)
5157 });
5158
5159 let mut cx = EditorTestContext::new(cx).await;
5160
5161 // MULTI SELECTION
5162 // Ln.1 "«" tests empty lines
5163 // Ln.11 tests just leading whitespace
5164 cx.set_state(indoc! {"
5165 «
5166 abˇ»ˇc // No indentation
5167 abc ˇ ˇ // 1 space (< 3 so dont convert)
5168 abc « // 2 spaces (< 3 so dont convert)
5169 abc // 3 spaces (convert)
5170 abc ˇ» // 5 spaces (1 tab + 2 spaces)
5171 «\tˇ»\t«\tˇ»abc // Already tab indented
5172 «\t abc // Tab followed by space
5173 \tabc // Space followed by tab (should be consumed due to tab)
5174 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5175 \tˇ» «\t
5176 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
5177 "});
5178 cx.update_editor(|e, window, cx| {
5179 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5180 });
5181 cx.assert_editor_state(indoc! {"
5182 «
5183 abc // No indentation
5184 abc // 1 space (< 3 so dont convert)
5185 abc // 2 spaces (< 3 so dont convert)
5186 \tabc // 3 spaces (convert)
5187 \t abc // 5 spaces (1 tab + 2 spaces)
5188 \t\t\tabc // Already tab indented
5189 \t abc // Tab followed by space
5190 \tabc // Space followed by tab (should be consumed due to tab)
5191 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5192 \t\t\t
5193 \tabc \t // Only the leading spaces should be convertedˇ»
5194 "});
5195
5196 // Test on just a few lines, the other should remain unchanged
5197 // Only lines (4, 8, 11, 12) should change
5198 cx.set_state(
5199 indoc! {"
5200 ·
5201 abc // No indentation
5202 abc // 1 space (< 3 so dont convert)
5203 abc // 2 spaces (< 3 so dont convert)
5204 « abc // 3 spaces (convert)ˇ»
5205 abc // 5 spaces (1 tab + 2 spaces)
5206 \t\t\tabc // Already tab indented
5207 \t abc // Tab followed by space
5208 \tabc ˇ // Space followed by tab (should be consumed due to tab)
5209 \t\t \tabc // Mixed indentation
5210 \t \t \t \tabc // Mixed indentation
5211 \t \tˇ
5212 « abc \t // Only the leading spaces should be convertedˇ»
5213 "}
5214 .replace("·", "")
5215 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5216 );
5217 cx.update_editor(|e, window, cx| {
5218 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5219 });
5220 cx.assert_editor_state(
5221 indoc! {"
5222 ·
5223 abc // No indentation
5224 abc // 1 space (< 3 so dont convert)
5225 abc // 2 spaces (< 3 so dont convert)
5226 «\tabc // 3 spaces (convert)ˇ»
5227 abc // 5 spaces (1 tab + 2 spaces)
5228 \t\t\tabc // Already tab indented
5229 \t abc // Tab followed by space
5230 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
5231 \t\t \tabc // Mixed indentation
5232 \t \t \t \tabc // Mixed indentation
5233 «\t\t\t
5234 \tabc \t // Only the leading spaces should be convertedˇ»
5235 "}
5236 .replace("·", "")
5237 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5238 );
5239
5240 // SINGLE SELECTION
5241 // Ln.1 "«" tests empty lines
5242 // Ln.11 tests just leading whitespace
5243 cx.set_state(indoc! {"
5244 «
5245 abc // No indentation
5246 abc // 1 space (< 3 so dont convert)
5247 abc // 2 spaces (< 3 so dont convert)
5248 abc // 3 spaces (convert)
5249 abc // 5 spaces (1 tab + 2 spaces)
5250 \t\t\tabc // Already tab indented
5251 \t abc // Tab followed by space
5252 \tabc // Space followed by tab (should be consumed due to tab)
5253 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5254 \t \t
5255 abc \t // Only the leading spaces should be convertedˇ»
5256 "});
5257 cx.update_editor(|e, window, cx| {
5258 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5259 });
5260 cx.assert_editor_state(indoc! {"
5261 «
5262 abc // No indentation
5263 abc // 1 space (< 3 so dont convert)
5264 abc // 2 spaces (< 3 so dont convert)
5265 \tabc // 3 spaces (convert)
5266 \t abc // 5 spaces (1 tab + 2 spaces)
5267 \t\t\tabc // Already tab indented
5268 \t abc // Tab followed by space
5269 \tabc // Space followed by tab (should be consumed due to tab)
5270 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5271 \t\t\t
5272 \tabc \t // Only the leading spaces should be convertedˇ»
5273 "});
5274}
5275
5276#[gpui::test]
5277async fn test_toggle_case(cx: &mut TestAppContext) {
5278 init_test(cx, |_| {});
5279
5280 let mut cx = EditorTestContext::new(cx).await;
5281
5282 // If all lower case -> upper case
5283 cx.set_state(indoc! {"
5284 «hello worldˇ»
5285 "});
5286 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5287 cx.assert_editor_state(indoc! {"
5288 «HELLO WORLDˇ»
5289 "});
5290
5291 // If all upper case -> lower case
5292 cx.set_state(indoc! {"
5293 «HELLO WORLDˇ»
5294 "});
5295 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5296 cx.assert_editor_state(indoc! {"
5297 «hello worldˇ»
5298 "});
5299
5300 // If any upper case characters are identified -> lower case
5301 // This matches JetBrains IDEs
5302 cx.set_state(indoc! {"
5303 «hEllo worldˇ»
5304 "});
5305 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5306 cx.assert_editor_state(indoc! {"
5307 «hello worldˇ»
5308 "});
5309}
5310
5311#[gpui::test]
5312async fn test_convert_to_sentence_case(cx: &mut TestAppContext) {
5313 init_test(cx, |_| {});
5314
5315 let mut cx = EditorTestContext::new(cx).await;
5316
5317 cx.set_state(indoc! {"
5318 «implement-windows-supportˇ»
5319 "});
5320 cx.update_editor(|e, window, cx| {
5321 e.convert_to_sentence_case(&ConvertToSentenceCase, window, cx)
5322 });
5323 cx.assert_editor_state(indoc! {"
5324 «Implement windows supportˇ»
5325 "});
5326}
5327
5328#[gpui::test]
5329async fn test_manipulate_text(cx: &mut TestAppContext) {
5330 init_test(cx, |_| {});
5331
5332 let mut cx = EditorTestContext::new(cx).await;
5333
5334 // Test convert_to_upper_case()
5335 cx.set_state(indoc! {"
5336 «hello worldˇ»
5337 "});
5338 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5339 cx.assert_editor_state(indoc! {"
5340 «HELLO WORLDˇ»
5341 "});
5342
5343 // Test convert_to_lower_case()
5344 cx.set_state(indoc! {"
5345 «HELLO WORLDˇ»
5346 "});
5347 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
5348 cx.assert_editor_state(indoc! {"
5349 «hello worldˇ»
5350 "});
5351
5352 // Test multiple line, single selection case
5353 cx.set_state(indoc! {"
5354 «The quick brown
5355 fox jumps over
5356 the lazy dogˇ»
5357 "});
5358 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
5359 cx.assert_editor_state(indoc! {"
5360 «The Quick Brown
5361 Fox Jumps Over
5362 The Lazy Dogˇ»
5363 "});
5364
5365 // Test multiple line, single selection case
5366 cx.set_state(indoc! {"
5367 «The quick brown
5368 fox jumps over
5369 the lazy dogˇ»
5370 "});
5371 cx.update_editor(|e, window, cx| {
5372 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
5373 });
5374 cx.assert_editor_state(indoc! {"
5375 «TheQuickBrown
5376 FoxJumpsOver
5377 TheLazyDogˇ»
5378 "});
5379
5380 // From here on out, test more complex cases of manipulate_text()
5381
5382 // Test no selection case - should affect words cursors are in
5383 // Cursor at beginning, middle, and end of word
5384 cx.set_state(indoc! {"
5385 ˇhello big beauˇtiful worldˇ
5386 "});
5387 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5388 cx.assert_editor_state(indoc! {"
5389 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
5390 "});
5391
5392 // Test multiple selections on a single line and across multiple lines
5393 cx.set_state(indoc! {"
5394 «Theˇ» quick «brown
5395 foxˇ» jumps «overˇ»
5396 the «lazyˇ» dog
5397 "});
5398 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5399 cx.assert_editor_state(indoc! {"
5400 «THEˇ» quick «BROWN
5401 FOXˇ» jumps «OVERˇ»
5402 the «LAZYˇ» dog
5403 "});
5404
5405 // Test case where text length grows
5406 cx.set_state(indoc! {"
5407 «tschüߡ»
5408 "});
5409 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5410 cx.assert_editor_state(indoc! {"
5411 «TSCHÜSSˇ»
5412 "});
5413
5414 // Test to make sure we don't crash when text shrinks
5415 cx.set_state(indoc! {"
5416 aaa_bbbˇ
5417 "});
5418 cx.update_editor(|e, window, cx| {
5419 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5420 });
5421 cx.assert_editor_state(indoc! {"
5422 «aaaBbbˇ»
5423 "});
5424
5425 // Test to make sure we all aware of the fact that each word can grow and shrink
5426 // Final selections should be aware of this fact
5427 cx.set_state(indoc! {"
5428 aaa_bˇbb bbˇb_ccc ˇccc_ddd
5429 "});
5430 cx.update_editor(|e, window, cx| {
5431 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5432 });
5433 cx.assert_editor_state(indoc! {"
5434 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
5435 "});
5436
5437 cx.set_state(indoc! {"
5438 «hElLo, WoRld!ˇ»
5439 "});
5440 cx.update_editor(|e, window, cx| {
5441 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
5442 });
5443 cx.assert_editor_state(indoc! {"
5444 «HeLlO, wOrLD!ˇ»
5445 "});
5446
5447 // Test selections with `line_mode() = true`.
5448 cx.update_editor(|editor, _window, _cx| editor.selections.set_line_mode(true));
5449 cx.set_state(indoc! {"
5450 «The quick brown
5451 fox jumps over
5452 tˇ»he lazy dog
5453 "});
5454 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5455 cx.assert_editor_state(indoc! {"
5456 «THE QUICK BROWN
5457 FOX JUMPS OVER
5458 THE LAZY DOGˇ»
5459 "});
5460}
5461
5462#[gpui::test]
5463fn test_duplicate_line(cx: &mut TestAppContext) {
5464 init_test(cx, |_| {});
5465
5466 let editor = cx.add_window(|window, cx| {
5467 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5468 build_editor(buffer, window, cx)
5469 });
5470 _ = editor.update(cx, |editor, window, cx| {
5471 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5472 s.select_display_ranges([
5473 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5474 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5475 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5476 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5477 ])
5478 });
5479 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5480 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5481 assert_eq!(
5482 editor.selections.display_ranges(cx),
5483 vec![
5484 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
5485 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
5486 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5487 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5488 ]
5489 );
5490 });
5491
5492 let editor = cx.add_window(|window, cx| {
5493 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5494 build_editor(buffer, window, cx)
5495 });
5496 _ = editor.update(cx, |editor, window, cx| {
5497 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5498 s.select_display_ranges([
5499 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5500 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5501 ])
5502 });
5503 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5504 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5505 assert_eq!(
5506 editor.selections.display_ranges(cx),
5507 vec![
5508 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
5509 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
5510 ]
5511 );
5512 });
5513
5514 // With `move_upwards` the selections stay in place, except for
5515 // the lines inserted above them
5516 let editor = cx.add_window(|window, cx| {
5517 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5518 build_editor(buffer, window, cx)
5519 });
5520 _ = editor.update(cx, |editor, window, cx| {
5521 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5522 s.select_display_ranges([
5523 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5524 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5525 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5526 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5527 ])
5528 });
5529 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5530 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5531 assert_eq!(
5532 editor.selections.display_ranges(cx),
5533 vec![
5534 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5535 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5536 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
5537 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5538 ]
5539 );
5540 });
5541
5542 let editor = cx.add_window(|window, cx| {
5543 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5544 build_editor(buffer, window, cx)
5545 });
5546 _ = editor.update(cx, |editor, window, cx| {
5547 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5548 s.select_display_ranges([
5549 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5550 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5551 ])
5552 });
5553 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5554 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5555 assert_eq!(
5556 editor.selections.display_ranges(cx),
5557 vec![
5558 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5559 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5560 ]
5561 );
5562 });
5563
5564 let editor = cx.add_window(|window, cx| {
5565 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5566 build_editor(buffer, window, cx)
5567 });
5568 _ = editor.update(cx, |editor, window, cx| {
5569 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5570 s.select_display_ranges([
5571 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5572 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5573 ])
5574 });
5575 editor.duplicate_selection(&DuplicateSelection, window, cx);
5576 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
5577 assert_eq!(
5578 editor.selections.display_ranges(cx),
5579 vec![
5580 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5581 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
5582 ]
5583 );
5584 });
5585}
5586
5587#[gpui::test]
5588fn test_move_line_up_down(cx: &mut TestAppContext) {
5589 init_test(cx, |_| {});
5590
5591 let editor = cx.add_window(|window, cx| {
5592 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5593 build_editor(buffer, window, cx)
5594 });
5595 _ = editor.update(cx, |editor, window, cx| {
5596 editor.fold_creases(
5597 vec![
5598 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5599 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5600 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5601 ],
5602 true,
5603 window,
5604 cx,
5605 );
5606 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5607 s.select_display_ranges([
5608 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5609 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5610 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5611 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
5612 ])
5613 });
5614 assert_eq!(
5615 editor.display_text(cx),
5616 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
5617 );
5618
5619 editor.move_line_up(&MoveLineUp, window, cx);
5620 assert_eq!(
5621 editor.display_text(cx),
5622 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
5623 );
5624 assert_eq!(
5625 editor.selections.display_ranges(cx),
5626 vec![
5627 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5628 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5629 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5630 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5631 ]
5632 );
5633 });
5634
5635 _ = editor.update(cx, |editor, window, cx| {
5636 editor.move_line_down(&MoveLineDown, window, cx);
5637 assert_eq!(
5638 editor.display_text(cx),
5639 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
5640 );
5641 assert_eq!(
5642 editor.selections.display_ranges(cx),
5643 vec![
5644 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5645 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5646 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5647 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5648 ]
5649 );
5650 });
5651
5652 _ = editor.update(cx, |editor, window, cx| {
5653 editor.move_line_down(&MoveLineDown, window, cx);
5654 assert_eq!(
5655 editor.display_text(cx),
5656 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
5657 );
5658 assert_eq!(
5659 editor.selections.display_ranges(cx),
5660 vec![
5661 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5662 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5663 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5664 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5665 ]
5666 );
5667 });
5668
5669 _ = editor.update(cx, |editor, window, cx| {
5670 editor.move_line_up(&MoveLineUp, window, cx);
5671 assert_eq!(
5672 editor.display_text(cx),
5673 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
5674 );
5675 assert_eq!(
5676 editor.selections.display_ranges(cx),
5677 vec![
5678 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5679 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5680 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5681 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5682 ]
5683 );
5684 });
5685}
5686
5687#[gpui::test]
5688fn test_move_line_up_selection_at_end_of_fold(cx: &mut TestAppContext) {
5689 init_test(cx, |_| {});
5690 let editor = cx.add_window(|window, cx| {
5691 let buffer = MultiBuffer::build_simple("\n\n\n\n\n\naaaa\nbbbb\ncccc", cx);
5692 build_editor(buffer, window, cx)
5693 });
5694 _ = editor.update(cx, |editor, window, cx| {
5695 editor.fold_creases(
5696 vec![Crease::simple(
5697 Point::new(6, 4)..Point::new(7, 4),
5698 FoldPlaceholder::test(),
5699 )],
5700 true,
5701 window,
5702 cx,
5703 );
5704 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5705 s.select_ranges([Point::new(7, 4)..Point::new(7, 4)])
5706 });
5707 assert_eq!(editor.display_text(cx), "\n\n\n\n\n\naaaa⋯\ncccc");
5708 editor.move_line_up(&MoveLineUp, window, cx);
5709 let buffer_text = editor.buffer.read(cx).snapshot(cx).text();
5710 assert_eq!(buffer_text, "\n\n\n\n\naaaa\nbbbb\n\ncccc");
5711 });
5712}
5713
5714#[gpui::test]
5715fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
5716 init_test(cx, |_| {});
5717
5718 let editor = cx.add_window(|window, cx| {
5719 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5720 build_editor(buffer, window, cx)
5721 });
5722 _ = editor.update(cx, |editor, window, cx| {
5723 let snapshot = editor.buffer.read(cx).snapshot(cx);
5724 editor.insert_blocks(
5725 [BlockProperties {
5726 style: BlockStyle::Fixed,
5727 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
5728 height: Some(1),
5729 render: Arc::new(|_| div().into_any()),
5730 priority: 0,
5731 }],
5732 Some(Autoscroll::fit()),
5733 cx,
5734 );
5735 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5736 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
5737 });
5738 editor.move_line_down(&MoveLineDown, window, cx);
5739 });
5740}
5741
5742#[gpui::test]
5743async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
5744 init_test(cx, |_| {});
5745
5746 let mut cx = EditorTestContext::new(cx).await;
5747 cx.set_state(
5748 &"
5749 ˇzero
5750 one
5751 two
5752 three
5753 four
5754 five
5755 "
5756 .unindent(),
5757 );
5758
5759 // Create a four-line block that replaces three lines of text.
5760 cx.update_editor(|editor, window, cx| {
5761 let snapshot = editor.snapshot(window, cx);
5762 let snapshot = &snapshot.buffer_snapshot();
5763 let placement = BlockPlacement::Replace(
5764 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
5765 );
5766 editor.insert_blocks(
5767 [BlockProperties {
5768 placement,
5769 height: Some(4),
5770 style: BlockStyle::Sticky,
5771 render: Arc::new(|_| gpui::div().into_any_element()),
5772 priority: 0,
5773 }],
5774 None,
5775 cx,
5776 );
5777 });
5778
5779 // Move down so that the cursor touches the block.
5780 cx.update_editor(|editor, window, cx| {
5781 editor.move_down(&Default::default(), window, cx);
5782 });
5783 cx.assert_editor_state(
5784 &"
5785 zero
5786 «one
5787 two
5788 threeˇ»
5789 four
5790 five
5791 "
5792 .unindent(),
5793 );
5794
5795 // Move down past the block.
5796 cx.update_editor(|editor, window, cx| {
5797 editor.move_down(&Default::default(), window, cx);
5798 });
5799 cx.assert_editor_state(
5800 &"
5801 zero
5802 one
5803 two
5804 three
5805 ˇfour
5806 five
5807 "
5808 .unindent(),
5809 );
5810}
5811
5812#[gpui::test]
5813fn test_transpose(cx: &mut TestAppContext) {
5814 init_test(cx, |_| {});
5815
5816 _ = cx.add_window(|window, cx| {
5817 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
5818 editor.set_style(EditorStyle::default(), window, cx);
5819 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5820 s.select_ranges([1..1])
5821 });
5822 editor.transpose(&Default::default(), window, cx);
5823 assert_eq!(editor.text(cx), "bac");
5824 assert_eq!(editor.selections.ranges(cx), [2..2]);
5825
5826 editor.transpose(&Default::default(), window, cx);
5827 assert_eq!(editor.text(cx), "bca");
5828 assert_eq!(editor.selections.ranges(cx), [3..3]);
5829
5830 editor.transpose(&Default::default(), window, cx);
5831 assert_eq!(editor.text(cx), "bac");
5832 assert_eq!(editor.selections.ranges(cx), [3..3]);
5833
5834 editor
5835 });
5836
5837 _ = cx.add_window(|window, cx| {
5838 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5839 editor.set_style(EditorStyle::default(), window, cx);
5840 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5841 s.select_ranges([3..3])
5842 });
5843 editor.transpose(&Default::default(), window, cx);
5844 assert_eq!(editor.text(cx), "acb\nde");
5845 assert_eq!(editor.selections.ranges(cx), [3..3]);
5846
5847 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5848 s.select_ranges([4..4])
5849 });
5850 editor.transpose(&Default::default(), window, cx);
5851 assert_eq!(editor.text(cx), "acbd\ne");
5852 assert_eq!(editor.selections.ranges(cx), [5..5]);
5853
5854 editor.transpose(&Default::default(), window, cx);
5855 assert_eq!(editor.text(cx), "acbde\n");
5856 assert_eq!(editor.selections.ranges(cx), [6..6]);
5857
5858 editor.transpose(&Default::default(), window, cx);
5859 assert_eq!(editor.text(cx), "acbd\ne");
5860 assert_eq!(editor.selections.ranges(cx), [6..6]);
5861
5862 editor
5863 });
5864
5865 _ = cx.add_window(|window, cx| {
5866 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5867 editor.set_style(EditorStyle::default(), window, cx);
5868 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5869 s.select_ranges([1..1, 2..2, 4..4])
5870 });
5871 editor.transpose(&Default::default(), window, cx);
5872 assert_eq!(editor.text(cx), "bacd\ne");
5873 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
5874
5875 editor.transpose(&Default::default(), window, cx);
5876 assert_eq!(editor.text(cx), "bcade\n");
5877 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
5878
5879 editor.transpose(&Default::default(), window, cx);
5880 assert_eq!(editor.text(cx), "bcda\ne");
5881 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5882
5883 editor.transpose(&Default::default(), window, cx);
5884 assert_eq!(editor.text(cx), "bcade\n");
5885 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5886
5887 editor.transpose(&Default::default(), window, cx);
5888 assert_eq!(editor.text(cx), "bcaed\n");
5889 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
5890
5891 editor
5892 });
5893
5894 _ = cx.add_window(|window, cx| {
5895 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
5896 editor.set_style(EditorStyle::default(), window, cx);
5897 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5898 s.select_ranges([4..4])
5899 });
5900 editor.transpose(&Default::default(), window, cx);
5901 assert_eq!(editor.text(cx), "🏀🍐✋");
5902 assert_eq!(editor.selections.ranges(cx), [8..8]);
5903
5904 editor.transpose(&Default::default(), window, cx);
5905 assert_eq!(editor.text(cx), "🏀✋🍐");
5906 assert_eq!(editor.selections.ranges(cx), [11..11]);
5907
5908 editor.transpose(&Default::default(), window, cx);
5909 assert_eq!(editor.text(cx), "🏀🍐✋");
5910 assert_eq!(editor.selections.ranges(cx), [11..11]);
5911
5912 editor
5913 });
5914}
5915
5916#[gpui::test]
5917async fn test_rewrap(cx: &mut TestAppContext) {
5918 init_test(cx, |settings| {
5919 settings.languages.0.extend([
5920 (
5921 "Markdown".into(),
5922 LanguageSettingsContent {
5923 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5924 preferred_line_length: Some(40),
5925 ..Default::default()
5926 },
5927 ),
5928 (
5929 "Plain Text".into(),
5930 LanguageSettingsContent {
5931 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5932 preferred_line_length: Some(40),
5933 ..Default::default()
5934 },
5935 ),
5936 (
5937 "C++".into(),
5938 LanguageSettingsContent {
5939 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5940 preferred_line_length: Some(40),
5941 ..Default::default()
5942 },
5943 ),
5944 (
5945 "Python".into(),
5946 LanguageSettingsContent {
5947 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5948 preferred_line_length: Some(40),
5949 ..Default::default()
5950 },
5951 ),
5952 (
5953 "Rust".into(),
5954 LanguageSettingsContent {
5955 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5956 preferred_line_length: Some(40),
5957 ..Default::default()
5958 },
5959 ),
5960 ])
5961 });
5962
5963 let mut cx = EditorTestContext::new(cx).await;
5964
5965 let cpp_language = Arc::new(Language::new(
5966 LanguageConfig {
5967 name: "C++".into(),
5968 line_comments: vec!["// ".into()],
5969 ..LanguageConfig::default()
5970 },
5971 None,
5972 ));
5973 let python_language = Arc::new(Language::new(
5974 LanguageConfig {
5975 name: "Python".into(),
5976 line_comments: vec!["# ".into()],
5977 ..LanguageConfig::default()
5978 },
5979 None,
5980 ));
5981 let markdown_language = Arc::new(Language::new(
5982 LanguageConfig {
5983 name: "Markdown".into(),
5984 rewrap_prefixes: vec![
5985 regex::Regex::new("\\d+\\.\\s+").unwrap(),
5986 regex::Regex::new("[-*+]\\s+").unwrap(),
5987 ],
5988 ..LanguageConfig::default()
5989 },
5990 None,
5991 ));
5992 let rust_language = Arc::new(
5993 Language::new(
5994 LanguageConfig {
5995 name: "Rust".into(),
5996 line_comments: vec!["// ".into(), "/// ".into()],
5997 ..LanguageConfig::default()
5998 },
5999 Some(tree_sitter_rust::LANGUAGE.into()),
6000 )
6001 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
6002 .unwrap(),
6003 );
6004
6005 let plaintext_language = Arc::new(Language::new(
6006 LanguageConfig {
6007 name: "Plain Text".into(),
6008 ..LanguageConfig::default()
6009 },
6010 None,
6011 ));
6012
6013 // Test basic rewrapping of a long line with a cursor
6014 assert_rewrap(
6015 indoc! {"
6016 // ˇThis is a long comment that needs to be wrapped.
6017 "},
6018 indoc! {"
6019 // ˇThis is a long comment that needs to
6020 // be wrapped.
6021 "},
6022 cpp_language.clone(),
6023 &mut cx,
6024 );
6025
6026 // Test rewrapping a full selection
6027 assert_rewrap(
6028 indoc! {"
6029 «// This selected long comment needs to be wrapped.ˇ»"
6030 },
6031 indoc! {"
6032 «// This selected long comment needs to
6033 // be wrapped.ˇ»"
6034 },
6035 cpp_language.clone(),
6036 &mut cx,
6037 );
6038
6039 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
6040 assert_rewrap(
6041 indoc! {"
6042 // ˇThis is the first line.
6043 // Thisˇ is the second line.
6044 // This is the thirdˇ line, all part of one paragraph.
6045 "},
6046 indoc! {"
6047 // ˇThis is the first line. Thisˇ is the
6048 // second line. This is the thirdˇ line,
6049 // all part of one paragraph.
6050 "},
6051 cpp_language.clone(),
6052 &mut cx,
6053 );
6054
6055 // Test multiple cursors in different paragraphs trigger separate rewraps
6056 assert_rewrap(
6057 indoc! {"
6058 // ˇThis is the first paragraph, first line.
6059 // ˇThis is the first paragraph, second line.
6060
6061 // ˇThis is the second paragraph, first line.
6062 // ˇThis is the second paragraph, second line.
6063 "},
6064 indoc! {"
6065 // ˇThis is the first paragraph, first
6066 // line. ˇThis is the first paragraph,
6067 // second line.
6068
6069 // ˇThis is the second paragraph, first
6070 // line. ˇThis is the second paragraph,
6071 // second line.
6072 "},
6073 cpp_language.clone(),
6074 &mut cx,
6075 );
6076
6077 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
6078 assert_rewrap(
6079 indoc! {"
6080 «// A regular long long comment to be wrapped.
6081 /// A documentation long comment to be wrapped.ˇ»
6082 "},
6083 indoc! {"
6084 «// A regular long long comment to be
6085 // wrapped.
6086 /// A documentation long comment to be
6087 /// wrapped.ˇ»
6088 "},
6089 rust_language.clone(),
6090 &mut cx,
6091 );
6092
6093 // Test that change in indentation level trigger seperate rewraps
6094 assert_rewrap(
6095 indoc! {"
6096 fn foo() {
6097 «// This is a long comment at the base indent.
6098 // This is a long comment at the next indent.ˇ»
6099 }
6100 "},
6101 indoc! {"
6102 fn foo() {
6103 «// This is a long comment at the
6104 // base indent.
6105 // This is a long comment at the
6106 // next indent.ˇ»
6107 }
6108 "},
6109 rust_language.clone(),
6110 &mut cx,
6111 );
6112
6113 // Test that different comment prefix characters (e.g., '#') are handled correctly
6114 assert_rewrap(
6115 indoc! {"
6116 # ˇThis is a long comment using a pound sign.
6117 "},
6118 indoc! {"
6119 # ˇThis is a long comment using a pound
6120 # sign.
6121 "},
6122 python_language,
6123 &mut cx,
6124 );
6125
6126 // Test rewrapping only affects comments, not code even when selected
6127 assert_rewrap(
6128 indoc! {"
6129 «/// This doc comment is long and should be wrapped.
6130 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6131 "},
6132 indoc! {"
6133 «/// This doc comment is long and should
6134 /// be wrapped.
6135 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6136 "},
6137 rust_language.clone(),
6138 &mut cx,
6139 );
6140
6141 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
6142 assert_rewrap(
6143 indoc! {"
6144 # Header
6145
6146 A long long long line of markdown text to wrap.ˇ
6147 "},
6148 indoc! {"
6149 # Header
6150
6151 A long long long line of markdown text
6152 to wrap.ˇ
6153 "},
6154 markdown_language.clone(),
6155 &mut cx,
6156 );
6157
6158 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
6159 assert_rewrap(
6160 indoc! {"
6161 «1. This is a numbered list item that is very long and needs to be wrapped properly.
6162 2. This is a numbered list item that is very long and needs to be wrapped properly.
6163 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
6164 "},
6165 indoc! {"
6166 «1. This is a numbered list item that is
6167 very long and needs to be wrapped
6168 properly.
6169 2. This is a numbered list item that is
6170 very long and needs to be wrapped
6171 properly.
6172 - This is an unordered list item that is
6173 also very long and should not merge
6174 with the numbered item.ˇ»
6175 "},
6176 markdown_language.clone(),
6177 &mut cx,
6178 );
6179
6180 // Test that rewrapping add indents for rewrapping boundary if not exists already.
6181 assert_rewrap(
6182 indoc! {"
6183 «1. This is a numbered list item that is
6184 very long and needs to be wrapped
6185 properly.
6186 2. This is a numbered list item that is
6187 very long and needs to be wrapped
6188 properly.
6189 - This is an unordered list item that is
6190 also very long and should not merge with
6191 the numbered item.ˇ»
6192 "},
6193 indoc! {"
6194 «1. This is a numbered list item that is
6195 very long and needs to be wrapped
6196 properly.
6197 2. This is a numbered list item that is
6198 very long and needs to be wrapped
6199 properly.
6200 - This is an unordered list item that is
6201 also very long and should not merge
6202 with the numbered item.ˇ»
6203 "},
6204 markdown_language.clone(),
6205 &mut cx,
6206 );
6207
6208 // Test that rewrapping maintain indents even when they already exists.
6209 assert_rewrap(
6210 indoc! {"
6211 «1. This is a numbered list
6212 item that is very long and needs to be wrapped properly.
6213 2. This is a numbered list
6214 item that is very long and needs to be wrapped properly.
6215 - This is an unordered list item that is also very long and
6216 should not merge with the numbered item.ˇ»
6217 "},
6218 indoc! {"
6219 «1. This is a numbered list item that is
6220 very long and needs to be wrapped
6221 properly.
6222 2. This is a numbered list item that is
6223 very long and needs to be wrapped
6224 properly.
6225 - This is an unordered list item that is
6226 also very long and should not merge
6227 with the numbered item.ˇ»
6228 "},
6229 markdown_language,
6230 &mut cx,
6231 );
6232
6233 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
6234 assert_rewrap(
6235 indoc! {"
6236 ˇThis is a very long line of plain text that will be wrapped.
6237 "},
6238 indoc! {"
6239 ˇThis is a very long line of plain text
6240 that will be wrapped.
6241 "},
6242 plaintext_language.clone(),
6243 &mut cx,
6244 );
6245
6246 // Test that non-commented code acts as a paragraph boundary within a selection
6247 assert_rewrap(
6248 indoc! {"
6249 «// This is the first long comment block to be wrapped.
6250 fn my_func(a: u32);
6251 // This is the second long comment block to be wrapped.ˇ»
6252 "},
6253 indoc! {"
6254 «// This is the first long comment block
6255 // to be wrapped.
6256 fn my_func(a: u32);
6257 // This is the second long comment block
6258 // to be wrapped.ˇ»
6259 "},
6260 rust_language,
6261 &mut cx,
6262 );
6263
6264 // Test rewrapping multiple selections, including ones with blank lines or tabs
6265 assert_rewrap(
6266 indoc! {"
6267 «ˇThis is a very long line that will be wrapped.
6268
6269 This is another paragraph in the same selection.»
6270
6271 «\tThis is a very long indented line that will be wrapped.ˇ»
6272 "},
6273 indoc! {"
6274 «ˇThis is a very long line that will be
6275 wrapped.
6276
6277 This is another paragraph in the same
6278 selection.»
6279
6280 «\tThis is a very long indented line
6281 \tthat will be wrapped.ˇ»
6282 "},
6283 plaintext_language,
6284 &mut cx,
6285 );
6286
6287 // Test that an empty comment line acts as a paragraph boundary
6288 assert_rewrap(
6289 indoc! {"
6290 // ˇThis is a long comment that will be wrapped.
6291 //
6292 // And this is another long comment that will also be wrapped.ˇ
6293 "},
6294 indoc! {"
6295 // ˇThis is a long comment that will be
6296 // wrapped.
6297 //
6298 // And this is another long comment that
6299 // will also be wrapped.ˇ
6300 "},
6301 cpp_language,
6302 &mut cx,
6303 );
6304
6305 #[track_caller]
6306 fn assert_rewrap(
6307 unwrapped_text: &str,
6308 wrapped_text: &str,
6309 language: Arc<Language>,
6310 cx: &mut EditorTestContext,
6311 ) {
6312 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6313 cx.set_state(unwrapped_text);
6314 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6315 cx.assert_editor_state(wrapped_text);
6316 }
6317}
6318
6319#[gpui::test]
6320async fn test_rewrap_block_comments(cx: &mut TestAppContext) {
6321 init_test(cx, |settings| {
6322 settings.languages.0.extend([(
6323 "Rust".into(),
6324 LanguageSettingsContent {
6325 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6326 preferred_line_length: Some(40),
6327 ..Default::default()
6328 },
6329 )])
6330 });
6331
6332 let mut cx = EditorTestContext::new(cx).await;
6333
6334 let rust_lang = Arc::new(
6335 Language::new(
6336 LanguageConfig {
6337 name: "Rust".into(),
6338 line_comments: vec!["// ".into()],
6339 block_comment: Some(BlockCommentConfig {
6340 start: "/*".into(),
6341 end: "*/".into(),
6342 prefix: "* ".into(),
6343 tab_size: 1,
6344 }),
6345 documentation_comment: Some(BlockCommentConfig {
6346 start: "/**".into(),
6347 end: "*/".into(),
6348 prefix: "* ".into(),
6349 tab_size: 1,
6350 }),
6351
6352 ..LanguageConfig::default()
6353 },
6354 Some(tree_sitter_rust::LANGUAGE.into()),
6355 )
6356 .with_override_query("[(line_comment) (block_comment)] @comment.inclusive")
6357 .unwrap(),
6358 );
6359
6360 // regular block comment
6361 assert_rewrap(
6362 indoc! {"
6363 /*
6364 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6365 */
6366 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6367 "},
6368 indoc! {"
6369 /*
6370 *ˇ Lorem ipsum dolor sit amet,
6371 * consectetur adipiscing elit.
6372 */
6373 /*
6374 *ˇ Lorem ipsum dolor sit amet,
6375 * consectetur adipiscing elit.
6376 */
6377 "},
6378 rust_lang.clone(),
6379 &mut cx,
6380 );
6381
6382 // indent is respected
6383 assert_rewrap(
6384 indoc! {"
6385 {}
6386 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6387 "},
6388 indoc! {"
6389 {}
6390 /*
6391 *ˇ Lorem ipsum dolor sit amet,
6392 * consectetur adipiscing elit.
6393 */
6394 "},
6395 rust_lang.clone(),
6396 &mut cx,
6397 );
6398
6399 // short block comments with inline delimiters
6400 assert_rewrap(
6401 indoc! {"
6402 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6403 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6404 */
6405 /*
6406 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6407 "},
6408 indoc! {"
6409 /*
6410 *ˇ Lorem ipsum dolor sit amet,
6411 * consectetur adipiscing elit.
6412 */
6413 /*
6414 *ˇ Lorem ipsum dolor sit amet,
6415 * consectetur adipiscing elit.
6416 */
6417 /*
6418 *ˇ Lorem ipsum dolor sit amet,
6419 * consectetur adipiscing elit.
6420 */
6421 "},
6422 rust_lang.clone(),
6423 &mut cx,
6424 );
6425
6426 // multiline block comment with inline start/end delimiters
6427 assert_rewrap(
6428 indoc! {"
6429 /*ˇ Lorem ipsum dolor sit amet,
6430 * consectetur adipiscing elit. */
6431 "},
6432 indoc! {"
6433 /*
6434 *ˇ Lorem ipsum dolor sit amet,
6435 * consectetur adipiscing elit.
6436 */
6437 "},
6438 rust_lang.clone(),
6439 &mut cx,
6440 );
6441
6442 // block comment rewrap still respects paragraph bounds
6443 assert_rewrap(
6444 indoc! {"
6445 /*
6446 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6447 *
6448 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6449 */
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 "},
6459 rust_lang.clone(),
6460 &mut cx,
6461 );
6462
6463 // documentation comments
6464 assert_rewrap(
6465 indoc! {"
6466 /**ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6467 /**
6468 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6469 */
6470 "},
6471 indoc! {"
6472 /**
6473 *ˇ Lorem ipsum dolor sit amet,
6474 * consectetur adipiscing elit.
6475 */
6476 /**
6477 *ˇ Lorem ipsum dolor sit amet,
6478 * consectetur adipiscing elit.
6479 */
6480 "},
6481 rust_lang.clone(),
6482 &mut cx,
6483 );
6484
6485 // different, adjacent comments
6486 assert_rewrap(
6487 indoc! {"
6488 /**
6489 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6490 */
6491 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6492 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6493 "},
6494 indoc! {"
6495 /**
6496 *ˇ Lorem ipsum dolor sit amet,
6497 * consectetur adipiscing elit.
6498 */
6499 /*
6500 *ˇ Lorem ipsum dolor sit amet,
6501 * consectetur adipiscing elit.
6502 */
6503 //ˇ Lorem ipsum dolor sit amet,
6504 // consectetur adipiscing elit.
6505 "},
6506 rust_lang.clone(),
6507 &mut cx,
6508 );
6509
6510 // selection w/ single short block comment
6511 assert_rewrap(
6512 indoc! {"
6513 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6514 "},
6515 indoc! {"
6516 «/*
6517 * Lorem ipsum dolor sit amet,
6518 * consectetur adipiscing elit.
6519 */ˇ»
6520 "},
6521 rust_lang.clone(),
6522 &mut cx,
6523 );
6524
6525 // rewrapping a single comment w/ abutting comments
6526 assert_rewrap(
6527 indoc! {"
6528 /* ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. */
6529 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6530 "},
6531 indoc! {"
6532 /*
6533 * ˇLorem ipsum dolor sit amet,
6534 * consectetur adipiscing elit.
6535 */
6536 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6537 "},
6538 rust_lang.clone(),
6539 &mut cx,
6540 );
6541
6542 // selection w/ non-abutting short block comments
6543 assert_rewrap(
6544 indoc! {"
6545 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6546
6547 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6548 "},
6549 indoc! {"
6550 «/*
6551 * Lorem ipsum dolor sit amet,
6552 * consectetur adipiscing elit.
6553 */
6554
6555 /*
6556 * Lorem ipsum dolor sit amet,
6557 * consectetur adipiscing elit.
6558 */ˇ»
6559 "},
6560 rust_lang.clone(),
6561 &mut cx,
6562 );
6563
6564 // selection of multiline block comments
6565 assert_rewrap(
6566 indoc! {"
6567 «/* Lorem ipsum dolor sit amet,
6568 * consectetur adipiscing elit. */ˇ»
6569 "},
6570 indoc! {"
6571 «/*
6572 * Lorem ipsum dolor sit amet,
6573 * consectetur adipiscing elit.
6574 */ˇ»
6575 "},
6576 rust_lang.clone(),
6577 &mut cx,
6578 );
6579
6580 // partial selection of multiline block comments
6581 assert_rewrap(
6582 indoc! {"
6583 «/* Lorem ipsum dolor sit amet,ˇ»
6584 * consectetur adipiscing elit. */
6585 /* Lorem ipsum dolor sit amet,
6586 «* consectetur adipiscing elit. */ˇ»
6587 "},
6588 indoc! {"
6589 «/*
6590 * Lorem ipsum dolor sit amet,ˇ»
6591 * consectetur adipiscing elit. */
6592 /* Lorem ipsum dolor sit amet,
6593 «* consectetur adipiscing elit.
6594 */ˇ»
6595 "},
6596 rust_lang.clone(),
6597 &mut cx,
6598 );
6599
6600 // selection w/ abutting short block comments
6601 // TODO: should not be combined; should rewrap as 2 comments
6602 assert_rewrap(
6603 indoc! {"
6604 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6605 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6606 "},
6607 // desired behavior:
6608 // indoc! {"
6609 // «/*
6610 // * Lorem ipsum dolor sit amet,
6611 // * consectetur adipiscing elit.
6612 // */
6613 // /*
6614 // * Lorem ipsum dolor sit amet,
6615 // * consectetur adipiscing elit.
6616 // */ˇ»
6617 // "},
6618 // actual behaviour:
6619 indoc! {"
6620 «/*
6621 * Lorem ipsum dolor sit amet,
6622 * consectetur adipiscing elit. Lorem
6623 * ipsum dolor sit amet, consectetur
6624 * adipiscing elit.
6625 */ˇ»
6626 "},
6627 rust_lang.clone(),
6628 &mut cx,
6629 );
6630
6631 // TODO: same as above, but with delimiters on separate line
6632 // assert_rewrap(
6633 // indoc! {"
6634 // «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6635 // */
6636 // /*
6637 // * Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6638 // "},
6639 // // desired:
6640 // // indoc! {"
6641 // // «/*
6642 // // * Lorem ipsum dolor sit amet,
6643 // // * consectetur adipiscing elit.
6644 // // */
6645 // // /*
6646 // // * Lorem ipsum dolor sit amet,
6647 // // * consectetur adipiscing elit.
6648 // // */ˇ»
6649 // // "},
6650 // // actual: (but with trailing w/s on the empty lines)
6651 // indoc! {"
6652 // «/*
6653 // * Lorem ipsum dolor sit amet,
6654 // * consectetur adipiscing elit.
6655 // *
6656 // */
6657 // /*
6658 // *
6659 // * Lorem ipsum dolor sit amet,
6660 // * consectetur adipiscing elit.
6661 // */ˇ»
6662 // "},
6663 // rust_lang.clone(),
6664 // &mut cx,
6665 // );
6666
6667 // TODO these are unhandled edge cases; not correct, just documenting known issues
6668 assert_rewrap(
6669 indoc! {"
6670 /*
6671 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6672 */
6673 /*
6674 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6675 /*ˇ Lorem ipsum dolor sit amet */ /* consectetur adipiscing elit. */
6676 "},
6677 // desired:
6678 // indoc! {"
6679 // /*
6680 // *ˇ Lorem ipsum dolor sit amet,
6681 // * consectetur adipiscing elit.
6682 // */
6683 // /*
6684 // *ˇ Lorem ipsum dolor sit amet,
6685 // * consectetur adipiscing elit.
6686 // */
6687 // /*
6688 // *ˇ Lorem ipsum dolor sit amet
6689 // */ /* consectetur adipiscing elit. */
6690 // "},
6691 // actual:
6692 indoc! {"
6693 /*
6694 //ˇ Lorem ipsum dolor sit amet,
6695 // consectetur adipiscing elit.
6696 */
6697 /*
6698 * //ˇ Lorem ipsum dolor sit amet,
6699 * consectetur adipiscing elit.
6700 */
6701 /*
6702 *ˇ Lorem ipsum dolor sit amet */ /*
6703 * consectetur adipiscing elit.
6704 */
6705 "},
6706 rust_lang,
6707 &mut cx,
6708 );
6709
6710 #[track_caller]
6711 fn assert_rewrap(
6712 unwrapped_text: &str,
6713 wrapped_text: &str,
6714 language: Arc<Language>,
6715 cx: &mut EditorTestContext,
6716 ) {
6717 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6718 cx.set_state(unwrapped_text);
6719 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6720 cx.assert_editor_state(wrapped_text);
6721 }
6722}
6723
6724#[gpui::test]
6725async fn test_hard_wrap(cx: &mut TestAppContext) {
6726 init_test(cx, |_| {});
6727 let mut cx = EditorTestContext::new(cx).await;
6728
6729 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
6730 cx.update_editor(|editor, _, cx| {
6731 editor.set_hard_wrap(Some(14), cx);
6732 });
6733
6734 cx.set_state(indoc!(
6735 "
6736 one two three ˇ
6737 "
6738 ));
6739 cx.simulate_input("four");
6740 cx.run_until_parked();
6741
6742 cx.assert_editor_state(indoc!(
6743 "
6744 one two three
6745 fourˇ
6746 "
6747 ));
6748
6749 cx.update_editor(|editor, window, cx| {
6750 editor.newline(&Default::default(), window, cx);
6751 });
6752 cx.run_until_parked();
6753 cx.assert_editor_state(indoc!(
6754 "
6755 one two three
6756 four
6757 ˇ
6758 "
6759 ));
6760
6761 cx.simulate_input("five");
6762 cx.run_until_parked();
6763 cx.assert_editor_state(indoc!(
6764 "
6765 one two three
6766 four
6767 fiveˇ
6768 "
6769 ));
6770
6771 cx.update_editor(|editor, window, cx| {
6772 editor.newline(&Default::default(), window, cx);
6773 });
6774 cx.run_until_parked();
6775 cx.simulate_input("# ");
6776 cx.run_until_parked();
6777 cx.assert_editor_state(indoc!(
6778 "
6779 one two three
6780 four
6781 five
6782 # ˇ
6783 "
6784 ));
6785
6786 cx.update_editor(|editor, window, cx| {
6787 editor.newline(&Default::default(), window, cx);
6788 });
6789 cx.run_until_parked();
6790 cx.assert_editor_state(indoc!(
6791 "
6792 one two three
6793 four
6794 five
6795 #\x20
6796 #ˇ
6797 "
6798 ));
6799
6800 cx.simulate_input(" 6");
6801 cx.run_until_parked();
6802 cx.assert_editor_state(indoc!(
6803 "
6804 one two three
6805 four
6806 five
6807 #
6808 # 6ˇ
6809 "
6810 ));
6811}
6812
6813#[gpui::test]
6814async fn test_cut_line_ends(cx: &mut TestAppContext) {
6815 init_test(cx, |_| {});
6816
6817 let mut cx = EditorTestContext::new(cx).await;
6818
6819 cx.set_state(indoc! {"The quick brownˇ"});
6820 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
6821 cx.assert_editor_state(indoc! {"The quick brownˇ"});
6822
6823 cx.set_state(indoc! {"The emacs foxˇ"});
6824 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
6825 cx.assert_editor_state(indoc! {"The emacs foxˇ"});
6826
6827 cx.set_state(indoc! {"
6828 The quick« brownˇ»
6829 fox jumps overˇ
6830 the lazy dog"});
6831 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6832 cx.assert_editor_state(indoc! {"
6833 The quickˇ
6834 ˇthe lazy dog"});
6835
6836 cx.set_state(indoc! {"
6837 The quick« brownˇ»
6838 fox jumps overˇ
6839 the lazy dog"});
6840 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
6841 cx.assert_editor_state(indoc! {"
6842 The quickˇ
6843 fox jumps overˇthe lazy dog"});
6844
6845 cx.set_state(indoc! {"
6846 The quick« brownˇ»
6847 fox jumps overˇ
6848 the lazy dog"});
6849 cx.update_editor(|e, window, cx| {
6850 e.cut_to_end_of_line(
6851 &CutToEndOfLine {
6852 stop_at_newlines: true,
6853 },
6854 window,
6855 cx,
6856 )
6857 });
6858 cx.assert_editor_state(indoc! {"
6859 The quickˇ
6860 fox jumps overˇ
6861 the lazy dog"});
6862
6863 cx.set_state(indoc! {"
6864 The quick« brownˇ»
6865 fox jumps overˇ
6866 the lazy dog"});
6867 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
6868 cx.assert_editor_state(indoc! {"
6869 The quickˇ
6870 fox jumps overˇthe lazy dog"});
6871}
6872
6873#[gpui::test]
6874async fn test_clipboard(cx: &mut TestAppContext) {
6875 init_test(cx, |_| {});
6876
6877 let mut cx = EditorTestContext::new(cx).await;
6878
6879 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
6880 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6881 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
6882
6883 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
6884 cx.set_state("two ˇfour ˇsix ˇ");
6885 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6886 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
6887
6888 // Paste again but with only two cursors. Since the number of cursors doesn't
6889 // match the number of slices in the clipboard, the entire clipboard text
6890 // is pasted at each cursor.
6891 cx.set_state("ˇtwo one✅ four three six five ˇ");
6892 cx.update_editor(|e, window, cx| {
6893 e.handle_input("( ", window, cx);
6894 e.paste(&Paste, window, cx);
6895 e.handle_input(") ", window, cx);
6896 });
6897 cx.assert_editor_state(
6898 &([
6899 "( one✅ ",
6900 "three ",
6901 "five ) ˇtwo one✅ four three six five ( one✅ ",
6902 "three ",
6903 "five ) ˇ",
6904 ]
6905 .join("\n")),
6906 );
6907
6908 // Cut with three selections, one of which is full-line.
6909 cx.set_state(indoc! {"
6910 1«2ˇ»3
6911 4ˇ567
6912 «8ˇ»9"});
6913 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6914 cx.assert_editor_state(indoc! {"
6915 1ˇ3
6916 ˇ9"});
6917
6918 // Paste with three selections, noticing how the copied selection that was full-line
6919 // gets inserted before the second cursor.
6920 cx.set_state(indoc! {"
6921 1ˇ3
6922 9ˇ
6923 «oˇ»ne"});
6924 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6925 cx.assert_editor_state(indoc! {"
6926 12ˇ3
6927 4567
6928 9ˇ
6929 8ˇne"});
6930
6931 // Copy with a single cursor only, which writes the whole line into the clipboard.
6932 cx.set_state(indoc! {"
6933 The quick brown
6934 fox juˇmps over
6935 the lazy dog"});
6936 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6937 assert_eq!(
6938 cx.read_from_clipboard()
6939 .and_then(|item| item.text().as_deref().map(str::to_string)),
6940 Some("fox jumps over\n".to_string())
6941 );
6942
6943 // Paste with three selections, noticing how the copied full-line selection is inserted
6944 // before the empty selections but replaces the selection that is non-empty.
6945 cx.set_state(indoc! {"
6946 Tˇhe quick brown
6947 «foˇ»x jumps over
6948 tˇhe lazy dog"});
6949 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6950 cx.assert_editor_state(indoc! {"
6951 fox jumps over
6952 Tˇhe quick brown
6953 fox jumps over
6954 ˇx jumps over
6955 fox jumps over
6956 tˇhe lazy dog"});
6957}
6958
6959#[gpui::test]
6960async fn test_copy_trim(cx: &mut TestAppContext) {
6961 init_test(cx, |_| {});
6962
6963 let mut cx = EditorTestContext::new(cx).await;
6964 cx.set_state(
6965 r#" «for selection in selections.iter() {
6966 let mut start = selection.start;
6967 let mut end = selection.end;
6968 let is_entire_line = selection.is_empty();
6969 if is_entire_line {
6970 start = Point::new(start.row, 0);ˇ»
6971 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6972 }
6973 "#,
6974 );
6975 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6976 assert_eq!(
6977 cx.read_from_clipboard()
6978 .and_then(|item| item.text().as_deref().map(str::to_string)),
6979 Some(
6980 "for selection in selections.iter() {
6981 let mut start = selection.start;
6982 let mut end = selection.end;
6983 let is_entire_line = selection.is_empty();
6984 if is_entire_line {
6985 start = Point::new(start.row, 0);"
6986 .to_string()
6987 ),
6988 "Regular copying preserves all indentation selected",
6989 );
6990 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6991 assert_eq!(
6992 cx.read_from_clipboard()
6993 .and_then(|item| item.text().as_deref().map(str::to_string)),
6994 Some(
6995 "for selection in selections.iter() {
6996let mut start = selection.start;
6997let mut end = selection.end;
6998let is_entire_line = selection.is_empty();
6999if is_entire_line {
7000 start = Point::new(start.row, 0);"
7001 .to_string()
7002 ),
7003 "Copying with stripping should strip all leading whitespaces"
7004 );
7005
7006 cx.set_state(
7007 r#" « for selection in selections.iter() {
7008 let mut start = selection.start;
7009 let mut end = selection.end;
7010 let is_entire_line = selection.is_empty();
7011 if is_entire_line {
7012 start = Point::new(start.row, 0);ˇ»
7013 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7014 }
7015 "#,
7016 );
7017 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7018 assert_eq!(
7019 cx.read_from_clipboard()
7020 .and_then(|item| item.text().as_deref().map(str::to_string)),
7021 Some(
7022 " for selection in selections.iter() {
7023 let mut start = selection.start;
7024 let mut end = selection.end;
7025 let is_entire_line = selection.is_empty();
7026 if is_entire_line {
7027 start = Point::new(start.row, 0);"
7028 .to_string()
7029 ),
7030 "Regular copying preserves all indentation selected",
7031 );
7032 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7033 assert_eq!(
7034 cx.read_from_clipboard()
7035 .and_then(|item| item.text().as_deref().map(str::to_string)),
7036 Some(
7037 "for selection in selections.iter() {
7038let mut start = selection.start;
7039let mut end = selection.end;
7040let is_entire_line = selection.is_empty();
7041if is_entire_line {
7042 start = Point::new(start.row, 0);"
7043 .to_string()
7044 ),
7045 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
7046 );
7047
7048 cx.set_state(
7049 r#" «ˇ for selection in selections.iter() {
7050 let mut start = selection.start;
7051 let mut end = selection.end;
7052 let is_entire_line = selection.is_empty();
7053 if is_entire_line {
7054 start = Point::new(start.row, 0);»
7055 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7056 }
7057 "#,
7058 );
7059 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7060 assert_eq!(
7061 cx.read_from_clipboard()
7062 .and_then(|item| item.text().as_deref().map(str::to_string)),
7063 Some(
7064 " for selection in selections.iter() {
7065 let mut start = selection.start;
7066 let mut end = selection.end;
7067 let is_entire_line = selection.is_empty();
7068 if is_entire_line {
7069 start = Point::new(start.row, 0);"
7070 .to_string()
7071 ),
7072 "Regular copying for reverse selection works the same",
7073 );
7074 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7075 assert_eq!(
7076 cx.read_from_clipboard()
7077 .and_then(|item| item.text().as_deref().map(str::to_string)),
7078 Some(
7079 "for selection in selections.iter() {
7080let mut start = selection.start;
7081let mut end = selection.end;
7082let is_entire_line = selection.is_empty();
7083if is_entire_line {
7084 start = Point::new(start.row, 0);"
7085 .to_string()
7086 ),
7087 "Copying with stripping for reverse selection works the same"
7088 );
7089
7090 cx.set_state(
7091 r#" for selection «in selections.iter() {
7092 let mut start = selection.start;
7093 let mut end = selection.end;
7094 let is_entire_line = selection.is_empty();
7095 if is_entire_line {
7096 start = Point::new(start.row, 0);ˇ»
7097 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7098 }
7099 "#,
7100 );
7101 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7102 assert_eq!(
7103 cx.read_from_clipboard()
7104 .and_then(|item| item.text().as_deref().map(str::to_string)),
7105 Some(
7106 "in selections.iter() {
7107 let mut start = selection.start;
7108 let mut end = selection.end;
7109 let is_entire_line = selection.is_empty();
7110 if is_entire_line {
7111 start = Point::new(start.row, 0);"
7112 .to_string()
7113 ),
7114 "When selecting past the indent, the copying works as usual",
7115 );
7116 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7117 assert_eq!(
7118 cx.read_from_clipboard()
7119 .and_then(|item| item.text().as_deref().map(str::to_string)),
7120 Some(
7121 "in selections.iter() {
7122 let mut start = selection.start;
7123 let mut end = selection.end;
7124 let is_entire_line = selection.is_empty();
7125 if is_entire_line {
7126 start = Point::new(start.row, 0);"
7127 .to_string()
7128 ),
7129 "When selecting past the indent, nothing is trimmed"
7130 );
7131
7132 cx.set_state(
7133 r#" «for selection in selections.iter() {
7134 let mut start = selection.start;
7135
7136 let mut end = selection.end;
7137 let is_entire_line = selection.is_empty();
7138 if is_entire_line {
7139 start = Point::new(start.row, 0);
7140ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
7141 }
7142 "#,
7143 );
7144 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7145 assert_eq!(
7146 cx.read_from_clipboard()
7147 .and_then(|item| item.text().as_deref().map(str::to_string)),
7148 Some(
7149 "for selection in selections.iter() {
7150let mut start = selection.start;
7151
7152let mut end = selection.end;
7153let is_entire_line = selection.is_empty();
7154if is_entire_line {
7155 start = Point::new(start.row, 0);
7156"
7157 .to_string()
7158 ),
7159 "Copying with stripping should ignore empty lines"
7160 );
7161}
7162
7163#[gpui::test]
7164async fn test_paste_multiline(cx: &mut TestAppContext) {
7165 init_test(cx, |_| {});
7166
7167 let mut cx = EditorTestContext::new(cx).await;
7168 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7169
7170 // Cut an indented block, without the leading whitespace.
7171 cx.set_state(indoc! {"
7172 const a: B = (
7173 c(),
7174 «d(
7175 e,
7176 f
7177 )ˇ»
7178 );
7179 "});
7180 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7181 cx.assert_editor_state(indoc! {"
7182 const a: B = (
7183 c(),
7184 ˇ
7185 );
7186 "});
7187
7188 // Paste it at the same position.
7189 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7190 cx.assert_editor_state(indoc! {"
7191 const a: B = (
7192 c(),
7193 d(
7194 e,
7195 f
7196 )ˇ
7197 );
7198 "});
7199
7200 // Paste it at a line with a lower indent level.
7201 cx.set_state(indoc! {"
7202 ˇ
7203 const a: B = (
7204 c(),
7205 );
7206 "});
7207 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7208 cx.assert_editor_state(indoc! {"
7209 d(
7210 e,
7211 f
7212 )ˇ
7213 const a: B = (
7214 c(),
7215 );
7216 "});
7217
7218 // Cut an indented block, with the leading whitespace.
7219 cx.set_state(indoc! {"
7220 const a: B = (
7221 c(),
7222 « d(
7223 e,
7224 f
7225 )
7226 ˇ»);
7227 "});
7228 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7229 cx.assert_editor_state(indoc! {"
7230 const a: B = (
7231 c(),
7232 ˇ);
7233 "});
7234
7235 // Paste it at the same position.
7236 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7237 cx.assert_editor_state(indoc! {"
7238 const a: B = (
7239 c(),
7240 d(
7241 e,
7242 f
7243 )
7244 ˇ);
7245 "});
7246
7247 // Paste it at a line with a higher indent level.
7248 cx.set_state(indoc! {"
7249 const a: B = (
7250 c(),
7251 d(
7252 e,
7253 fˇ
7254 )
7255 );
7256 "});
7257 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7258 cx.assert_editor_state(indoc! {"
7259 const a: B = (
7260 c(),
7261 d(
7262 e,
7263 f d(
7264 e,
7265 f
7266 )
7267 ˇ
7268 )
7269 );
7270 "});
7271
7272 // Copy an indented block, starting mid-line
7273 cx.set_state(indoc! {"
7274 const a: B = (
7275 c(),
7276 somethin«g(
7277 e,
7278 f
7279 )ˇ»
7280 );
7281 "});
7282 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7283
7284 // Paste it on a line with a lower indent level
7285 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
7286 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7287 cx.assert_editor_state(indoc! {"
7288 const a: B = (
7289 c(),
7290 something(
7291 e,
7292 f
7293 )
7294 );
7295 g(
7296 e,
7297 f
7298 )ˇ"});
7299}
7300
7301#[gpui::test]
7302async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
7303 init_test(cx, |_| {});
7304
7305 cx.write_to_clipboard(ClipboardItem::new_string(
7306 " d(\n e\n );\n".into(),
7307 ));
7308
7309 let mut cx = EditorTestContext::new(cx).await;
7310 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7311
7312 cx.set_state(indoc! {"
7313 fn a() {
7314 b();
7315 if c() {
7316 ˇ
7317 }
7318 }
7319 "});
7320
7321 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7322 cx.assert_editor_state(indoc! {"
7323 fn a() {
7324 b();
7325 if c() {
7326 d(
7327 e
7328 );
7329 ˇ
7330 }
7331 }
7332 "});
7333
7334 cx.set_state(indoc! {"
7335 fn a() {
7336 b();
7337 ˇ
7338 }
7339 "});
7340
7341 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7342 cx.assert_editor_state(indoc! {"
7343 fn a() {
7344 b();
7345 d(
7346 e
7347 );
7348 ˇ
7349 }
7350 "});
7351}
7352
7353#[gpui::test]
7354fn test_select_all(cx: &mut TestAppContext) {
7355 init_test(cx, |_| {});
7356
7357 let editor = cx.add_window(|window, cx| {
7358 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
7359 build_editor(buffer, window, cx)
7360 });
7361 _ = editor.update(cx, |editor, window, cx| {
7362 editor.select_all(&SelectAll, window, cx);
7363 assert_eq!(
7364 editor.selections.display_ranges(cx),
7365 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
7366 );
7367 });
7368}
7369
7370#[gpui::test]
7371fn test_select_line(cx: &mut TestAppContext) {
7372 init_test(cx, |_| {});
7373
7374 let editor = cx.add_window(|window, cx| {
7375 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
7376 build_editor(buffer, window, cx)
7377 });
7378 _ = editor.update(cx, |editor, window, cx| {
7379 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7380 s.select_display_ranges([
7381 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7382 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7383 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7384 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
7385 ])
7386 });
7387 editor.select_line(&SelectLine, window, cx);
7388 assert_eq!(
7389 editor.selections.display_ranges(cx),
7390 vec![
7391 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
7392 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
7393 ]
7394 );
7395 });
7396
7397 _ = editor.update(cx, |editor, window, cx| {
7398 editor.select_line(&SelectLine, window, cx);
7399 assert_eq!(
7400 editor.selections.display_ranges(cx),
7401 vec![
7402 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
7403 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
7404 ]
7405 );
7406 });
7407
7408 _ = editor.update(cx, |editor, window, cx| {
7409 editor.select_line(&SelectLine, window, cx);
7410 assert_eq!(
7411 editor.selections.display_ranges(cx),
7412 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
7413 );
7414 });
7415}
7416
7417#[gpui::test]
7418async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
7419 init_test(cx, |_| {});
7420 let mut cx = EditorTestContext::new(cx).await;
7421
7422 #[track_caller]
7423 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
7424 cx.set_state(initial_state);
7425 cx.update_editor(|e, window, cx| {
7426 e.split_selection_into_lines(&Default::default(), window, cx)
7427 });
7428 cx.assert_editor_state(expected_state);
7429 }
7430
7431 // Selection starts and ends at the middle of lines, left-to-right
7432 test(
7433 &mut cx,
7434 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
7435 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7436 );
7437 // Same thing, right-to-left
7438 test(
7439 &mut cx,
7440 "aa\nb«b\ncc\ndd\neˇ»e\nff",
7441 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7442 );
7443
7444 // Whole buffer, left-to-right, last line *doesn't* end with newline
7445 test(
7446 &mut cx,
7447 "«ˇaa\nbb\ncc\ndd\nee\nff»",
7448 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7449 );
7450 // Same thing, right-to-left
7451 test(
7452 &mut cx,
7453 "«aa\nbb\ncc\ndd\nee\nffˇ»",
7454 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7455 );
7456
7457 // Whole buffer, left-to-right, last line ends with newline
7458 test(
7459 &mut cx,
7460 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
7461 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7462 );
7463 // Same thing, right-to-left
7464 test(
7465 &mut cx,
7466 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
7467 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7468 );
7469
7470 // Starts at the end of a line, ends at the start of another
7471 test(
7472 &mut cx,
7473 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
7474 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
7475 );
7476}
7477
7478#[gpui::test]
7479async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
7480 init_test(cx, |_| {});
7481
7482 let editor = cx.add_window(|window, cx| {
7483 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
7484 build_editor(buffer, window, cx)
7485 });
7486
7487 // setup
7488 _ = editor.update(cx, |editor, window, cx| {
7489 editor.fold_creases(
7490 vec![
7491 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
7492 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
7493 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
7494 ],
7495 true,
7496 window,
7497 cx,
7498 );
7499 assert_eq!(
7500 editor.display_text(cx),
7501 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7502 );
7503 });
7504
7505 _ = editor.update(cx, |editor, window, cx| {
7506 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7507 s.select_display_ranges([
7508 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7509 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7510 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7511 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
7512 ])
7513 });
7514 editor.split_selection_into_lines(&Default::default(), window, cx);
7515 assert_eq!(
7516 editor.display_text(cx),
7517 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7518 );
7519 });
7520 EditorTestContext::for_editor(editor, cx)
7521 .await
7522 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
7523
7524 _ = editor.update(cx, |editor, window, cx| {
7525 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7526 s.select_display_ranges([
7527 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
7528 ])
7529 });
7530 editor.split_selection_into_lines(&Default::default(), window, cx);
7531 assert_eq!(
7532 editor.display_text(cx),
7533 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
7534 );
7535 assert_eq!(
7536 editor.selections.display_ranges(cx),
7537 [
7538 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
7539 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
7540 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
7541 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
7542 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
7543 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
7544 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
7545 ]
7546 );
7547 });
7548 EditorTestContext::for_editor(editor, cx)
7549 .await
7550 .assert_editor_state(
7551 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
7552 );
7553}
7554
7555#[gpui::test]
7556async fn test_add_selection_above_below(cx: &mut TestAppContext) {
7557 init_test(cx, |_| {});
7558
7559 let mut cx = EditorTestContext::new(cx).await;
7560
7561 cx.set_state(indoc!(
7562 r#"abc
7563 defˇghi
7564
7565 jk
7566 nlmo
7567 "#
7568 ));
7569
7570 cx.update_editor(|editor, window, cx| {
7571 editor.add_selection_above(&Default::default(), window, cx);
7572 });
7573
7574 cx.assert_editor_state(indoc!(
7575 r#"abcˇ
7576 defˇghi
7577
7578 jk
7579 nlmo
7580 "#
7581 ));
7582
7583 cx.update_editor(|editor, window, cx| {
7584 editor.add_selection_above(&Default::default(), window, cx);
7585 });
7586
7587 cx.assert_editor_state(indoc!(
7588 r#"abcˇ
7589 defˇghi
7590
7591 jk
7592 nlmo
7593 "#
7594 ));
7595
7596 cx.update_editor(|editor, window, cx| {
7597 editor.add_selection_below(&Default::default(), window, cx);
7598 });
7599
7600 cx.assert_editor_state(indoc!(
7601 r#"abc
7602 defˇghi
7603
7604 jk
7605 nlmo
7606 "#
7607 ));
7608
7609 cx.update_editor(|editor, window, cx| {
7610 editor.undo_selection(&Default::default(), window, cx);
7611 });
7612
7613 cx.assert_editor_state(indoc!(
7614 r#"abcˇ
7615 defˇghi
7616
7617 jk
7618 nlmo
7619 "#
7620 ));
7621
7622 cx.update_editor(|editor, window, cx| {
7623 editor.redo_selection(&Default::default(), window, cx);
7624 });
7625
7626 cx.assert_editor_state(indoc!(
7627 r#"abc
7628 defˇghi
7629
7630 jk
7631 nlmo
7632 "#
7633 ));
7634
7635 cx.update_editor(|editor, window, cx| {
7636 editor.add_selection_below(&Default::default(), window, cx);
7637 });
7638
7639 cx.assert_editor_state(indoc!(
7640 r#"abc
7641 defˇghi
7642 ˇ
7643 jk
7644 nlmo
7645 "#
7646 ));
7647
7648 cx.update_editor(|editor, window, cx| {
7649 editor.add_selection_below(&Default::default(), window, cx);
7650 });
7651
7652 cx.assert_editor_state(indoc!(
7653 r#"abc
7654 defˇghi
7655 ˇ
7656 jkˇ
7657 nlmo
7658 "#
7659 ));
7660
7661 cx.update_editor(|editor, window, cx| {
7662 editor.add_selection_below(&Default::default(), window, cx);
7663 });
7664
7665 cx.assert_editor_state(indoc!(
7666 r#"abc
7667 defˇghi
7668 ˇ
7669 jkˇ
7670 nlmˇo
7671 "#
7672 ));
7673
7674 cx.update_editor(|editor, window, cx| {
7675 editor.add_selection_below(&Default::default(), window, cx);
7676 });
7677
7678 cx.assert_editor_state(indoc!(
7679 r#"abc
7680 defˇghi
7681 ˇ
7682 jkˇ
7683 nlmˇo
7684 ˇ"#
7685 ));
7686
7687 // change selections
7688 cx.set_state(indoc!(
7689 r#"abc
7690 def«ˇg»hi
7691
7692 jk
7693 nlmo
7694 "#
7695 ));
7696
7697 cx.update_editor(|editor, window, cx| {
7698 editor.add_selection_below(&Default::default(), window, cx);
7699 });
7700
7701 cx.assert_editor_state(indoc!(
7702 r#"abc
7703 def«ˇg»hi
7704
7705 jk
7706 nlm«ˇo»
7707 "#
7708 ));
7709
7710 cx.update_editor(|editor, window, cx| {
7711 editor.add_selection_below(&Default::default(), window, cx);
7712 });
7713
7714 cx.assert_editor_state(indoc!(
7715 r#"abc
7716 def«ˇg»hi
7717
7718 jk
7719 nlm«ˇo»
7720 "#
7721 ));
7722
7723 cx.update_editor(|editor, window, cx| {
7724 editor.add_selection_above(&Default::default(), window, cx);
7725 });
7726
7727 cx.assert_editor_state(indoc!(
7728 r#"abc
7729 def«ˇg»hi
7730
7731 jk
7732 nlmo
7733 "#
7734 ));
7735
7736 cx.update_editor(|editor, window, cx| {
7737 editor.add_selection_above(&Default::default(), window, cx);
7738 });
7739
7740 cx.assert_editor_state(indoc!(
7741 r#"abc
7742 def«ˇg»hi
7743
7744 jk
7745 nlmo
7746 "#
7747 ));
7748
7749 // Change selections again
7750 cx.set_state(indoc!(
7751 r#"a«bc
7752 defgˇ»hi
7753
7754 jk
7755 nlmo
7756 "#
7757 ));
7758
7759 cx.update_editor(|editor, window, cx| {
7760 editor.add_selection_below(&Default::default(), window, cx);
7761 });
7762
7763 cx.assert_editor_state(indoc!(
7764 r#"a«bcˇ»
7765 d«efgˇ»hi
7766
7767 j«kˇ»
7768 nlmo
7769 "#
7770 ));
7771
7772 cx.update_editor(|editor, window, cx| {
7773 editor.add_selection_below(&Default::default(), window, cx);
7774 });
7775 cx.assert_editor_state(indoc!(
7776 r#"a«bcˇ»
7777 d«efgˇ»hi
7778
7779 j«kˇ»
7780 n«lmoˇ»
7781 "#
7782 ));
7783 cx.update_editor(|editor, window, cx| {
7784 editor.add_selection_above(&Default::default(), window, cx);
7785 });
7786
7787 cx.assert_editor_state(indoc!(
7788 r#"a«bcˇ»
7789 d«efgˇ»hi
7790
7791 j«kˇ»
7792 nlmo
7793 "#
7794 ));
7795
7796 // Change selections again
7797 cx.set_state(indoc!(
7798 r#"abc
7799 d«ˇefghi
7800
7801 jk
7802 nlm»o
7803 "#
7804 ));
7805
7806 cx.update_editor(|editor, window, cx| {
7807 editor.add_selection_above(&Default::default(), window, cx);
7808 });
7809
7810 cx.assert_editor_state(indoc!(
7811 r#"a«ˇbc»
7812 d«ˇef»ghi
7813
7814 j«ˇk»
7815 n«ˇlm»o
7816 "#
7817 ));
7818
7819 cx.update_editor(|editor, window, cx| {
7820 editor.add_selection_below(&Default::default(), window, cx);
7821 });
7822
7823 cx.assert_editor_state(indoc!(
7824 r#"abc
7825 d«ˇef»ghi
7826
7827 j«ˇk»
7828 n«ˇlm»o
7829 "#
7830 ));
7831}
7832
7833#[gpui::test]
7834async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
7835 init_test(cx, |_| {});
7836 let mut cx = EditorTestContext::new(cx).await;
7837
7838 cx.set_state(indoc!(
7839 r#"line onˇe
7840 liˇne two
7841 line three
7842 line four"#
7843 ));
7844
7845 cx.update_editor(|editor, window, cx| {
7846 editor.add_selection_below(&Default::default(), window, cx);
7847 });
7848
7849 // test multiple cursors expand in the same direction
7850 cx.assert_editor_state(indoc!(
7851 r#"line onˇe
7852 liˇne twˇo
7853 liˇne three
7854 line four"#
7855 ));
7856
7857 cx.update_editor(|editor, window, cx| {
7858 editor.add_selection_below(&Default::default(), window, cx);
7859 });
7860
7861 cx.update_editor(|editor, window, cx| {
7862 editor.add_selection_below(&Default::default(), window, cx);
7863 });
7864
7865 // test multiple cursors expand below overflow
7866 cx.assert_editor_state(indoc!(
7867 r#"line onˇe
7868 liˇne twˇo
7869 liˇne thˇree
7870 liˇne foˇur"#
7871 ));
7872
7873 cx.update_editor(|editor, window, cx| {
7874 editor.add_selection_above(&Default::default(), window, cx);
7875 });
7876
7877 // test multiple cursors retrieves back correctly
7878 cx.assert_editor_state(indoc!(
7879 r#"line onˇe
7880 liˇne twˇo
7881 liˇne thˇree
7882 line four"#
7883 ));
7884
7885 cx.update_editor(|editor, window, cx| {
7886 editor.add_selection_above(&Default::default(), window, cx);
7887 });
7888
7889 cx.update_editor(|editor, window, cx| {
7890 editor.add_selection_above(&Default::default(), window, cx);
7891 });
7892
7893 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
7894 cx.assert_editor_state(indoc!(
7895 r#"liˇne onˇe
7896 liˇne two
7897 line three
7898 line four"#
7899 ));
7900
7901 cx.update_editor(|editor, window, cx| {
7902 editor.undo_selection(&Default::default(), window, cx);
7903 });
7904
7905 // test undo
7906 cx.assert_editor_state(indoc!(
7907 r#"line onˇe
7908 liˇne twˇo
7909 line three
7910 line four"#
7911 ));
7912
7913 cx.update_editor(|editor, window, cx| {
7914 editor.redo_selection(&Default::default(), window, cx);
7915 });
7916
7917 // test redo
7918 cx.assert_editor_state(indoc!(
7919 r#"liˇne onˇe
7920 liˇne two
7921 line three
7922 line four"#
7923 ));
7924
7925 cx.set_state(indoc!(
7926 r#"abcd
7927 ef«ghˇ»
7928 ijkl
7929 «mˇ»nop"#
7930 ));
7931
7932 cx.update_editor(|editor, window, cx| {
7933 editor.add_selection_above(&Default::default(), window, cx);
7934 });
7935
7936 // test multiple selections expand in the same direction
7937 cx.assert_editor_state(indoc!(
7938 r#"ab«cdˇ»
7939 ef«ghˇ»
7940 «iˇ»jkl
7941 «mˇ»nop"#
7942 ));
7943
7944 cx.update_editor(|editor, window, cx| {
7945 editor.add_selection_above(&Default::default(), window, cx);
7946 });
7947
7948 // test multiple selection upward overflow
7949 cx.assert_editor_state(indoc!(
7950 r#"ab«cdˇ»
7951 «eˇ»f«ghˇ»
7952 «iˇ»jkl
7953 «mˇ»nop"#
7954 ));
7955
7956 cx.update_editor(|editor, window, cx| {
7957 editor.add_selection_below(&Default::default(), window, cx);
7958 });
7959
7960 // test multiple selection retrieves back correctly
7961 cx.assert_editor_state(indoc!(
7962 r#"abcd
7963 ef«ghˇ»
7964 «iˇ»jkl
7965 «mˇ»nop"#
7966 ));
7967
7968 cx.update_editor(|editor, window, cx| {
7969 editor.add_selection_below(&Default::default(), window, cx);
7970 });
7971
7972 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
7973 cx.assert_editor_state(indoc!(
7974 r#"abcd
7975 ef«ghˇ»
7976 ij«klˇ»
7977 «mˇ»nop"#
7978 ));
7979
7980 cx.update_editor(|editor, window, cx| {
7981 editor.undo_selection(&Default::default(), window, cx);
7982 });
7983
7984 // test undo
7985 cx.assert_editor_state(indoc!(
7986 r#"abcd
7987 ef«ghˇ»
7988 «iˇ»jkl
7989 «mˇ»nop"#
7990 ));
7991
7992 cx.update_editor(|editor, window, cx| {
7993 editor.redo_selection(&Default::default(), window, cx);
7994 });
7995
7996 // test redo
7997 cx.assert_editor_state(indoc!(
7998 r#"abcd
7999 ef«ghˇ»
8000 ij«klˇ»
8001 «mˇ»nop"#
8002 ));
8003}
8004
8005#[gpui::test]
8006async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
8007 init_test(cx, |_| {});
8008 let mut cx = EditorTestContext::new(cx).await;
8009
8010 cx.set_state(indoc!(
8011 r#"line onˇe
8012 liˇne two
8013 line three
8014 line four"#
8015 ));
8016
8017 cx.update_editor(|editor, window, cx| {
8018 editor.add_selection_below(&Default::default(), window, cx);
8019 editor.add_selection_below(&Default::default(), window, cx);
8020 editor.add_selection_below(&Default::default(), window, cx);
8021 });
8022
8023 // initial state with two multi cursor groups
8024 cx.assert_editor_state(indoc!(
8025 r#"line onˇe
8026 liˇne twˇo
8027 liˇne thˇree
8028 liˇne foˇur"#
8029 ));
8030
8031 // add single cursor in middle - simulate opt click
8032 cx.update_editor(|editor, window, cx| {
8033 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
8034 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8035 editor.end_selection(window, cx);
8036 });
8037
8038 cx.assert_editor_state(indoc!(
8039 r#"line onˇe
8040 liˇne twˇo
8041 liˇneˇ thˇree
8042 liˇne foˇur"#
8043 ));
8044
8045 cx.update_editor(|editor, window, cx| {
8046 editor.add_selection_above(&Default::default(), window, cx);
8047 });
8048
8049 // test new added selection expands above and existing selection shrinks
8050 cx.assert_editor_state(indoc!(
8051 r#"line onˇe
8052 liˇneˇ twˇo
8053 liˇneˇ thˇree
8054 line four"#
8055 ));
8056
8057 cx.update_editor(|editor, window, cx| {
8058 editor.add_selection_above(&Default::default(), window, cx);
8059 });
8060
8061 // test new added selection expands above and existing selection shrinks
8062 cx.assert_editor_state(indoc!(
8063 r#"lineˇ onˇe
8064 liˇneˇ twˇo
8065 lineˇ three
8066 line four"#
8067 ));
8068
8069 // intial state with two selection groups
8070 cx.set_state(indoc!(
8071 r#"abcd
8072 ef«ghˇ»
8073 ijkl
8074 «mˇ»nop"#
8075 ));
8076
8077 cx.update_editor(|editor, window, cx| {
8078 editor.add_selection_above(&Default::default(), window, cx);
8079 editor.add_selection_above(&Default::default(), window, cx);
8080 });
8081
8082 cx.assert_editor_state(indoc!(
8083 r#"ab«cdˇ»
8084 «eˇ»f«ghˇ»
8085 «iˇ»jkl
8086 «mˇ»nop"#
8087 ));
8088
8089 // add single selection in middle - simulate opt drag
8090 cx.update_editor(|editor, window, cx| {
8091 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
8092 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8093 editor.update_selection(
8094 DisplayPoint::new(DisplayRow(2), 4),
8095 0,
8096 gpui::Point::<f32>::default(),
8097 window,
8098 cx,
8099 );
8100 editor.end_selection(window, cx);
8101 });
8102
8103 cx.assert_editor_state(indoc!(
8104 r#"ab«cdˇ»
8105 «eˇ»f«ghˇ»
8106 «iˇ»jk«lˇ»
8107 «mˇ»nop"#
8108 ));
8109
8110 cx.update_editor(|editor, window, cx| {
8111 editor.add_selection_below(&Default::default(), window, cx);
8112 });
8113
8114 // test new added selection expands below, others shrinks from above
8115 cx.assert_editor_state(indoc!(
8116 r#"abcd
8117 ef«ghˇ»
8118 «iˇ»jk«lˇ»
8119 «mˇ»no«pˇ»"#
8120 ));
8121}
8122
8123#[gpui::test]
8124async fn test_select_next(cx: &mut TestAppContext) {
8125 init_test(cx, |_| {});
8126
8127 let mut cx = EditorTestContext::new(cx).await;
8128 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8129
8130 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8131 .unwrap();
8132 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8133
8134 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8135 .unwrap();
8136 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8137
8138 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8139 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8140
8141 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8142 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8143
8144 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8145 .unwrap();
8146 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8147
8148 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8149 .unwrap();
8150 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8151
8152 // Test selection direction should be preserved
8153 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8154
8155 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8156 .unwrap();
8157 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
8158}
8159
8160#[gpui::test]
8161async fn test_select_all_matches(cx: &mut TestAppContext) {
8162 init_test(cx, |_| {});
8163
8164 let mut cx = EditorTestContext::new(cx).await;
8165
8166 // Test caret-only selections
8167 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8168 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8169 .unwrap();
8170 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8171
8172 // Test left-to-right selections
8173 cx.set_state("abc\n«abcˇ»\nabc");
8174 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8175 .unwrap();
8176 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
8177
8178 // Test right-to-left selections
8179 cx.set_state("abc\n«ˇabc»\nabc");
8180 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8181 .unwrap();
8182 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
8183
8184 // Test selecting whitespace with caret selection
8185 cx.set_state("abc\nˇ abc\nabc");
8186 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8187 .unwrap();
8188 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8189
8190 // Test selecting whitespace with left-to-right selection
8191 cx.set_state("abc\n«ˇ »abc\nabc");
8192 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8193 .unwrap();
8194 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
8195
8196 // Test no matches with right-to-left selection
8197 cx.set_state("abc\n« ˇ»abc\nabc");
8198 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8199 .unwrap();
8200 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8201
8202 // Test with a single word and clip_at_line_ends=true (#29823)
8203 cx.set_state("aˇbc");
8204 cx.update_editor(|e, window, cx| {
8205 e.set_clip_at_line_ends(true, cx);
8206 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8207 e.set_clip_at_line_ends(false, cx);
8208 });
8209 cx.assert_editor_state("«abcˇ»");
8210}
8211
8212#[gpui::test]
8213async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
8214 init_test(cx, |_| {});
8215
8216 let mut cx = EditorTestContext::new(cx).await;
8217
8218 let large_body_1 = "\nd".repeat(200);
8219 let large_body_2 = "\ne".repeat(200);
8220
8221 cx.set_state(&format!(
8222 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
8223 ));
8224 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
8225 let scroll_position = editor.scroll_position(cx);
8226 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
8227 scroll_position
8228 });
8229
8230 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8231 .unwrap();
8232 cx.assert_editor_state(&format!(
8233 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
8234 ));
8235 let scroll_position_after_selection =
8236 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
8237 assert_eq!(
8238 initial_scroll_position, scroll_position_after_selection,
8239 "Scroll position should not change after selecting all matches"
8240 );
8241}
8242
8243#[gpui::test]
8244async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
8245 init_test(cx, |_| {});
8246
8247 let mut cx = EditorLspTestContext::new_rust(
8248 lsp::ServerCapabilities {
8249 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8250 ..Default::default()
8251 },
8252 cx,
8253 )
8254 .await;
8255
8256 cx.set_state(indoc! {"
8257 line 1
8258 line 2
8259 linˇe 3
8260 line 4
8261 line 5
8262 "});
8263
8264 // Make an edit
8265 cx.update_editor(|editor, window, cx| {
8266 editor.handle_input("X", window, cx);
8267 });
8268
8269 // Move cursor to a different position
8270 cx.update_editor(|editor, window, cx| {
8271 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8272 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
8273 });
8274 });
8275
8276 cx.assert_editor_state(indoc! {"
8277 line 1
8278 line 2
8279 linXe 3
8280 line 4
8281 liˇne 5
8282 "});
8283
8284 cx.lsp
8285 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8286 Ok(Some(vec![lsp::TextEdit::new(
8287 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8288 "PREFIX ".to_string(),
8289 )]))
8290 });
8291
8292 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
8293 .unwrap()
8294 .await
8295 .unwrap();
8296
8297 cx.assert_editor_state(indoc! {"
8298 PREFIX line 1
8299 line 2
8300 linXe 3
8301 line 4
8302 liˇne 5
8303 "});
8304
8305 // Undo formatting
8306 cx.update_editor(|editor, window, cx| {
8307 editor.undo(&Default::default(), window, cx);
8308 });
8309
8310 // Verify cursor moved back to position after edit
8311 cx.assert_editor_state(indoc! {"
8312 line 1
8313 line 2
8314 linXˇe 3
8315 line 4
8316 line 5
8317 "});
8318}
8319
8320#[gpui::test]
8321async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) {
8322 init_test(cx, |_| {});
8323
8324 let mut cx = EditorTestContext::new(cx).await;
8325
8326 let provider = cx.new(|_| FakeEditPredictionProvider::default());
8327 cx.update_editor(|editor, window, cx| {
8328 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
8329 });
8330
8331 cx.set_state(indoc! {"
8332 line 1
8333 line 2
8334 linˇe 3
8335 line 4
8336 line 5
8337 line 6
8338 line 7
8339 line 8
8340 line 9
8341 line 10
8342 "});
8343
8344 let snapshot = cx.buffer_snapshot();
8345 let edit_position = snapshot.anchor_after(Point::new(2, 4));
8346
8347 cx.update(|_, cx| {
8348 provider.update(cx, |provider, _| {
8349 provider.set_edit_prediction(Some(edit_prediction::EditPrediction::Local {
8350 id: None,
8351 edits: vec![(edit_position..edit_position, "X".into())],
8352 edit_preview: None,
8353 }))
8354 })
8355 });
8356
8357 cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
8358 cx.update_editor(|editor, window, cx| {
8359 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
8360 });
8361
8362 cx.assert_editor_state(indoc! {"
8363 line 1
8364 line 2
8365 lineXˇ 3
8366 line 4
8367 line 5
8368 line 6
8369 line 7
8370 line 8
8371 line 9
8372 line 10
8373 "});
8374
8375 cx.update_editor(|editor, window, cx| {
8376 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8377 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
8378 });
8379 });
8380
8381 cx.assert_editor_state(indoc! {"
8382 line 1
8383 line 2
8384 lineX 3
8385 line 4
8386 line 5
8387 line 6
8388 line 7
8389 line 8
8390 line 9
8391 liˇne 10
8392 "});
8393
8394 cx.update_editor(|editor, window, cx| {
8395 editor.undo(&Default::default(), window, cx);
8396 });
8397
8398 cx.assert_editor_state(indoc! {"
8399 line 1
8400 line 2
8401 lineˇ 3
8402 line 4
8403 line 5
8404 line 6
8405 line 7
8406 line 8
8407 line 9
8408 line 10
8409 "});
8410}
8411
8412#[gpui::test]
8413async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
8414 init_test(cx, |_| {});
8415
8416 let mut cx = EditorTestContext::new(cx).await;
8417 cx.set_state(
8418 r#"let foo = 2;
8419lˇet foo = 2;
8420let fooˇ = 2;
8421let foo = 2;
8422let foo = ˇ2;"#,
8423 );
8424
8425 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8426 .unwrap();
8427 cx.assert_editor_state(
8428 r#"let foo = 2;
8429«letˇ» foo = 2;
8430let «fooˇ» = 2;
8431let foo = 2;
8432let foo = «2ˇ»;"#,
8433 );
8434
8435 // noop for multiple selections with different contents
8436 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8437 .unwrap();
8438 cx.assert_editor_state(
8439 r#"let foo = 2;
8440«letˇ» foo = 2;
8441let «fooˇ» = 2;
8442let foo = 2;
8443let foo = «2ˇ»;"#,
8444 );
8445
8446 // Test last selection direction should be preserved
8447 cx.set_state(
8448 r#"let foo = 2;
8449let foo = 2;
8450let «fooˇ» = 2;
8451let «ˇfoo» = 2;
8452let foo = 2;"#,
8453 );
8454
8455 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8456 .unwrap();
8457 cx.assert_editor_state(
8458 r#"let foo = 2;
8459let foo = 2;
8460let «fooˇ» = 2;
8461let «ˇfoo» = 2;
8462let «ˇfoo» = 2;"#,
8463 );
8464}
8465
8466#[gpui::test]
8467async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
8468 init_test(cx, |_| {});
8469
8470 let mut cx =
8471 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
8472
8473 cx.assert_editor_state(indoc! {"
8474 ˇbbb
8475 ccc
8476
8477 bbb
8478 ccc
8479 "});
8480 cx.dispatch_action(SelectPrevious::default());
8481 cx.assert_editor_state(indoc! {"
8482 «bbbˇ»
8483 ccc
8484
8485 bbb
8486 ccc
8487 "});
8488 cx.dispatch_action(SelectPrevious::default());
8489 cx.assert_editor_state(indoc! {"
8490 «bbbˇ»
8491 ccc
8492
8493 «bbbˇ»
8494 ccc
8495 "});
8496}
8497
8498#[gpui::test]
8499async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
8500 init_test(cx, |_| {});
8501
8502 let mut cx = EditorTestContext::new(cx).await;
8503 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8504
8505 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8506 .unwrap();
8507 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8508
8509 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8510 .unwrap();
8511 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8512
8513 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8514 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8515
8516 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8517 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8518
8519 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8520 .unwrap();
8521 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
8522
8523 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8524 .unwrap();
8525 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8526}
8527
8528#[gpui::test]
8529async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
8530 init_test(cx, |_| {});
8531
8532 let mut cx = EditorTestContext::new(cx).await;
8533 cx.set_state("aˇ");
8534
8535 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8536 .unwrap();
8537 cx.assert_editor_state("«aˇ»");
8538 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8539 .unwrap();
8540 cx.assert_editor_state("«aˇ»");
8541}
8542
8543#[gpui::test]
8544async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
8545 init_test(cx, |_| {});
8546
8547 let mut cx = EditorTestContext::new(cx).await;
8548 cx.set_state(
8549 r#"let foo = 2;
8550lˇet foo = 2;
8551let fooˇ = 2;
8552let foo = 2;
8553let foo = ˇ2;"#,
8554 );
8555
8556 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8557 .unwrap();
8558 cx.assert_editor_state(
8559 r#"let foo = 2;
8560«letˇ» foo = 2;
8561let «fooˇ» = 2;
8562let foo = 2;
8563let foo = «2ˇ»;"#,
8564 );
8565
8566 // noop for multiple selections with different contents
8567 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8568 .unwrap();
8569 cx.assert_editor_state(
8570 r#"let foo = 2;
8571«letˇ» foo = 2;
8572let «fooˇ» = 2;
8573let foo = 2;
8574let foo = «2ˇ»;"#,
8575 );
8576}
8577
8578#[gpui::test]
8579async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
8580 init_test(cx, |_| {});
8581
8582 let mut cx = EditorTestContext::new(cx).await;
8583 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8584
8585 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8586 .unwrap();
8587 // selection direction is preserved
8588 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8589
8590 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8591 .unwrap();
8592 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8593
8594 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8595 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8596
8597 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8598 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8599
8600 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8601 .unwrap();
8602 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
8603
8604 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8605 .unwrap();
8606 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
8607}
8608
8609#[gpui::test]
8610async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
8611 init_test(cx, |_| {});
8612
8613 let language = Arc::new(Language::new(
8614 LanguageConfig::default(),
8615 Some(tree_sitter_rust::LANGUAGE.into()),
8616 ));
8617
8618 let text = r#"
8619 use mod1::mod2::{mod3, mod4};
8620
8621 fn fn_1(param1: bool, param2: &str) {
8622 let var1 = "text";
8623 }
8624 "#
8625 .unindent();
8626
8627 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8628 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8629 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8630
8631 editor
8632 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8633 .await;
8634
8635 editor.update_in(cx, |editor, window, cx| {
8636 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8637 s.select_display_ranges([
8638 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
8639 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
8640 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
8641 ]);
8642 });
8643 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8644 });
8645 editor.update(cx, |editor, cx| {
8646 assert_text_with_selections(
8647 editor,
8648 indoc! {r#"
8649 use mod1::mod2::{mod3, «mod4ˇ»};
8650
8651 fn fn_1«ˇ(param1: bool, param2: &str)» {
8652 let var1 = "«ˇtext»";
8653 }
8654 "#},
8655 cx,
8656 );
8657 });
8658
8659 editor.update_in(cx, |editor, window, cx| {
8660 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8661 });
8662 editor.update(cx, |editor, cx| {
8663 assert_text_with_selections(
8664 editor,
8665 indoc! {r#"
8666 use mod1::mod2::«{mod3, mod4}ˇ»;
8667
8668 «ˇfn fn_1(param1: bool, param2: &str) {
8669 let var1 = "text";
8670 }»
8671 "#},
8672 cx,
8673 );
8674 });
8675
8676 editor.update_in(cx, |editor, window, cx| {
8677 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8678 });
8679 assert_eq!(
8680 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
8681 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8682 );
8683
8684 // Trying to expand the selected syntax node one more time has no effect.
8685 editor.update_in(cx, |editor, window, cx| {
8686 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8687 });
8688 assert_eq!(
8689 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
8690 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8691 );
8692
8693 editor.update_in(cx, |editor, window, cx| {
8694 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8695 });
8696 editor.update(cx, |editor, cx| {
8697 assert_text_with_selections(
8698 editor,
8699 indoc! {r#"
8700 use mod1::mod2::«{mod3, mod4}ˇ»;
8701
8702 «ˇfn fn_1(param1: bool, param2: &str) {
8703 let var1 = "text";
8704 }»
8705 "#},
8706 cx,
8707 );
8708 });
8709
8710 editor.update_in(cx, |editor, window, cx| {
8711 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8712 });
8713 editor.update(cx, |editor, cx| {
8714 assert_text_with_selections(
8715 editor,
8716 indoc! {r#"
8717 use mod1::mod2::{mod3, «mod4ˇ»};
8718
8719 fn fn_1«ˇ(param1: bool, param2: &str)» {
8720 let var1 = "«ˇtext»";
8721 }
8722 "#},
8723 cx,
8724 );
8725 });
8726
8727 editor.update_in(cx, |editor, window, cx| {
8728 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8729 });
8730 editor.update(cx, |editor, cx| {
8731 assert_text_with_selections(
8732 editor,
8733 indoc! {r#"
8734 use mod1::mod2::{mod3, moˇd4};
8735
8736 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
8737 let var1 = "teˇxt";
8738 }
8739 "#},
8740 cx,
8741 );
8742 });
8743
8744 // Trying to shrink the selected syntax node one more time has no effect.
8745 editor.update_in(cx, |editor, window, cx| {
8746 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8747 });
8748 editor.update_in(cx, |editor, _, cx| {
8749 assert_text_with_selections(
8750 editor,
8751 indoc! {r#"
8752 use mod1::mod2::{mod3, moˇd4};
8753
8754 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
8755 let var1 = "teˇxt";
8756 }
8757 "#},
8758 cx,
8759 );
8760 });
8761
8762 // Ensure that we keep expanding the selection if the larger selection starts or ends within
8763 // a fold.
8764 editor.update_in(cx, |editor, window, cx| {
8765 editor.fold_creases(
8766 vec![
8767 Crease::simple(
8768 Point::new(0, 21)..Point::new(0, 24),
8769 FoldPlaceholder::test(),
8770 ),
8771 Crease::simple(
8772 Point::new(3, 20)..Point::new(3, 22),
8773 FoldPlaceholder::test(),
8774 ),
8775 ],
8776 true,
8777 window,
8778 cx,
8779 );
8780 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8781 });
8782 editor.update(cx, |editor, cx| {
8783 assert_text_with_selections(
8784 editor,
8785 indoc! {r#"
8786 use mod1::mod2::«{mod3, mod4}ˇ»;
8787
8788 fn fn_1«ˇ(param1: bool, param2: &str)» {
8789 let var1 = "«ˇtext»";
8790 }
8791 "#},
8792 cx,
8793 );
8794 });
8795}
8796
8797#[gpui::test]
8798async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
8799 init_test(cx, |_| {});
8800
8801 let language = Arc::new(Language::new(
8802 LanguageConfig::default(),
8803 Some(tree_sitter_rust::LANGUAGE.into()),
8804 ));
8805
8806 let text = "let a = 2;";
8807
8808 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8809 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8810 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8811
8812 editor
8813 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8814 .await;
8815
8816 // Test case 1: Cursor at end of word
8817 editor.update_in(cx, |editor, window, cx| {
8818 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8819 s.select_display_ranges([
8820 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
8821 ]);
8822 });
8823 });
8824 editor.update(cx, |editor, cx| {
8825 assert_text_with_selections(editor, "let aˇ = 2;", cx);
8826 });
8827 editor.update_in(cx, |editor, window, cx| {
8828 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8829 });
8830 editor.update(cx, |editor, cx| {
8831 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
8832 });
8833 editor.update_in(cx, |editor, window, cx| {
8834 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8835 });
8836 editor.update(cx, |editor, cx| {
8837 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
8838 });
8839
8840 // Test case 2: Cursor at end of statement
8841 editor.update_in(cx, |editor, window, cx| {
8842 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8843 s.select_display_ranges([
8844 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
8845 ]);
8846 });
8847 });
8848 editor.update(cx, |editor, cx| {
8849 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
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(editor, "«ˇlet a = 2;»", cx);
8856 });
8857}
8858
8859#[gpui::test]
8860async fn test_select_larger_syntax_node_for_cursor_at_symbol(cx: &mut TestAppContext) {
8861 init_test(cx, |_| {});
8862
8863 let language = Arc::new(Language::new(
8864 LanguageConfig {
8865 name: "JavaScript".into(),
8866 ..Default::default()
8867 },
8868 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
8869 ));
8870
8871 let text = r#"
8872 let a = {
8873 key: "value",
8874 };
8875 "#
8876 .unindent();
8877
8878 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8879 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8880 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8881
8882 editor
8883 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8884 .await;
8885
8886 // Test case 1: Cursor after '{'
8887 editor.update_in(cx, |editor, window, cx| {
8888 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8889 s.select_display_ranges([
8890 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 9)
8891 ]);
8892 });
8893 });
8894 editor.update(cx, |editor, cx| {
8895 assert_text_with_selections(
8896 editor,
8897 indoc! {r#"
8898 let a = {ˇ
8899 key: "value",
8900 };
8901 "#},
8902 cx,
8903 );
8904 });
8905 editor.update_in(cx, |editor, window, cx| {
8906 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8907 });
8908 editor.update(cx, |editor, cx| {
8909 assert_text_with_selections(
8910 editor,
8911 indoc! {r#"
8912 let a = «ˇ{
8913 key: "value",
8914 }»;
8915 "#},
8916 cx,
8917 );
8918 });
8919
8920 // Test case 2: Cursor after ':'
8921 editor.update_in(cx, |editor, window, cx| {
8922 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8923 s.select_display_ranges([
8924 DisplayPoint::new(DisplayRow(1), 8)..DisplayPoint::new(DisplayRow(1), 8)
8925 ]);
8926 });
8927 });
8928 editor.update(cx, |editor, cx| {
8929 assert_text_with_selections(
8930 editor,
8931 indoc! {r#"
8932 let a = {
8933 key:ˇ "value",
8934 };
8935 "#},
8936 cx,
8937 );
8938 });
8939 editor.update_in(cx, |editor, window, cx| {
8940 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8941 });
8942 editor.update(cx, |editor, cx| {
8943 assert_text_with_selections(
8944 editor,
8945 indoc! {r#"
8946 let a = {
8947 «ˇkey: "value"»,
8948 };
8949 "#},
8950 cx,
8951 );
8952 });
8953 editor.update_in(cx, |editor, window, cx| {
8954 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8955 });
8956 editor.update(cx, |editor, cx| {
8957 assert_text_with_selections(
8958 editor,
8959 indoc! {r#"
8960 let a = «ˇ{
8961 key: "value",
8962 }»;
8963 "#},
8964 cx,
8965 );
8966 });
8967
8968 // Test case 3: Cursor after ','
8969 editor.update_in(cx, |editor, window, cx| {
8970 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8971 s.select_display_ranges([
8972 DisplayPoint::new(DisplayRow(1), 17)..DisplayPoint::new(DisplayRow(1), 17)
8973 ]);
8974 });
8975 });
8976 editor.update(cx, |editor, cx| {
8977 assert_text_with_selections(
8978 editor,
8979 indoc! {r#"
8980 let a = {
8981 key: "value",ˇ
8982 };
8983 "#},
8984 cx,
8985 );
8986 });
8987 editor.update_in(cx, |editor, window, cx| {
8988 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8989 });
8990 editor.update(cx, |editor, cx| {
8991 assert_text_with_selections(
8992 editor,
8993 indoc! {r#"
8994 let a = «ˇ{
8995 key: "value",
8996 }»;
8997 "#},
8998 cx,
8999 );
9000 });
9001
9002 // Test case 4: Cursor after ';'
9003 editor.update_in(cx, |editor, window, cx| {
9004 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9005 s.select_display_ranges([
9006 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)
9007 ]);
9008 });
9009 });
9010 editor.update(cx, |editor, cx| {
9011 assert_text_with_selections(
9012 editor,
9013 indoc! {r#"
9014 let a = {
9015 key: "value",
9016 };ˇ
9017 "#},
9018 cx,
9019 );
9020 });
9021 editor.update_in(cx, |editor, window, cx| {
9022 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9023 });
9024 editor.update(cx, |editor, cx| {
9025 assert_text_with_selections(
9026 editor,
9027 indoc! {r#"
9028 «ˇlet a = {
9029 key: "value",
9030 };
9031 »"#},
9032 cx,
9033 );
9034 });
9035}
9036
9037#[gpui::test]
9038async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
9039 init_test(cx, |_| {});
9040
9041 let language = Arc::new(Language::new(
9042 LanguageConfig::default(),
9043 Some(tree_sitter_rust::LANGUAGE.into()),
9044 ));
9045
9046 let text = r#"
9047 use mod1::mod2::{mod3, mod4};
9048
9049 fn fn_1(param1: bool, param2: &str) {
9050 let var1 = "hello world";
9051 }
9052 "#
9053 .unindent();
9054
9055 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9056 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9057 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9058
9059 editor
9060 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9061 .await;
9062
9063 // Test 1: Cursor on a letter of a string word
9064 editor.update_in(cx, |editor, window, cx| {
9065 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9066 s.select_display_ranges([
9067 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
9068 ]);
9069 });
9070 });
9071 editor.update_in(cx, |editor, window, cx| {
9072 assert_text_with_selections(
9073 editor,
9074 indoc! {r#"
9075 use mod1::mod2::{mod3, mod4};
9076
9077 fn fn_1(param1: bool, param2: &str) {
9078 let var1 = "hˇello world";
9079 }
9080 "#},
9081 cx,
9082 );
9083 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9084 assert_text_with_selections(
9085 editor,
9086 indoc! {r#"
9087 use mod1::mod2::{mod3, mod4};
9088
9089 fn fn_1(param1: bool, param2: &str) {
9090 let var1 = "«ˇhello» world";
9091 }
9092 "#},
9093 cx,
9094 );
9095 });
9096
9097 // Test 2: Partial selection within a word
9098 editor.update_in(cx, |editor, window, cx| {
9099 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9100 s.select_display_ranges([
9101 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
9102 ]);
9103 });
9104 });
9105 editor.update_in(cx, |editor, window, cx| {
9106 assert_text_with_selections(
9107 editor,
9108 indoc! {r#"
9109 use mod1::mod2::{mod3, mod4};
9110
9111 fn fn_1(param1: bool, param2: &str) {
9112 let var1 = "h«elˇ»lo world";
9113 }
9114 "#},
9115 cx,
9116 );
9117 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9118 assert_text_with_selections(
9119 editor,
9120 indoc! {r#"
9121 use mod1::mod2::{mod3, mod4};
9122
9123 fn fn_1(param1: bool, param2: &str) {
9124 let var1 = "«ˇhello» world";
9125 }
9126 "#},
9127 cx,
9128 );
9129 });
9130
9131 // Test 3: Complete word already selected
9132 editor.update_in(cx, |editor, window, cx| {
9133 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9134 s.select_display_ranges([
9135 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
9136 ]);
9137 });
9138 });
9139 editor.update_in(cx, |editor, window, cx| {
9140 assert_text_with_selections(
9141 editor,
9142 indoc! {r#"
9143 use mod1::mod2::{mod3, mod4};
9144
9145 fn fn_1(param1: bool, param2: &str) {
9146 let var1 = "«helloˇ» world";
9147 }
9148 "#},
9149 cx,
9150 );
9151 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9152 assert_text_with_selections(
9153 editor,
9154 indoc! {r#"
9155 use mod1::mod2::{mod3, mod4};
9156
9157 fn fn_1(param1: bool, param2: &str) {
9158 let var1 = "«hello worldˇ»";
9159 }
9160 "#},
9161 cx,
9162 );
9163 });
9164
9165 // Test 4: Selection spanning across words
9166 editor.update_in(cx, |editor, window, cx| {
9167 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9168 s.select_display_ranges([
9169 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
9170 ]);
9171 });
9172 });
9173 editor.update_in(cx, |editor, window, cx| {
9174 assert_text_with_selections(
9175 editor,
9176 indoc! {r#"
9177 use mod1::mod2::{mod3, mod4};
9178
9179 fn fn_1(param1: bool, param2: &str) {
9180 let var1 = "hel«lo woˇ»rld";
9181 }
9182 "#},
9183 cx,
9184 );
9185 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9186 assert_text_with_selections(
9187 editor,
9188 indoc! {r#"
9189 use mod1::mod2::{mod3, mod4};
9190
9191 fn fn_1(param1: bool, param2: &str) {
9192 let var1 = "«ˇhello world»";
9193 }
9194 "#},
9195 cx,
9196 );
9197 });
9198
9199 // Test 5: Expansion beyond string
9200 editor.update_in(cx, |editor, window, cx| {
9201 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9202 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9203 assert_text_with_selections(
9204 editor,
9205 indoc! {r#"
9206 use mod1::mod2::{mod3, mod4};
9207
9208 fn fn_1(param1: bool, param2: &str) {
9209 «ˇlet var1 = "hello world";»
9210 }
9211 "#},
9212 cx,
9213 );
9214 });
9215}
9216
9217#[gpui::test]
9218async fn test_unwrap_syntax_nodes(cx: &mut gpui::TestAppContext) {
9219 init_test(cx, |_| {});
9220
9221 let mut cx = EditorTestContext::new(cx).await;
9222
9223 let language = Arc::new(Language::new(
9224 LanguageConfig::default(),
9225 Some(tree_sitter_rust::LANGUAGE.into()),
9226 ));
9227
9228 cx.update_buffer(|buffer, cx| {
9229 buffer.set_language(Some(language), cx);
9230 });
9231
9232 cx.set_state(indoc! { r#"use mod1::{mod2::{«mod3ˇ», mod4}, mod5::{mod6, «mod7ˇ»}};"# });
9233 cx.update_editor(|editor, window, cx| {
9234 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9235 });
9236
9237 cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# });
9238
9239 cx.set_state(indoc! { r#"fn a() {
9240 // what
9241 // a
9242 // ˇlong
9243 // method
9244 // I
9245 // sure
9246 // hope
9247 // it
9248 // works
9249 }"# });
9250
9251 let buffer = cx.update_multibuffer(|multibuffer, _| multibuffer.as_singleton().unwrap());
9252 let multi_buffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
9253 cx.update(|_, cx| {
9254 multi_buffer.update(cx, |multi_buffer, cx| {
9255 multi_buffer.set_excerpts_for_path(
9256 PathKey::for_buffer(&buffer, cx),
9257 buffer,
9258 [Point::new(1, 0)..Point::new(1, 0)],
9259 3,
9260 cx,
9261 );
9262 });
9263 });
9264
9265 let editor2 = cx.new_window_entity(|window, cx| {
9266 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
9267 });
9268
9269 let mut cx = EditorTestContext::for_editor_in(editor2, &mut cx).await;
9270 cx.update_editor(|editor, window, cx| {
9271 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
9272 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)]);
9273 })
9274 });
9275
9276 cx.assert_editor_state(indoc! { "
9277 fn a() {
9278 // what
9279 // a
9280 ˇ // long
9281 // method"});
9282
9283 cx.update_editor(|editor, window, cx| {
9284 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9285 });
9286
9287 // Although we could potentially make the action work when the syntax node
9288 // is half-hidden, it seems a bit dangerous as you can't easily tell what it
9289 // did. Maybe we could also expand the excerpt to contain the range?
9290 cx.assert_editor_state(indoc! { "
9291 fn a() {
9292 // what
9293 // a
9294 ˇ // long
9295 // method"});
9296}
9297
9298#[gpui::test]
9299async fn test_fold_function_bodies(cx: &mut TestAppContext) {
9300 init_test(cx, |_| {});
9301
9302 let base_text = r#"
9303 impl A {
9304 // this is an uncommitted comment
9305
9306 fn b() {
9307 c();
9308 }
9309
9310 // this is another uncommitted comment
9311
9312 fn d() {
9313 // e
9314 // f
9315 }
9316 }
9317
9318 fn g() {
9319 // h
9320 }
9321 "#
9322 .unindent();
9323
9324 let text = r#"
9325 ˇimpl A {
9326
9327 fn b() {
9328 c();
9329 }
9330
9331 fn d() {
9332 // e
9333 // f
9334 }
9335 }
9336
9337 fn g() {
9338 // h
9339 }
9340 "#
9341 .unindent();
9342
9343 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9344 cx.set_state(&text);
9345 cx.set_head_text(&base_text);
9346 cx.update_editor(|editor, window, cx| {
9347 editor.expand_all_diff_hunks(&Default::default(), window, cx);
9348 });
9349
9350 cx.assert_state_with_diff(
9351 "
9352 ˇimpl A {
9353 - // this is an uncommitted comment
9354
9355 fn b() {
9356 c();
9357 }
9358
9359 - // this is another uncommitted comment
9360 -
9361 fn d() {
9362 // e
9363 // f
9364 }
9365 }
9366
9367 fn g() {
9368 // h
9369 }
9370 "
9371 .unindent(),
9372 );
9373
9374 let expected_display_text = "
9375 impl A {
9376 // this is an uncommitted comment
9377
9378 fn b() {
9379 ⋯
9380 }
9381
9382 // this is another uncommitted comment
9383
9384 fn d() {
9385 ⋯
9386 }
9387 }
9388
9389 fn g() {
9390 ⋯
9391 }
9392 "
9393 .unindent();
9394
9395 cx.update_editor(|editor, window, cx| {
9396 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
9397 assert_eq!(editor.display_text(cx), expected_display_text);
9398 });
9399}
9400
9401#[gpui::test]
9402async fn test_autoindent(cx: &mut TestAppContext) {
9403 init_test(cx, |_| {});
9404
9405 let language = Arc::new(
9406 Language::new(
9407 LanguageConfig {
9408 brackets: BracketPairConfig {
9409 pairs: vec![
9410 BracketPair {
9411 start: "{".to_string(),
9412 end: "}".to_string(),
9413 close: false,
9414 surround: false,
9415 newline: true,
9416 },
9417 BracketPair {
9418 start: "(".to_string(),
9419 end: ")".to_string(),
9420 close: false,
9421 surround: false,
9422 newline: true,
9423 },
9424 ],
9425 ..Default::default()
9426 },
9427 ..Default::default()
9428 },
9429 Some(tree_sitter_rust::LANGUAGE.into()),
9430 )
9431 .with_indents_query(
9432 r#"
9433 (_ "(" ")" @end) @indent
9434 (_ "{" "}" @end) @indent
9435 "#,
9436 )
9437 .unwrap(),
9438 );
9439
9440 let text = "fn a() {}";
9441
9442 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9443 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9444 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9445 editor
9446 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9447 .await;
9448
9449 editor.update_in(cx, |editor, window, cx| {
9450 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9451 s.select_ranges([5..5, 8..8, 9..9])
9452 });
9453 editor.newline(&Newline, window, cx);
9454 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
9455 assert_eq!(
9456 editor.selections.ranges(cx),
9457 &[
9458 Point::new(1, 4)..Point::new(1, 4),
9459 Point::new(3, 4)..Point::new(3, 4),
9460 Point::new(5, 0)..Point::new(5, 0)
9461 ]
9462 );
9463 });
9464}
9465
9466#[gpui::test]
9467async fn test_autoindent_disabled(cx: &mut TestAppContext) {
9468 init_test(cx, |settings| settings.defaults.auto_indent = Some(false));
9469
9470 let language = Arc::new(
9471 Language::new(
9472 LanguageConfig {
9473 brackets: BracketPairConfig {
9474 pairs: vec![
9475 BracketPair {
9476 start: "{".to_string(),
9477 end: "}".to_string(),
9478 close: false,
9479 surround: false,
9480 newline: true,
9481 },
9482 BracketPair {
9483 start: "(".to_string(),
9484 end: ")".to_string(),
9485 close: false,
9486 surround: false,
9487 newline: true,
9488 },
9489 ],
9490 ..Default::default()
9491 },
9492 ..Default::default()
9493 },
9494 Some(tree_sitter_rust::LANGUAGE.into()),
9495 )
9496 .with_indents_query(
9497 r#"
9498 (_ "(" ")" @end) @indent
9499 (_ "{" "}" @end) @indent
9500 "#,
9501 )
9502 .unwrap(),
9503 );
9504
9505 let text = "fn a() {}";
9506
9507 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9508 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9509 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9510 editor
9511 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9512 .await;
9513
9514 editor.update_in(cx, |editor, window, cx| {
9515 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9516 s.select_ranges([5..5, 8..8, 9..9])
9517 });
9518 editor.newline(&Newline, window, cx);
9519 assert_eq!(
9520 editor.text(cx),
9521 indoc!(
9522 "
9523 fn a(
9524
9525 ) {
9526
9527 }
9528 "
9529 )
9530 );
9531 assert_eq!(
9532 editor.selections.ranges(cx),
9533 &[
9534 Point::new(1, 0)..Point::new(1, 0),
9535 Point::new(3, 0)..Point::new(3, 0),
9536 Point::new(5, 0)..Point::new(5, 0)
9537 ]
9538 );
9539 });
9540}
9541
9542#[gpui::test]
9543async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) {
9544 init_test(cx, |settings| {
9545 settings.defaults.auto_indent = Some(true);
9546 settings.languages.0.insert(
9547 "python".into(),
9548 LanguageSettingsContent {
9549 auto_indent: Some(false),
9550 ..Default::default()
9551 },
9552 );
9553 });
9554
9555 let mut cx = EditorTestContext::new(cx).await;
9556
9557 let injected_language = Arc::new(
9558 Language::new(
9559 LanguageConfig {
9560 brackets: BracketPairConfig {
9561 pairs: vec![
9562 BracketPair {
9563 start: "{".to_string(),
9564 end: "}".to_string(),
9565 close: false,
9566 surround: false,
9567 newline: true,
9568 },
9569 BracketPair {
9570 start: "(".to_string(),
9571 end: ")".to_string(),
9572 close: true,
9573 surround: false,
9574 newline: true,
9575 },
9576 ],
9577 ..Default::default()
9578 },
9579 name: "python".into(),
9580 ..Default::default()
9581 },
9582 Some(tree_sitter_python::LANGUAGE.into()),
9583 )
9584 .with_indents_query(
9585 r#"
9586 (_ "(" ")" @end) @indent
9587 (_ "{" "}" @end) @indent
9588 "#,
9589 )
9590 .unwrap(),
9591 );
9592
9593 let language = Arc::new(
9594 Language::new(
9595 LanguageConfig {
9596 brackets: BracketPairConfig {
9597 pairs: vec![
9598 BracketPair {
9599 start: "{".to_string(),
9600 end: "}".to_string(),
9601 close: false,
9602 surround: false,
9603 newline: true,
9604 },
9605 BracketPair {
9606 start: "(".to_string(),
9607 end: ")".to_string(),
9608 close: true,
9609 surround: false,
9610 newline: true,
9611 },
9612 ],
9613 ..Default::default()
9614 },
9615 name: LanguageName::new("rust"),
9616 ..Default::default()
9617 },
9618 Some(tree_sitter_rust::LANGUAGE.into()),
9619 )
9620 .with_indents_query(
9621 r#"
9622 (_ "(" ")" @end) @indent
9623 (_ "{" "}" @end) @indent
9624 "#,
9625 )
9626 .unwrap()
9627 .with_injection_query(
9628 r#"
9629 (macro_invocation
9630 macro: (identifier) @_macro_name
9631 (token_tree) @injection.content
9632 (#set! injection.language "python"))
9633 "#,
9634 )
9635 .unwrap(),
9636 );
9637
9638 cx.language_registry().add(injected_language);
9639 cx.language_registry().add(language.clone());
9640
9641 cx.update_buffer(|buffer, cx| {
9642 buffer.set_language(Some(language), cx);
9643 });
9644
9645 cx.set_state(r#"struct A {ˇ}"#);
9646
9647 cx.update_editor(|editor, window, cx| {
9648 editor.newline(&Default::default(), window, cx);
9649 });
9650
9651 cx.assert_editor_state(indoc!(
9652 "struct A {
9653 ˇ
9654 }"
9655 ));
9656
9657 cx.set_state(r#"select_biased!(ˇ)"#);
9658
9659 cx.update_editor(|editor, window, cx| {
9660 editor.newline(&Default::default(), window, cx);
9661 editor.handle_input("def ", window, cx);
9662 editor.handle_input("(", window, cx);
9663 editor.newline(&Default::default(), window, cx);
9664 editor.handle_input("a", window, cx);
9665 });
9666
9667 cx.assert_editor_state(indoc!(
9668 "select_biased!(
9669 def (
9670 aˇ
9671 )
9672 )"
9673 ));
9674}
9675
9676#[gpui::test]
9677async fn test_autoindent_selections(cx: &mut TestAppContext) {
9678 init_test(cx, |_| {});
9679
9680 {
9681 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9682 cx.set_state(indoc! {"
9683 impl A {
9684
9685 fn b() {}
9686
9687 «fn c() {
9688
9689 }ˇ»
9690 }
9691 "});
9692
9693 cx.update_editor(|editor, window, cx| {
9694 editor.autoindent(&Default::default(), window, cx);
9695 });
9696
9697 cx.assert_editor_state(indoc! {"
9698 impl A {
9699
9700 fn b() {}
9701
9702 «fn c() {
9703
9704 }ˇ»
9705 }
9706 "});
9707 }
9708
9709 {
9710 let mut cx = EditorTestContext::new_multibuffer(
9711 cx,
9712 [indoc! { "
9713 impl A {
9714 «
9715 // a
9716 fn b(){}
9717 »
9718 «
9719 }
9720 fn c(){}
9721 »
9722 "}],
9723 );
9724
9725 let buffer = cx.update_editor(|editor, _, cx| {
9726 let buffer = editor.buffer().update(cx, |buffer, _| {
9727 buffer.all_buffers().iter().next().unwrap().clone()
9728 });
9729 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
9730 buffer
9731 });
9732
9733 cx.run_until_parked();
9734 cx.update_editor(|editor, window, cx| {
9735 editor.select_all(&Default::default(), window, cx);
9736 editor.autoindent(&Default::default(), window, cx)
9737 });
9738 cx.run_until_parked();
9739
9740 cx.update(|_, cx| {
9741 assert_eq!(
9742 buffer.read(cx).text(),
9743 indoc! { "
9744 impl A {
9745
9746 // a
9747 fn b(){}
9748
9749
9750 }
9751 fn c(){}
9752
9753 " }
9754 )
9755 });
9756 }
9757}
9758
9759#[gpui::test]
9760async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
9761 init_test(cx, |_| {});
9762
9763 let mut cx = EditorTestContext::new(cx).await;
9764
9765 let language = Arc::new(Language::new(
9766 LanguageConfig {
9767 brackets: BracketPairConfig {
9768 pairs: vec![
9769 BracketPair {
9770 start: "{".to_string(),
9771 end: "}".to_string(),
9772 close: true,
9773 surround: true,
9774 newline: true,
9775 },
9776 BracketPair {
9777 start: "(".to_string(),
9778 end: ")".to_string(),
9779 close: true,
9780 surround: true,
9781 newline: true,
9782 },
9783 BracketPair {
9784 start: "/*".to_string(),
9785 end: " */".to_string(),
9786 close: true,
9787 surround: true,
9788 newline: true,
9789 },
9790 BracketPair {
9791 start: "[".to_string(),
9792 end: "]".to_string(),
9793 close: false,
9794 surround: false,
9795 newline: true,
9796 },
9797 BracketPair {
9798 start: "\"".to_string(),
9799 end: "\"".to_string(),
9800 close: true,
9801 surround: true,
9802 newline: false,
9803 },
9804 BracketPair {
9805 start: "<".to_string(),
9806 end: ">".to_string(),
9807 close: false,
9808 surround: true,
9809 newline: true,
9810 },
9811 ],
9812 ..Default::default()
9813 },
9814 autoclose_before: "})]".to_string(),
9815 ..Default::default()
9816 },
9817 Some(tree_sitter_rust::LANGUAGE.into()),
9818 ));
9819
9820 cx.language_registry().add(language.clone());
9821 cx.update_buffer(|buffer, cx| {
9822 buffer.set_language(Some(language), cx);
9823 });
9824
9825 cx.set_state(
9826 &r#"
9827 🏀ˇ
9828 εˇ
9829 ❤️ˇ
9830 "#
9831 .unindent(),
9832 );
9833
9834 // autoclose multiple nested brackets at multiple cursors
9835 cx.update_editor(|editor, window, cx| {
9836 editor.handle_input("{", window, cx);
9837 editor.handle_input("{", window, cx);
9838 editor.handle_input("{", window, cx);
9839 });
9840 cx.assert_editor_state(
9841 &"
9842 🏀{{{ˇ}}}
9843 ε{{{ˇ}}}
9844 ❤️{{{ˇ}}}
9845 "
9846 .unindent(),
9847 );
9848
9849 // insert a different closing bracket
9850 cx.update_editor(|editor, window, cx| {
9851 editor.handle_input(")", window, cx);
9852 });
9853 cx.assert_editor_state(
9854 &"
9855 🏀{{{)ˇ}}}
9856 ε{{{)ˇ}}}
9857 ❤️{{{)ˇ}}}
9858 "
9859 .unindent(),
9860 );
9861
9862 // skip over the auto-closed brackets when typing a closing bracket
9863 cx.update_editor(|editor, window, cx| {
9864 editor.move_right(&MoveRight, window, cx);
9865 editor.handle_input("}", window, cx);
9866 editor.handle_input("}", window, cx);
9867 editor.handle_input("}", window, cx);
9868 });
9869 cx.assert_editor_state(
9870 &"
9871 🏀{{{)}}}}ˇ
9872 ε{{{)}}}}ˇ
9873 ❤️{{{)}}}}ˇ
9874 "
9875 .unindent(),
9876 );
9877
9878 // autoclose multi-character pairs
9879 cx.set_state(
9880 &"
9881 ˇ
9882 ˇ
9883 "
9884 .unindent(),
9885 );
9886 cx.update_editor(|editor, window, cx| {
9887 editor.handle_input("/", window, cx);
9888 editor.handle_input("*", window, cx);
9889 });
9890 cx.assert_editor_state(
9891 &"
9892 /*ˇ */
9893 /*ˇ */
9894 "
9895 .unindent(),
9896 );
9897
9898 // one cursor autocloses a multi-character pair, one cursor
9899 // does not autoclose.
9900 cx.set_state(
9901 &"
9902 /ˇ
9903 ˇ
9904 "
9905 .unindent(),
9906 );
9907 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
9908 cx.assert_editor_state(
9909 &"
9910 /*ˇ */
9911 *ˇ
9912 "
9913 .unindent(),
9914 );
9915
9916 // Don't autoclose if the next character isn't whitespace and isn't
9917 // listed in the language's "autoclose_before" section.
9918 cx.set_state("ˇa b");
9919 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
9920 cx.assert_editor_state("{ˇa b");
9921
9922 // Don't autoclose if `close` is false for the bracket pair
9923 cx.set_state("ˇ");
9924 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
9925 cx.assert_editor_state("[ˇ");
9926
9927 // Surround with brackets if text is selected
9928 cx.set_state("«aˇ» b");
9929 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
9930 cx.assert_editor_state("{«aˇ»} b");
9931
9932 // Autoclose when not immediately after a word character
9933 cx.set_state("a ˇ");
9934 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9935 cx.assert_editor_state("a \"ˇ\"");
9936
9937 // Autoclose pair where the start and end characters are the same
9938 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9939 cx.assert_editor_state("a \"\"ˇ");
9940
9941 // Don't autoclose when immediately after a word character
9942 cx.set_state("aˇ");
9943 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9944 cx.assert_editor_state("a\"ˇ");
9945
9946 // Do autoclose when after a non-word character
9947 cx.set_state("{ˇ");
9948 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9949 cx.assert_editor_state("{\"ˇ\"");
9950
9951 // Non identical pairs autoclose regardless of preceding character
9952 cx.set_state("aˇ");
9953 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
9954 cx.assert_editor_state("a{ˇ}");
9955
9956 // Don't autoclose pair if autoclose is disabled
9957 cx.set_state("ˇ");
9958 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
9959 cx.assert_editor_state("<ˇ");
9960
9961 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
9962 cx.set_state("«aˇ» b");
9963 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
9964 cx.assert_editor_state("<«aˇ»> b");
9965}
9966
9967#[gpui::test]
9968async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
9969 init_test(cx, |settings| {
9970 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
9971 });
9972
9973 let mut cx = EditorTestContext::new(cx).await;
9974
9975 let language = Arc::new(Language::new(
9976 LanguageConfig {
9977 brackets: BracketPairConfig {
9978 pairs: vec![
9979 BracketPair {
9980 start: "{".to_string(),
9981 end: "}".to_string(),
9982 close: true,
9983 surround: true,
9984 newline: true,
9985 },
9986 BracketPair {
9987 start: "(".to_string(),
9988 end: ")".to_string(),
9989 close: true,
9990 surround: true,
9991 newline: true,
9992 },
9993 BracketPair {
9994 start: "[".to_string(),
9995 end: "]".to_string(),
9996 close: false,
9997 surround: false,
9998 newline: true,
9999 },
10000 ],
10001 ..Default::default()
10002 },
10003 autoclose_before: "})]".to_string(),
10004 ..Default::default()
10005 },
10006 Some(tree_sitter_rust::LANGUAGE.into()),
10007 ));
10008
10009 cx.language_registry().add(language.clone());
10010 cx.update_buffer(|buffer, cx| {
10011 buffer.set_language(Some(language), cx);
10012 });
10013
10014 cx.set_state(
10015 &"
10016 ˇ
10017 ˇ
10018 ˇ
10019 "
10020 .unindent(),
10021 );
10022
10023 // ensure only matching closing brackets are skipped over
10024 cx.update_editor(|editor, window, cx| {
10025 editor.handle_input("}", window, cx);
10026 editor.move_left(&MoveLeft, window, cx);
10027 editor.handle_input(")", window, cx);
10028 editor.move_left(&MoveLeft, window, cx);
10029 });
10030 cx.assert_editor_state(
10031 &"
10032 ˇ)}
10033 ˇ)}
10034 ˇ)}
10035 "
10036 .unindent(),
10037 );
10038
10039 // skip-over closing brackets at multiple cursors
10040 cx.update_editor(|editor, window, cx| {
10041 editor.handle_input(")", window, cx);
10042 editor.handle_input("}", window, cx);
10043 });
10044 cx.assert_editor_state(
10045 &"
10046 )}ˇ
10047 )}ˇ
10048 )}ˇ
10049 "
10050 .unindent(),
10051 );
10052
10053 // ignore non-close brackets
10054 cx.update_editor(|editor, window, cx| {
10055 editor.handle_input("]", window, cx);
10056 editor.move_left(&MoveLeft, window, cx);
10057 editor.handle_input("]", window, cx);
10058 });
10059 cx.assert_editor_state(
10060 &"
10061 )}]ˇ]
10062 )}]ˇ]
10063 )}]ˇ]
10064 "
10065 .unindent(),
10066 );
10067}
10068
10069#[gpui::test]
10070async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
10071 init_test(cx, |_| {});
10072
10073 let mut cx = EditorTestContext::new(cx).await;
10074
10075 let html_language = Arc::new(
10076 Language::new(
10077 LanguageConfig {
10078 name: "HTML".into(),
10079 brackets: BracketPairConfig {
10080 pairs: vec![
10081 BracketPair {
10082 start: "<".into(),
10083 end: ">".into(),
10084 close: true,
10085 ..Default::default()
10086 },
10087 BracketPair {
10088 start: "{".into(),
10089 end: "}".into(),
10090 close: true,
10091 ..Default::default()
10092 },
10093 BracketPair {
10094 start: "(".into(),
10095 end: ")".into(),
10096 close: true,
10097 ..Default::default()
10098 },
10099 ],
10100 ..Default::default()
10101 },
10102 autoclose_before: "})]>".into(),
10103 ..Default::default()
10104 },
10105 Some(tree_sitter_html::LANGUAGE.into()),
10106 )
10107 .with_injection_query(
10108 r#"
10109 (script_element
10110 (raw_text) @injection.content
10111 (#set! injection.language "javascript"))
10112 "#,
10113 )
10114 .unwrap(),
10115 );
10116
10117 let javascript_language = Arc::new(Language::new(
10118 LanguageConfig {
10119 name: "JavaScript".into(),
10120 brackets: BracketPairConfig {
10121 pairs: vec![
10122 BracketPair {
10123 start: "/*".into(),
10124 end: " */".into(),
10125 close: true,
10126 ..Default::default()
10127 },
10128 BracketPair {
10129 start: "{".into(),
10130 end: "}".into(),
10131 close: true,
10132 ..Default::default()
10133 },
10134 BracketPair {
10135 start: "(".into(),
10136 end: ")".into(),
10137 close: true,
10138 ..Default::default()
10139 },
10140 ],
10141 ..Default::default()
10142 },
10143 autoclose_before: "})]>".into(),
10144 ..Default::default()
10145 },
10146 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
10147 ));
10148
10149 cx.language_registry().add(html_language.clone());
10150 cx.language_registry().add(javascript_language);
10151 cx.executor().run_until_parked();
10152
10153 cx.update_buffer(|buffer, cx| {
10154 buffer.set_language(Some(html_language), cx);
10155 });
10156
10157 cx.set_state(
10158 &r#"
10159 <body>ˇ
10160 <script>
10161 var x = 1;ˇ
10162 </script>
10163 </body>ˇ
10164 "#
10165 .unindent(),
10166 );
10167
10168 // Precondition: different languages are active at different locations.
10169 cx.update_editor(|editor, window, cx| {
10170 let snapshot = editor.snapshot(window, cx);
10171 let cursors = editor.selections.ranges::<usize>(cx);
10172 let languages = cursors
10173 .iter()
10174 .map(|c| snapshot.language_at(c.start).unwrap().name())
10175 .collect::<Vec<_>>();
10176 assert_eq!(
10177 languages,
10178 &["HTML".into(), "JavaScript".into(), "HTML".into()]
10179 );
10180 });
10181
10182 // Angle brackets autoclose in HTML, but not JavaScript.
10183 cx.update_editor(|editor, window, cx| {
10184 editor.handle_input("<", window, cx);
10185 editor.handle_input("a", window, cx);
10186 });
10187 cx.assert_editor_state(
10188 &r#"
10189 <body><aˇ>
10190 <script>
10191 var x = 1;<aˇ
10192 </script>
10193 </body><aˇ>
10194 "#
10195 .unindent(),
10196 );
10197
10198 // Curly braces and parens autoclose in both HTML and JavaScript.
10199 cx.update_editor(|editor, window, cx| {
10200 editor.handle_input(" b=", window, cx);
10201 editor.handle_input("{", window, cx);
10202 editor.handle_input("c", window, cx);
10203 editor.handle_input("(", window, cx);
10204 });
10205 cx.assert_editor_state(
10206 &r#"
10207 <body><a b={c(ˇ)}>
10208 <script>
10209 var x = 1;<a b={c(ˇ)}
10210 </script>
10211 </body><a b={c(ˇ)}>
10212 "#
10213 .unindent(),
10214 );
10215
10216 // Brackets that were already autoclosed are skipped.
10217 cx.update_editor(|editor, window, cx| {
10218 editor.handle_input(")", window, cx);
10219 editor.handle_input("d", window, cx);
10220 editor.handle_input("}", window, cx);
10221 });
10222 cx.assert_editor_state(
10223 &r#"
10224 <body><a b={c()d}ˇ>
10225 <script>
10226 var x = 1;<a b={c()d}ˇ
10227 </script>
10228 </body><a b={c()d}ˇ>
10229 "#
10230 .unindent(),
10231 );
10232 cx.update_editor(|editor, window, cx| {
10233 editor.handle_input(">", window, cx);
10234 });
10235 cx.assert_editor_state(
10236 &r#"
10237 <body><a b={c()d}>ˇ
10238 <script>
10239 var x = 1;<a b={c()d}>ˇ
10240 </script>
10241 </body><a b={c()d}>ˇ
10242 "#
10243 .unindent(),
10244 );
10245
10246 // Reset
10247 cx.set_state(
10248 &r#"
10249 <body>ˇ
10250 <script>
10251 var x = 1;ˇ
10252 </script>
10253 </body>ˇ
10254 "#
10255 .unindent(),
10256 );
10257
10258 cx.update_editor(|editor, window, cx| {
10259 editor.handle_input("<", window, cx);
10260 });
10261 cx.assert_editor_state(
10262 &r#"
10263 <body><ˇ>
10264 <script>
10265 var x = 1;<ˇ
10266 </script>
10267 </body><ˇ>
10268 "#
10269 .unindent(),
10270 );
10271
10272 // When backspacing, the closing angle brackets are removed.
10273 cx.update_editor(|editor, window, cx| {
10274 editor.backspace(&Backspace, window, cx);
10275 });
10276 cx.assert_editor_state(
10277 &r#"
10278 <body>ˇ
10279 <script>
10280 var x = 1;ˇ
10281 </script>
10282 </body>ˇ
10283 "#
10284 .unindent(),
10285 );
10286
10287 // Block comments autoclose in JavaScript, but not HTML.
10288 cx.update_editor(|editor, window, cx| {
10289 editor.handle_input("/", window, cx);
10290 editor.handle_input("*", window, cx);
10291 });
10292 cx.assert_editor_state(
10293 &r#"
10294 <body>/*ˇ
10295 <script>
10296 var x = 1;/*ˇ */
10297 </script>
10298 </body>/*ˇ
10299 "#
10300 .unindent(),
10301 );
10302}
10303
10304#[gpui::test]
10305async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
10306 init_test(cx, |_| {});
10307
10308 let mut cx = EditorTestContext::new(cx).await;
10309
10310 let rust_language = Arc::new(
10311 Language::new(
10312 LanguageConfig {
10313 name: "Rust".into(),
10314 brackets: serde_json::from_value(json!([
10315 { "start": "{", "end": "}", "close": true, "newline": true },
10316 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
10317 ]))
10318 .unwrap(),
10319 autoclose_before: "})]>".into(),
10320 ..Default::default()
10321 },
10322 Some(tree_sitter_rust::LANGUAGE.into()),
10323 )
10324 .with_override_query("(string_literal) @string")
10325 .unwrap(),
10326 );
10327
10328 cx.language_registry().add(rust_language.clone());
10329 cx.update_buffer(|buffer, cx| {
10330 buffer.set_language(Some(rust_language), cx);
10331 });
10332
10333 cx.set_state(
10334 &r#"
10335 let x = ˇ
10336 "#
10337 .unindent(),
10338 );
10339
10340 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
10341 cx.update_editor(|editor, window, cx| {
10342 editor.handle_input("\"", window, cx);
10343 });
10344 cx.assert_editor_state(
10345 &r#"
10346 let x = "ˇ"
10347 "#
10348 .unindent(),
10349 );
10350
10351 // Inserting another quotation mark. The cursor moves across the existing
10352 // automatically-inserted quotation mark.
10353 cx.update_editor(|editor, window, cx| {
10354 editor.handle_input("\"", window, cx);
10355 });
10356 cx.assert_editor_state(
10357 &r#"
10358 let x = ""ˇ
10359 "#
10360 .unindent(),
10361 );
10362
10363 // Reset
10364 cx.set_state(
10365 &r#"
10366 let x = ˇ
10367 "#
10368 .unindent(),
10369 );
10370
10371 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
10372 cx.update_editor(|editor, window, cx| {
10373 editor.handle_input("\"", window, cx);
10374 editor.handle_input(" ", window, cx);
10375 editor.move_left(&Default::default(), window, cx);
10376 editor.handle_input("\\", window, cx);
10377 editor.handle_input("\"", window, cx);
10378 });
10379 cx.assert_editor_state(
10380 &r#"
10381 let x = "\"ˇ "
10382 "#
10383 .unindent(),
10384 );
10385
10386 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
10387 // mark. Nothing is inserted.
10388 cx.update_editor(|editor, window, cx| {
10389 editor.move_right(&Default::default(), window, cx);
10390 editor.handle_input("\"", window, cx);
10391 });
10392 cx.assert_editor_state(
10393 &r#"
10394 let x = "\" "ˇ
10395 "#
10396 .unindent(),
10397 );
10398}
10399
10400#[gpui::test]
10401async fn test_surround_with_pair(cx: &mut TestAppContext) {
10402 init_test(cx, |_| {});
10403
10404 let language = Arc::new(Language::new(
10405 LanguageConfig {
10406 brackets: BracketPairConfig {
10407 pairs: vec![
10408 BracketPair {
10409 start: "{".to_string(),
10410 end: "}".to_string(),
10411 close: true,
10412 surround: true,
10413 newline: true,
10414 },
10415 BracketPair {
10416 start: "/* ".to_string(),
10417 end: "*/".to_string(),
10418 close: true,
10419 surround: true,
10420 ..Default::default()
10421 },
10422 ],
10423 ..Default::default()
10424 },
10425 ..Default::default()
10426 },
10427 Some(tree_sitter_rust::LANGUAGE.into()),
10428 ));
10429
10430 let text = r#"
10431 a
10432 b
10433 c
10434 "#
10435 .unindent();
10436
10437 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10438 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10439 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10440 editor
10441 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10442 .await;
10443
10444 editor.update_in(cx, |editor, window, cx| {
10445 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10446 s.select_display_ranges([
10447 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10448 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10449 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
10450 ])
10451 });
10452
10453 editor.handle_input("{", window, cx);
10454 editor.handle_input("{", window, cx);
10455 editor.handle_input("{", window, cx);
10456 assert_eq!(
10457 editor.text(cx),
10458 "
10459 {{{a}}}
10460 {{{b}}}
10461 {{{c}}}
10462 "
10463 .unindent()
10464 );
10465 assert_eq!(
10466 editor.selections.display_ranges(cx),
10467 [
10468 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
10469 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
10470 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
10471 ]
10472 );
10473
10474 editor.undo(&Undo, window, cx);
10475 editor.undo(&Undo, window, cx);
10476 editor.undo(&Undo, window, cx);
10477 assert_eq!(
10478 editor.text(cx),
10479 "
10480 a
10481 b
10482 c
10483 "
10484 .unindent()
10485 );
10486 assert_eq!(
10487 editor.selections.display_ranges(cx),
10488 [
10489 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10490 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10491 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10492 ]
10493 );
10494
10495 // Ensure inserting the first character of a multi-byte bracket pair
10496 // doesn't surround the selections with the bracket.
10497 editor.handle_input("/", window, cx);
10498 assert_eq!(
10499 editor.text(cx),
10500 "
10501 /
10502 /
10503 /
10504 "
10505 .unindent()
10506 );
10507 assert_eq!(
10508 editor.selections.display_ranges(cx),
10509 [
10510 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10511 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10512 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10513 ]
10514 );
10515
10516 editor.undo(&Undo, window, cx);
10517 assert_eq!(
10518 editor.text(cx),
10519 "
10520 a
10521 b
10522 c
10523 "
10524 .unindent()
10525 );
10526 assert_eq!(
10527 editor.selections.display_ranges(cx),
10528 [
10529 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10530 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10531 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10532 ]
10533 );
10534
10535 // Ensure inserting the last character of a multi-byte bracket pair
10536 // doesn't surround the selections with the bracket.
10537 editor.handle_input("*", window, cx);
10538 assert_eq!(
10539 editor.text(cx),
10540 "
10541 *
10542 *
10543 *
10544 "
10545 .unindent()
10546 );
10547 assert_eq!(
10548 editor.selections.display_ranges(cx),
10549 [
10550 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10551 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10552 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10553 ]
10554 );
10555 });
10556}
10557
10558#[gpui::test]
10559async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
10560 init_test(cx, |_| {});
10561
10562 let language = Arc::new(Language::new(
10563 LanguageConfig {
10564 brackets: BracketPairConfig {
10565 pairs: vec![BracketPair {
10566 start: "{".to_string(),
10567 end: "}".to_string(),
10568 close: true,
10569 surround: true,
10570 newline: true,
10571 }],
10572 ..Default::default()
10573 },
10574 autoclose_before: "}".to_string(),
10575 ..Default::default()
10576 },
10577 Some(tree_sitter_rust::LANGUAGE.into()),
10578 ));
10579
10580 let text = r#"
10581 a
10582 b
10583 c
10584 "#
10585 .unindent();
10586
10587 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10588 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10589 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10590 editor
10591 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10592 .await;
10593
10594 editor.update_in(cx, |editor, window, cx| {
10595 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10596 s.select_ranges([
10597 Point::new(0, 1)..Point::new(0, 1),
10598 Point::new(1, 1)..Point::new(1, 1),
10599 Point::new(2, 1)..Point::new(2, 1),
10600 ])
10601 });
10602
10603 editor.handle_input("{", window, cx);
10604 editor.handle_input("{", window, cx);
10605 editor.handle_input("_", window, cx);
10606 assert_eq!(
10607 editor.text(cx),
10608 "
10609 a{{_}}
10610 b{{_}}
10611 c{{_}}
10612 "
10613 .unindent()
10614 );
10615 assert_eq!(
10616 editor.selections.ranges::<Point>(cx),
10617 [
10618 Point::new(0, 4)..Point::new(0, 4),
10619 Point::new(1, 4)..Point::new(1, 4),
10620 Point::new(2, 4)..Point::new(2, 4)
10621 ]
10622 );
10623
10624 editor.backspace(&Default::default(), window, cx);
10625 editor.backspace(&Default::default(), window, cx);
10626 assert_eq!(
10627 editor.text(cx),
10628 "
10629 a{}
10630 b{}
10631 c{}
10632 "
10633 .unindent()
10634 );
10635 assert_eq!(
10636 editor.selections.ranges::<Point>(cx),
10637 [
10638 Point::new(0, 2)..Point::new(0, 2),
10639 Point::new(1, 2)..Point::new(1, 2),
10640 Point::new(2, 2)..Point::new(2, 2)
10641 ]
10642 );
10643
10644 editor.delete_to_previous_word_start(&Default::default(), window, cx);
10645 assert_eq!(
10646 editor.text(cx),
10647 "
10648 a
10649 b
10650 c
10651 "
10652 .unindent()
10653 );
10654 assert_eq!(
10655 editor.selections.ranges::<Point>(cx),
10656 [
10657 Point::new(0, 1)..Point::new(0, 1),
10658 Point::new(1, 1)..Point::new(1, 1),
10659 Point::new(2, 1)..Point::new(2, 1)
10660 ]
10661 );
10662 });
10663}
10664
10665#[gpui::test]
10666async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
10667 init_test(cx, |settings| {
10668 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10669 });
10670
10671 let mut cx = EditorTestContext::new(cx).await;
10672
10673 let language = Arc::new(Language::new(
10674 LanguageConfig {
10675 brackets: BracketPairConfig {
10676 pairs: vec![
10677 BracketPair {
10678 start: "{".to_string(),
10679 end: "}".to_string(),
10680 close: true,
10681 surround: true,
10682 newline: true,
10683 },
10684 BracketPair {
10685 start: "(".to_string(),
10686 end: ")".to_string(),
10687 close: true,
10688 surround: true,
10689 newline: true,
10690 },
10691 BracketPair {
10692 start: "[".to_string(),
10693 end: "]".to_string(),
10694 close: false,
10695 surround: true,
10696 newline: true,
10697 },
10698 ],
10699 ..Default::default()
10700 },
10701 autoclose_before: "})]".to_string(),
10702 ..Default::default()
10703 },
10704 Some(tree_sitter_rust::LANGUAGE.into()),
10705 ));
10706
10707 cx.language_registry().add(language.clone());
10708 cx.update_buffer(|buffer, cx| {
10709 buffer.set_language(Some(language), cx);
10710 });
10711
10712 cx.set_state(
10713 &"
10714 {(ˇ)}
10715 [[ˇ]]
10716 {(ˇ)}
10717 "
10718 .unindent(),
10719 );
10720
10721 cx.update_editor(|editor, window, cx| {
10722 editor.backspace(&Default::default(), window, cx);
10723 editor.backspace(&Default::default(), window, cx);
10724 });
10725
10726 cx.assert_editor_state(
10727 &"
10728 ˇ
10729 ˇ]]
10730 ˇ
10731 "
10732 .unindent(),
10733 );
10734
10735 cx.update_editor(|editor, window, cx| {
10736 editor.handle_input("{", window, cx);
10737 editor.handle_input("{", window, cx);
10738 editor.move_right(&MoveRight, window, cx);
10739 editor.move_right(&MoveRight, window, cx);
10740 editor.move_left(&MoveLeft, window, cx);
10741 editor.move_left(&MoveLeft, window, cx);
10742 editor.backspace(&Default::default(), window, cx);
10743 });
10744
10745 cx.assert_editor_state(
10746 &"
10747 {ˇ}
10748 {ˇ}]]
10749 {ˇ}
10750 "
10751 .unindent(),
10752 );
10753
10754 cx.update_editor(|editor, window, cx| {
10755 editor.backspace(&Default::default(), window, cx);
10756 });
10757
10758 cx.assert_editor_state(
10759 &"
10760 ˇ
10761 ˇ]]
10762 ˇ
10763 "
10764 .unindent(),
10765 );
10766}
10767
10768#[gpui::test]
10769async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
10770 init_test(cx, |_| {});
10771
10772 let language = Arc::new(Language::new(
10773 LanguageConfig::default(),
10774 Some(tree_sitter_rust::LANGUAGE.into()),
10775 ));
10776
10777 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
10778 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10779 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10780 editor
10781 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10782 .await;
10783
10784 editor.update_in(cx, |editor, window, cx| {
10785 editor.set_auto_replace_emoji_shortcode(true);
10786
10787 editor.handle_input("Hello ", window, cx);
10788 editor.handle_input(":wave", window, cx);
10789 assert_eq!(editor.text(cx), "Hello :wave".unindent());
10790
10791 editor.handle_input(":", window, cx);
10792 assert_eq!(editor.text(cx), "Hello 👋".unindent());
10793
10794 editor.handle_input(" :smile", window, cx);
10795 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
10796
10797 editor.handle_input(":", window, cx);
10798 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
10799
10800 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
10801 editor.handle_input(":wave", window, cx);
10802 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
10803
10804 editor.handle_input(":", window, cx);
10805 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
10806
10807 editor.handle_input(":1", window, cx);
10808 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
10809
10810 editor.handle_input(":", window, cx);
10811 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
10812
10813 // Ensure shortcode does not get replaced when it is part of a word
10814 editor.handle_input(" Test:wave", window, cx);
10815 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
10816
10817 editor.handle_input(":", window, cx);
10818 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
10819
10820 editor.set_auto_replace_emoji_shortcode(false);
10821
10822 // Ensure shortcode does not get replaced when auto replace is off
10823 editor.handle_input(" :wave", window, cx);
10824 assert_eq!(
10825 editor.text(cx),
10826 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
10827 );
10828
10829 editor.handle_input(":", window, cx);
10830 assert_eq!(
10831 editor.text(cx),
10832 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
10833 );
10834 });
10835}
10836
10837#[gpui::test]
10838async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
10839 init_test(cx, |_| {});
10840
10841 let (text, insertion_ranges) = marked_text_ranges(
10842 indoc! {"
10843 ˇ
10844 "},
10845 false,
10846 );
10847
10848 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
10849 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10850
10851 _ = editor.update_in(cx, |editor, window, cx| {
10852 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
10853
10854 editor
10855 .insert_snippet(&insertion_ranges, snippet, window, cx)
10856 .unwrap();
10857
10858 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
10859 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
10860 assert_eq!(editor.text(cx), expected_text);
10861 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
10862 }
10863
10864 assert(
10865 editor,
10866 cx,
10867 indoc! {"
10868 type «» =•
10869 "},
10870 );
10871
10872 assert!(editor.context_menu_visible(), "There should be a matches");
10873 });
10874}
10875
10876#[gpui::test]
10877async fn test_snippets(cx: &mut TestAppContext) {
10878 init_test(cx, |_| {});
10879
10880 let mut cx = EditorTestContext::new(cx).await;
10881
10882 cx.set_state(indoc! {"
10883 a.ˇ b
10884 a.ˇ b
10885 a.ˇ b
10886 "});
10887
10888 cx.update_editor(|editor, window, cx| {
10889 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
10890 let insertion_ranges = editor
10891 .selections
10892 .all(cx)
10893 .iter()
10894 .map(|s| s.range())
10895 .collect::<Vec<_>>();
10896 editor
10897 .insert_snippet(&insertion_ranges, snippet, window, cx)
10898 .unwrap();
10899 });
10900
10901 cx.assert_editor_state(indoc! {"
10902 a.f(«oneˇ», two, «threeˇ») b
10903 a.f(«oneˇ», two, «threeˇ») b
10904 a.f(«oneˇ», two, «threeˇ») b
10905 "});
10906
10907 // Can't move earlier than the first tab stop
10908 cx.update_editor(|editor, window, cx| {
10909 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
10910 });
10911 cx.assert_editor_state(indoc! {"
10912 a.f(«oneˇ», two, «threeˇ») b
10913 a.f(«oneˇ», two, «threeˇ») b
10914 a.f(«oneˇ», two, «threeˇ») b
10915 "});
10916
10917 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10918 cx.assert_editor_state(indoc! {"
10919 a.f(one, «twoˇ», three) b
10920 a.f(one, «twoˇ», three) b
10921 a.f(one, «twoˇ», three) b
10922 "});
10923
10924 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
10925 cx.assert_editor_state(indoc! {"
10926 a.f(«oneˇ», two, «threeˇ») b
10927 a.f(«oneˇ», two, «threeˇ») b
10928 a.f(«oneˇ», two, «threeˇ») b
10929 "});
10930
10931 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10932 cx.assert_editor_state(indoc! {"
10933 a.f(one, «twoˇ», three) b
10934 a.f(one, «twoˇ», three) b
10935 a.f(one, «twoˇ», three) b
10936 "});
10937 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10938 cx.assert_editor_state(indoc! {"
10939 a.f(one, two, three)ˇ b
10940 a.f(one, two, three)ˇ b
10941 a.f(one, two, three)ˇ b
10942 "});
10943
10944 // As soon as the last tab stop is reached, snippet state is gone
10945 cx.update_editor(|editor, window, cx| {
10946 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
10947 });
10948 cx.assert_editor_state(indoc! {"
10949 a.f(one, two, three)ˇ b
10950 a.f(one, two, three)ˇ b
10951 a.f(one, two, three)ˇ b
10952 "});
10953}
10954
10955#[gpui::test]
10956async fn test_snippet_indentation(cx: &mut TestAppContext) {
10957 init_test(cx, |_| {});
10958
10959 let mut cx = EditorTestContext::new(cx).await;
10960
10961 cx.update_editor(|editor, window, cx| {
10962 let snippet = Snippet::parse(indoc! {"
10963 /*
10964 * Multiline comment with leading indentation
10965 *
10966 * $1
10967 */
10968 $0"})
10969 .unwrap();
10970 let insertion_ranges = editor
10971 .selections
10972 .all(cx)
10973 .iter()
10974 .map(|s| s.range())
10975 .collect::<Vec<_>>();
10976 editor
10977 .insert_snippet(&insertion_ranges, snippet, window, cx)
10978 .unwrap();
10979 });
10980
10981 cx.assert_editor_state(indoc! {"
10982 /*
10983 * Multiline comment with leading indentation
10984 *
10985 * ˇ
10986 */
10987 "});
10988
10989 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10990 cx.assert_editor_state(indoc! {"
10991 /*
10992 * Multiline comment with leading indentation
10993 *
10994 *•
10995 */
10996 ˇ"});
10997}
10998
10999#[gpui::test]
11000async fn test_document_format_during_save(cx: &mut TestAppContext) {
11001 init_test(cx, |_| {});
11002
11003 let fs = FakeFs::new(cx.executor());
11004 fs.insert_file(path!("/file.rs"), Default::default()).await;
11005
11006 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
11007
11008 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11009 language_registry.add(rust_lang());
11010 let mut fake_servers = language_registry.register_fake_lsp(
11011 "Rust",
11012 FakeLspAdapter {
11013 capabilities: lsp::ServerCapabilities {
11014 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11015 ..Default::default()
11016 },
11017 ..Default::default()
11018 },
11019 );
11020
11021 let buffer = project
11022 .update(cx, |project, cx| {
11023 project.open_local_buffer(path!("/file.rs"), cx)
11024 })
11025 .await
11026 .unwrap();
11027
11028 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11029 let (editor, cx) = cx.add_window_view(|window, cx| {
11030 build_editor_with_project(project.clone(), buffer, window, cx)
11031 });
11032 editor.update_in(cx, |editor, window, cx| {
11033 editor.set_text("one\ntwo\nthree\n", window, cx)
11034 });
11035 assert!(cx.read(|cx| editor.is_dirty(cx)));
11036
11037 cx.executor().start_waiting();
11038 let fake_server = fake_servers.next().await.unwrap();
11039
11040 {
11041 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11042 move |params, _| async move {
11043 assert_eq!(
11044 params.text_document.uri,
11045 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11046 );
11047 assert_eq!(params.options.tab_size, 4);
11048 Ok(Some(vec![lsp::TextEdit::new(
11049 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11050 ", ".to_string(),
11051 )]))
11052 },
11053 );
11054 let save = editor
11055 .update_in(cx, |editor, window, cx| {
11056 editor.save(
11057 SaveOptions {
11058 format: true,
11059 autosave: false,
11060 },
11061 project.clone(),
11062 window,
11063 cx,
11064 )
11065 })
11066 .unwrap();
11067 cx.executor().start_waiting();
11068 save.await;
11069
11070 assert_eq!(
11071 editor.update(cx, |editor, cx| editor.text(cx)),
11072 "one, two\nthree\n"
11073 );
11074 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11075 }
11076
11077 {
11078 editor.update_in(cx, |editor, window, cx| {
11079 editor.set_text("one\ntwo\nthree\n", window, cx)
11080 });
11081 assert!(cx.read(|cx| editor.is_dirty(cx)));
11082
11083 // Ensure we can still save even if formatting hangs.
11084 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11085 move |params, _| async move {
11086 assert_eq!(
11087 params.text_document.uri,
11088 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11089 );
11090 futures::future::pending::<()>().await;
11091 unreachable!()
11092 },
11093 );
11094 let save = editor
11095 .update_in(cx, |editor, window, cx| {
11096 editor.save(
11097 SaveOptions {
11098 format: true,
11099 autosave: false,
11100 },
11101 project.clone(),
11102 window,
11103 cx,
11104 )
11105 })
11106 .unwrap();
11107 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11108 cx.executor().start_waiting();
11109 save.await;
11110 assert_eq!(
11111 editor.update(cx, |editor, cx| editor.text(cx)),
11112 "one\ntwo\nthree\n"
11113 );
11114 }
11115
11116 // Set rust language override and assert overridden tabsize is sent to language server
11117 update_test_language_settings(cx, |settings| {
11118 settings.languages.0.insert(
11119 "Rust".into(),
11120 LanguageSettingsContent {
11121 tab_size: NonZeroU32::new(8),
11122 ..Default::default()
11123 },
11124 );
11125 });
11126
11127 {
11128 editor.update_in(cx, |editor, window, cx| {
11129 editor.set_text("somehting_new\n", window, cx)
11130 });
11131 assert!(cx.read(|cx| editor.is_dirty(cx)));
11132 let _formatting_request_signal = fake_server
11133 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
11134 assert_eq!(
11135 params.text_document.uri,
11136 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11137 );
11138 assert_eq!(params.options.tab_size, 8);
11139 Ok(Some(vec![]))
11140 });
11141 let save = editor
11142 .update_in(cx, |editor, window, cx| {
11143 editor.save(
11144 SaveOptions {
11145 format: true,
11146 autosave: false,
11147 },
11148 project.clone(),
11149 window,
11150 cx,
11151 )
11152 })
11153 .unwrap();
11154 cx.executor().start_waiting();
11155 save.await;
11156 }
11157}
11158
11159#[gpui::test]
11160async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
11161 init_test(cx, |settings| {
11162 settings.defaults.ensure_final_newline_on_save = Some(false);
11163 });
11164
11165 let fs = FakeFs::new(cx.executor());
11166 fs.insert_file(path!("/file.txt"), "foo".into()).await;
11167
11168 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
11169
11170 let buffer = project
11171 .update(cx, |project, cx| {
11172 project.open_local_buffer(path!("/file.txt"), cx)
11173 })
11174 .await
11175 .unwrap();
11176
11177 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11178 let (editor, cx) = cx.add_window_view(|window, cx| {
11179 build_editor_with_project(project.clone(), buffer, window, cx)
11180 });
11181 editor.update_in(cx, |editor, window, cx| {
11182 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
11183 s.select_ranges([0..0])
11184 });
11185 });
11186 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11187
11188 editor.update_in(cx, |editor, window, cx| {
11189 editor.handle_input("\n", window, cx)
11190 });
11191 cx.run_until_parked();
11192 save(&editor, &project, cx).await;
11193 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11194
11195 editor.update_in(cx, |editor, window, cx| {
11196 editor.undo(&Default::default(), window, cx);
11197 });
11198 save(&editor, &project, cx).await;
11199 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11200
11201 editor.update_in(cx, |editor, window, cx| {
11202 editor.redo(&Default::default(), window, cx);
11203 });
11204 cx.run_until_parked();
11205 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11206
11207 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
11208 let save = editor
11209 .update_in(cx, |editor, window, cx| {
11210 editor.save(
11211 SaveOptions {
11212 format: true,
11213 autosave: false,
11214 },
11215 project.clone(),
11216 window,
11217 cx,
11218 )
11219 })
11220 .unwrap();
11221 cx.executor().start_waiting();
11222 save.await;
11223 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11224 }
11225}
11226
11227#[gpui::test]
11228async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
11229 init_test(cx, |_| {});
11230
11231 let cols = 4;
11232 let rows = 10;
11233 let sample_text_1 = sample_text(rows, cols, 'a');
11234 assert_eq!(
11235 sample_text_1,
11236 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
11237 );
11238 let sample_text_2 = sample_text(rows, cols, 'l');
11239 assert_eq!(
11240 sample_text_2,
11241 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
11242 );
11243 let sample_text_3 = sample_text(rows, cols, 'v');
11244 assert_eq!(
11245 sample_text_3,
11246 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
11247 );
11248
11249 let fs = FakeFs::new(cx.executor());
11250 fs.insert_tree(
11251 path!("/a"),
11252 json!({
11253 "main.rs": sample_text_1,
11254 "other.rs": sample_text_2,
11255 "lib.rs": sample_text_3,
11256 }),
11257 )
11258 .await;
11259
11260 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11261 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11262 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11263
11264 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11265 language_registry.add(rust_lang());
11266 let mut fake_servers = language_registry.register_fake_lsp(
11267 "Rust",
11268 FakeLspAdapter {
11269 capabilities: lsp::ServerCapabilities {
11270 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11271 ..Default::default()
11272 },
11273 ..Default::default()
11274 },
11275 );
11276
11277 let worktree = project.update(cx, |project, cx| {
11278 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
11279 assert_eq!(worktrees.len(), 1);
11280 worktrees.pop().unwrap()
11281 });
11282 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11283
11284 let buffer_1 = project
11285 .update(cx, |project, cx| {
11286 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
11287 })
11288 .await
11289 .unwrap();
11290 let buffer_2 = project
11291 .update(cx, |project, cx| {
11292 project.open_buffer((worktree_id, rel_path("other.rs")), cx)
11293 })
11294 .await
11295 .unwrap();
11296 let buffer_3 = project
11297 .update(cx, |project, cx| {
11298 project.open_buffer((worktree_id, rel_path("lib.rs")), cx)
11299 })
11300 .await
11301 .unwrap();
11302
11303 let multi_buffer = cx.new(|cx| {
11304 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11305 multi_buffer.push_excerpts(
11306 buffer_1.clone(),
11307 [
11308 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11309 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11310 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11311 ],
11312 cx,
11313 );
11314 multi_buffer.push_excerpts(
11315 buffer_2.clone(),
11316 [
11317 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11318 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11319 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11320 ],
11321 cx,
11322 );
11323 multi_buffer.push_excerpts(
11324 buffer_3.clone(),
11325 [
11326 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11327 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11328 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11329 ],
11330 cx,
11331 );
11332 multi_buffer
11333 });
11334 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
11335 Editor::new(
11336 EditorMode::full(),
11337 multi_buffer,
11338 Some(project.clone()),
11339 window,
11340 cx,
11341 )
11342 });
11343
11344 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11345 editor.change_selections(
11346 SelectionEffects::scroll(Autoscroll::Next),
11347 window,
11348 cx,
11349 |s| s.select_ranges(Some(1..2)),
11350 );
11351 editor.insert("|one|two|three|", window, cx);
11352 });
11353 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11354 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11355 editor.change_selections(
11356 SelectionEffects::scroll(Autoscroll::Next),
11357 window,
11358 cx,
11359 |s| s.select_ranges(Some(60..70)),
11360 );
11361 editor.insert("|four|five|six|", window, cx);
11362 });
11363 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11364
11365 // First two buffers should be edited, but not the third one.
11366 assert_eq!(
11367 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11368 "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}",
11369 );
11370 buffer_1.update(cx, |buffer, _| {
11371 assert!(buffer.is_dirty());
11372 assert_eq!(
11373 buffer.text(),
11374 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
11375 )
11376 });
11377 buffer_2.update(cx, |buffer, _| {
11378 assert!(buffer.is_dirty());
11379 assert_eq!(
11380 buffer.text(),
11381 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
11382 )
11383 });
11384 buffer_3.update(cx, |buffer, _| {
11385 assert!(!buffer.is_dirty());
11386 assert_eq!(buffer.text(), sample_text_3,)
11387 });
11388 cx.executor().run_until_parked();
11389
11390 cx.executor().start_waiting();
11391 let save = multi_buffer_editor
11392 .update_in(cx, |editor, window, cx| {
11393 editor.save(
11394 SaveOptions {
11395 format: true,
11396 autosave: false,
11397 },
11398 project.clone(),
11399 window,
11400 cx,
11401 )
11402 })
11403 .unwrap();
11404
11405 let fake_server = fake_servers.next().await.unwrap();
11406 fake_server
11407 .server
11408 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
11409 Ok(Some(vec![lsp::TextEdit::new(
11410 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11411 format!("[{} formatted]", params.text_document.uri),
11412 )]))
11413 })
11414 .detach();
11415 save.await;
11416
11417 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
11418 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
11419 assert_eq!(
11420 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11421 uri!(
11422 "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}"
11423 ),
11424 );
11425 buffer_1.update(cx, |buffer, _| {
11426 assert!(!buffer.is_dirty());
11427 assert_eq!(
11428 buffer.text(),
11429 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
11430 )
11431 });
11432 buffer_2.update(cx, |buffer, _| {
11433 assert!(!buffer.is_dirty());
11434 assert_eq!(
11435 buffer.text(),
11436 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
11437 )
11438 });
11439 buffer_3.update(cx, |buffer, _| {
11440 assert!(!buffer.is_dirty());
11441 assert_eq!(buffer.text(), sample_text_3,)
11442 });
11443}
11444
11445#[gpui::test]
11446async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
11447 init_test(cx, |_| {});
11448
11449 let fs = FakeFs::new(cx.executor());
11450 fs.insert_tree(
11451 path!("/dir"),
11452 json!({
11453 "file1.rs": "fn main() { println!(\"hello\"); }",
11454 "file2.rs": "fn test() { println!(\"test\"); }",
11455 "file3.rs": "fn other() { println!(\"other\"); }\n",
11456 }),
11457 )
11458 .await;
11459
11460 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
11461 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11462 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11463
11464 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11465 language_registry.add(rust_lang());
11466
11467 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
11468 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11469
11470 // Open three buffers
11471 let buffer_1 = project
11472 .update(cx, |project, cx| {
11473 project.open_buffer((worktree_id, rel_path("file1.rs")), cx)
11474 })
11475 .await
11476 .unwrap();
11477 let buffer_2 = project
11478 .update(cx, |project, cx| {
11479 project.open_buffer((worktree_id, rel_path("file2.rs")), cx)
11480 })
11481 .await
11482 .unwrap();
11483 let buffer_3 = project
11484 .update(cx, |project, cx| {
11485 project.open_buffer((worktree_id, rel_path("file3.rs")), cx)
11486 })
11487 .await
11488 .unwrap();
11489
11490 // Create a multi-buffer with all three buffers
11491 let multi_buffer = cx.new(|cx| {
11492 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11493 multi_buffer.push_excerpts(
11494 buffer_1.clone(),
11495 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11496 cx,
11497 );
11498 multi_buffer.push_excerpts(
11499 buffer_2.clone(),
11500 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11501 cx,
11502 );
11503 multi_buffer.push_excerpts(
11504 buffer_3.clone(),
11505 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11506 cx,
11507 );
11508 multi_buffer
11509 });
11510
11511 let editor = cx.new_window_entity(|window, cx| {
11512 Editor::new(
11513 EditorMode::full(),
11514 multi_buffer,
11515 Some(project.clone()),
11516 window,
11517 cx,
11518 )
11519 });
11520
11521 // Edit only the first buffer
11522 editor.update_in(cx, |editor, window, cx| {
11523 editor.change_selections(
11524 SelectionEffects::scroll(Autoscroll::Next),
11525 window,
11526 cx,
11527 |s| s.select_ranges(Some(10..10)),
11528 );
11529 editor.insert("// edited", window, cx);
11530 });
11531
11532 // Verify that only buffer 1 is dirty
11533 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
11534 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11535 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11536
11537 // Get write counts after file creation (files were created with initial content)
11538 // We expect each file to have been written once during creation
11539 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
11540 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
11541 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
11542
11543 // Perform autosave
11544 let save_task = editor.update_in(cx, |editor, window, cx| {
11545 editor.save(
11546 SaveOptions {
11547 format: true,
11548 autosave: true,
11549 },
11550 project.clone(),
11551 window,
11552 cx,
11553 )
11554 });
11555 save_task.await.unwrap();
11556
11557 // Only the dirty buffer should have been saved
11558 assert_eq!(
11559 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11560 1,
11561 "Buffer 1 was dirty, so it should have been written once during autosave"
11562 );
11563 assert_eq!(
11564 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11565 0,
11566 "Buffer 2 was clean, so it should not have been written during autosave"
11567 );
11568 assert_eq!(
11569 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11570 0,
11571 "Buffer 3 was clean, so it should not have been written during autosave"
11572 );
11573
11574 // Verify buffer states after autosave
11575 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11576 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11577 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11578
11579 // Now perform a manual save (format = true)
11580 let save_task = editor.update_in(cx, |editor, window, cx| {
11581 editor.save(
11582 SaveOptions {
11583 format: true,
11584 autosave: false,
11585 },
11586 project.clone(),
11587 window,
11588 cx,
11589 )
11590 });
11591 save_task.await.unwrap();
11592
11593 // During manual save, clean buffers don't get written to disk
11594 // They just get did_save called for language server notifications
11595 assert_eq!(
11596 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11597 1,
11598 "Buffer 1 should only have been written once total (during autosave, not manual save)"
11599 );
11600 assert_eq!(
11601 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11602 0,
11603 "Buffer 2 should not have been written at all"
11604 );
11605 assert_eq!(
11606 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11607 0,
11608 "Buffer 3 should not have been written at all"
11609 );
11610}
11611
11612async fn setup_range_format_test(
11613 cx: &mut TestAppContext,
11614) -> (
11615 Entity<Project>,
11616 Entity<Editor>,
11617 &mut gpui::VisualTestContext,
11618 lsp::FakeLanguageServer,
11619) {
11620 init_test(cx, |_| {});
11621
11622 let fs = FakeFs::new(cx.executor());
11623 fs.insert_file(path!("/file.rs"), Default::default()).await;
11624
11625 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11626
11627 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11628 language_registry.add(rust_lang());
11629 let mut fake_servers = language_registry.register_fake_lsp(
11630 "Rust",
11631 FakeLspAdapter {
11632 capabilities: lsp::ServerCapabilities {
11633 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
11634 ..lsp::ServerCapabilities::default()
11635 },
11636 ..FakeLspAdapter::default()
11637 },
11638 );
11639
11640 let buffer = project
11641 .update(cx, |project, cx| {
11642 project.open_local_buffer(path!("/file.rs"), cx)
11643 })
11644 .await
11645 .unwrap();
11646
11647 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11648 let (editor, cx) = cx.add_window_view(|window, cx| {
11649 build_editor_with_project(project.clone(), buffer, window, cx)
11650 });
11651
11652 cx.executor().start_waiting();
11653 let fake_server = fake_servers.next().await.unwrap();
11654
11655 (project, editor, cx, fake_server)
11656}
11657
11658#[gpui::test]
11659async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
11660 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11661
11662 editor.update_in(cx, |editor, window, cx| {
11663 editor.set_text("one\ntwo\nthree\n", window, cx)
11664 });
11665 assert!(cx.read(|cx| editor.is_dirty(cx)));
11666
11667 let save = editor
11668 .update_in(cx, |editor, window, cx| {
11669 editor.save(
11670 SaveOptions {
11671 format: true,
11672 autosave: false,
11673 },
11674 project.clone(),
11675 window,
11676 cx,
11677 )
11678 })
11679 .unwrap();
11680 fake_server
11681 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
11682 assert_eq!(
11683 params.text_document.uri,
11684 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11685 );
11686 assert_eq!(params.options.tab_size, 4);
11687 Ok(Some(vec![lsp::TextEdit::new(
11688 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11689 ", ".to_string(),
11690 )]))
11691 })
11692 .next()
11693 .await;
11694 cx.executor().start_waiting();
11695 save.await;
11696 assert_eq!(
11697 editor.update(cx, |editor, cx| editor.text(cx)),
11698 "one, two\nthree\n"
11699 );
11700 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11701}
11702
11703#[gpui::test]
11704async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
11705 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11706
11707 editor.update_in(cx, |editor, window, cx| {
11708 editor.set_text("one\ntwo\nthree\n", window, cx)
11709 });
11710 assert!(cx.read(|cx| editor.is_dirty(cx)));
11711
11712 // Test that save still works when formatting hangs
11713 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
11714 move |params, _| async move {
11715 assert_eq!(
11716 params.text_document.uri,
11717 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11718 );
11719 futures::future::pending::<()>().await;
11720 unreachable!()
11721 },
11722 );
11723 let save = editor
11724 .update_in(cx, |editor, window, cx| {
11725 editor.save(
11726 SaveOptions {
11727 format: true,
11728 autosave: false,
11729 },
11730 project.clone(),
11731 window,
11732 cx,
11733 )
11734 })
11735 .unwrap();
11736 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11737 cx.executor().start_waiting();
11738 save.await;
11739 assert_eq!(
11740 editor.update(cx, |editor, cx| editor.text(cx)),
11741 "one\ntwo\nthree\n"
11742 );
11743 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11744}
11745
11746#[gpui::test]
11747async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
11748 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11749
11750 // Buffer starts clean, no formatting should be requested
11751 let save = editor
11752 .update_in(cx, |editor, window, cx| {
11753 editor.save(
11754 SaveOptions {
11755 format: false,
11756 autosave: false,
11757 },
11758 project.clone(),
11759 window,
11760 cx,
11761 )
11762 })
11763 .unwrap();
11764 let _pending_format_request = fake_server
11765 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
11766 panic!("Should not be invoked");
11767 })
11768 .next();
11769 cx.executor().start_waiting();
11770 save.await;
11771 cx.run_until_parked();
11772}
11773
11774#[gpui::test]
11775async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
11776 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11777
11778 // Set Rust language override and assert overridden tabsize is sent to language server
11779 update_test_language_settings(cx, |settings| {
11780 settings.languages.0.insert(
11781 "Rust".into(),
11782 LanguageSettingsContent {
11783 tab_size: NonZeroU32::new(8),
11784 ..Default::default()
11785 },
11786 );
11787 });
11788
11789 editor.update_in(cx, |editor, window, cx| {
11790 editor.set_text("something_new\n", window, cx)
11791 });
11792 assert!(cx.read(|cx| editor.is_dirty(cx)));
11793 let save = editor
11794 .update_in(cx, |editor, window, cx| {
11795 editor.save(
11796 SaveOptions {
11797 format: true,
11798 autosave: false,
11799 },
11800 project.clone(),
11801 window,
11802 cx,
11803 )
11804 })
11805 .unwrap();
11806 fake_server
11807 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
11808 assert_eq!(
11809 params.text_document.uri,
11810 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11811 );
11812 assert_eq!(params.options.tab_size, 8);
11813 Ok(Some(Vec::new()))
11814 })
11815 .next()
11816 .await;
11817 save.await;
11818}
11819
11820#[gpui::test]
11821async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
11822 init_test(cx, |settings| {
11823 settings.defaults.formatter = Some(FormatterList::Single(Formatter::LanguageServer(
11824 settings::LanguageServerFormatterSpecifier::Current,
11825 )))
11826 });
11827
11828 let fs = FakeFs::new(cx.executor());
11829 fs.insert_file(path!("/file.rs"), Default::default()).await;
11830
11831 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11832
11833 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11834 language_registry.add(Arc::new(Language::new(
11835 LanguageConfig {
11836 name: "Rust".into(),
11837 matcher: LanguageMatcher {
11838 path_suffixes: vec!["rs".to_string()],
11839 ..Default::default()
11840 },
11841 ..LanguageConfig::default()
11842 },
11843 Some(tree_sitter_rust::LANGUAGE.into()),
11844 )));
11845 update_test_language_settings(cx, |settings| {
11846 // Enable Prettier formatting for the same buffer, and ensure
11847 // LSP is called instead of Prettier.
11848 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
11849 });
11850 let mut fake_servers = language_registry.register_fake_lsp(
11851 "Rust",
11852 FakeLspAdapter {
11853 capabilities: lsp::ServerCapabilities {
11854 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11855 ..Default::default()
11856 },
11857 ..Default::default()
11858 },
11859 );
11860
11861 let buffer = project
11862 .update(cx, |project, cx| {
11863 project.open_local_buffer(path!("/file.rs"), cx)
11864 })
11865 .await
11866 .unwrap();
11867
11868 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11869 let (editor, cx) = cx.add_window_view(|window, cx| {
11870 build_editor_with_project(project.clone(), buffer, window, cx)
11871 });
11872 editor.update_in(cx, |editor, window, cx| {
11873 editor.set_text("one\ntwo\nthree\n", window, cx)
11874 });
11875
11876 cx.executor().start_waiting();
11877 let fake_server = fake_servers.next().await.unwrap();
11878
11879 let format = editor
11880 .update_in(cx, |editor, window, cx| {
11881 editor.perform_format(
11882 project.clone(),
11883 FormatTrigger::Manual,
11884 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
11885 window,
11886 cx,
11887 )
11888 })
11889 .unwrap();
11890 fake_server
11891 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
11892 assert_eq!(
11893 params.text_document.uri,
11894 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11895 );
11896 assert_eq!(params.options.tab_size, 4);
11897 Ok(Some(vec![lsp::TextEdit::new(
11898 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11899 ", ".to_string(),
11900 )]))
11901 })
11902 .next()
11903 .await;
11904 cx.executor().start_waiting();
11905 format.await;
11906 assert_eq!(
11907 editor.update(cx, |editor, cx| editor.text(cx)),
11908 "one, two\nthree\n"
11909 );
11910
11911 editor.update_in(cx, |editor, window, cx| {
11912 editor.set_text("one\ntwo\nthree\n", window, cx)
11913 });
11914 // Ensure we don't lock if formatting hangs.
11915 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11916 move |params, _| async move {
11917 assert_eq!(
11918 params.text_document.uri,
11919 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11920 );
11921 futures::future::pending::<()>().await;
11922 unreachable!()
11923 },
11924 );
11925 let format = editor
11926 .update_in(cx, |editor, window, cx| {
11927 editor.perform_format(
11928 project,
11929 FormatTrigger::Manual,
11930 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
11931 window,
11932 cx,
11933 )
11934 })
11935 .unwrap();
11936 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11937 cx.executor().start_waiting();
11938 format.await;
11939 assert_eq!(
11940 editor.update(cx, |editor, cx| editor.text(cx)),
11941 "one\ntwo\nthree\n"
11942 );
11943}
11944
11945#[gpui::test]
11946async fn test_multiple_formatters(cx: &mut TestAppContext) {
11947 init_test(cx, |settings| {
11948 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
11949 settings.defaults.formatter = Some(FormatterList::Vec(vec![
11950 Formatter::LanguageServer(settings::LanguageServerFormatterSpecifier::Current),
11951 Formatter::CodeAction("code-action-1".into()),
11952 Formatter::CodeAction("code-action-2".into()),
11953 ]))
11954 });
11955
11956 let fs = FakeFs::new(cx.executor());
11957 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
11958 .await;
11959
11960 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11961 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11962 language_registry.add(rust_lang());
11963
11964 let mut fake_servers = language_registry.register_fake_lsp(
11965 "Rust",
11966 FakeLspAdapter {
11967 capabilities: lsp::ServerCapabilities {
11968 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11969 execute_command_provider: Some(lsp::ExecuteCommandOptions {
11970 commands: vec!["the-command-for-code-action-1".into()],
11971 ..Default::default()
11972 }),
11973 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
11974 ..Default::default()
11975 },
11976 ..Default::default()
11977 },
11978 );
11979
11980 let buffer = project
11981 .update(cx, |project, cx| {
11982 project.open_local_buffer(path!("/file.rs"), cx)
11983 })
11984 .await
11985 .unwrap();
11986
11987 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11988 let (editor, cx) = cx.add_window_view(|window, cx| {
11989 build_editor_with_project(project.clone(), buffer, window, cx)
11990 });
11991
11992 cx.executor().start_waiting();
11993
11994 let fake_server = fake_servers.next().await.unwrap();
11995 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11996 move |_params, _| async move {
11997 Ok(Some(vec![lsp::TextEdit::new(
11998 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
11999 "applied-formatting\n".to_string(),
12000 )]))
12001 },
12002 );
12003 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12004 move |params, _| async move {
12005 let requested_code_actions = params.context.only.expect("Expected code action request");
12006 assert_eq!(requested_code_actions.len(), 1);
12007
12008 let uri = lsp::Uri::from_file_path(path!("/file.rs")).unwrap();
12009 let code_action = match requested_code_actions[0].as_str() {
12010 "code-action-1" => lsp::CodeAction {
12011 kind: Some("code-action-1".into()),
12012 edit: Some(lsp::WorkspaceEdit::new(
12013 [(
12014 uri,
12015 vec![lsp::TextEdit::new(
12016 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12017 "applied-code-action-1-edit\n".to_string(),
12018 )],
12019 )]
12020 .into_iter()
12021 .collect(),
12022 )),
12023 command: Some(lsp::Command {
12024 command: "the-command-for-code-action-1".into(),
12025 ..Default::default()
12026 }),
12027 ..Default::default()
12028 },
12029 "code-action-2" => lsp::CodeAction {
12030 kind: Some("code-action-2".into()),
12031 edit: Some(lsp::WorkspaceEdit::new(
12032 [(
12033 uri,
12034 vec![lsp::TextEdit::new(
12035 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12036 "applied-code-action-2-edit\n".to_string(),
12037 )],
12038 )]
12039 .into_iter()
12040 .collect(),
12041 )),
12042 ..Default::default()
12043 },
12044 req => panic!("Unexpected code action request: {:?}", req),
12045 };
12046 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12047 code_action,
12048 )]))
12049 },
12050 );
12051
12052 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
12053 move |params, _| async move { Ok(params) }
12054 });
12055
12056 let command_lock = Arc::new(futures::lock::Mutex::new(()));
12057 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
12058 let fake = fake_server.clone();
12059 let lock = command_lock.clone();
12060 move |params, _| {
12061 assert_eq!(params.command, "the-command-for-code-action-1");
12062 let fake = fake.clone();
12063 let lock = lock.clone();
12064 async move {
12065 lock.lock().await;
12066 fake.server
12067 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
12068 label: None,
12069 edit: lsp::WorkspaceEdit {
12070 changes: Some(
12071 [(
12072 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
12073 vec![lsp::TextEdit {
12074 range: lsp::Range::new(
12075 lsp::Position::new(0, 0),
12076 lsp::Position::new(0, 0),
12077 ),
12078 new_text: "applied-code-action-1-command\n".into(),
12079 }],
12080 )]
12081 .into_iter()
12082 .collect(),
12083 ),
12084 ..Default::default()
12085 },
12086 })
12087 .await
12088 .into_response()
12089 .unwrap();
12090 Ok(Some(json!(null)))
12091 }
12092 }
12093 });
12094
12095 cx.executor().start_waiting();
12096 editor
12097 .update_in(cx, |editor, window, cx| {
12098 editor.perform_format(
12099 project.clone(),
12100 FormatTrigger::Manual,
12101 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12102 window,
12103 cx,
12104 )
12105 })
12106 .unwrap()
12107 .await;
12108 editor.update(cx, |editor, cx| {
12109 assert_eq!(
12110 editor.text(cx),
12111 r#"
12112 applied-code-action-2-edit
12113 applied-code-action-1-command
12114 applied-code-action-1-edit
12115 applied-formatting
12116 one
12117 two
12118 three
12119 "#
12120 .unindent()
12121 );
12122 });
12123
12124 editor.update_in(cx, |editor, window, cx| {
12125 editor.undo(&Default::default(), window, cx);
12126 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12127 });
12128
12129 // Perform a manual edit while waiting for an LSP command
12130 // that's being run as part of a formatting code action.
12131 let lock_guard = command_lock.lock().await;
12132 let format = editor
12133 .update_in(cx, |editor, window, cx| {
12134 editor.perform_format(
12135 project.clone(),
12136 FormatTrigger::Manual,
12137 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12138 window,
12139 cx,
12140 )
12141 })
12142 .unwrap();
12143 cx.run_until_parked();
12144 editor.update(cx, |editor, cx| {
12145 assert_eq!(
12146 editor.text(cx),
12147 r#"
12148 applied-code-action-1-edit
12149 applied-formatting
12150 one
12151 two
12152 three
12153 "#
12154 .unindent()
12155 );
12156
12157 editor.buffer.update(cx, |buffer, cx| {
12158 let ix = buffer.len(cx);
12159 buffer.edit([(ix..ix, "edited\n")], None, cx);
12160 });
12161 });
12162
12163 // Allow the LSP command to proceed. Because the buffer was edited,
12164 // the second code action will not be run.
12165 drop(lock_guard);
12166 format.await;
12167 editor.update_in(cx, |editor, window, cx| {
12168 assert_eq!(
12169 editor.text(cx),
12170 r#"
12171 applied-code-action-1-command
12172 applied-code-action-1-edit
12173 applied-formatting
12174 one
12175 two
12176 three
12177 edited
12178 "#
12179 .unindent()
12180 );
12181
12182 // The manual edit is undone first, because it is the last thing the user did
12183 // (even though the command completed afterwards).
12184 editor.undo(&Default::default(), window, cx);
12185 assert_eq!(
12186 editor.text(cx),
12187 r#"
12188 applied-code-action-1-command
12189 applied-code-action-1-edit
12190 applied-formatting
12191 one
12192 two
12193 three
12194 "#
12195 .unindent()
12196 );
12197
12198 // All the formatting (including the command, which completed after the manual edit)
12199 // is undone together.
12200 editor.undo(&Default::default(), window, cx);
12201 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12202 });
12203}
12204
12205#[gpui::test]
12206async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
12207 init_test(cx, |settings| {
12208 settings.defaults.formatter = Some(FormatterList::Vec(vec![Formatter::LanguageServer(
12209 settings::LanguageServerFormatterSpecifier::Current,
12210 )]))
12211 });
12212
12213 let fs = FakeFs::new(cx.executor());
12214 fs.insert_file(path!("/file.ts"), Default::default()).await;
12215
12216 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12217
12218 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12219 language_registry.add(Arc::new(Language::new(
12220 LanguageConfig {
12221 name: "TypeScript".into(),
12222 matcher: LanguageMatcher {
12223 path_suffixes: vec!["ts".to_string()],
12224 ..Default::default()
12225 },
12226 ..LanguageConfig::default()
12227 },
12228 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12229 )));
12230 update_test_language_settings(cx, |settings| {
12231 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
12232 });
12233 let mut fake_servers = language_registry.register_fake_lsp(
12234 "TypeScript",
12235 FakeLspAdapter {
12236 capabilities: lsp::ServerCapabilities {
12237 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12238 ..Default::default()
12239 },
12240 ..Default::default()
12241 },
12242 );
12243
12244 let buffer = project
12245 .update(cx, |project, cx| {
12246 project.open_local_buffer(path!("/file.ts"), cx)
12247 })
12248 .await
12249 .unwrap();
12250
12251 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12252 let (editor, cx) = cx.add_window_view(|window, cx| {
12253 build_editor_with_project(project.clone(), buffer, window, cx)
12254 });
12255 editor.update_in(cx, |editor, window, cx| {
12256 editor.set_text(
12257 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12258 window,
12259 cx,
12260 )
12261 });
12262
12263 cx.executor().start_waiting();
12264 let fake_server = fake_servers.next().await.unwrap();
12265
12266 let format = editor
12267 .update_in(cx, |editor, window, cx| {
12268 editor.perform_code_action_kind(
12269 project.clone(),
12270 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12271 window,
12272 cx,
12273 )
12274 })
12275 .unwrap();
12276 fake_server
12277 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
12278 assert_eq!(
12279 params.text_document.uri,
12280 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12281 );
12282 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12283 lsp::CodeAction {
12284 title: "Organize Imports".to_string(),
12285 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
12286 edit: Some(lsp::WorkspaceEdit {
12287 changes: Some(
12288 [(
12289 params.text_document.uri.clone(),
12290 vec![lsp::TextEdit::new(
12291 lsp::Range::new(
12292 lsp::Position::new(1, 0),
12293 lsp::Position::new(2, 0),
12294 ),
12295 "".to_string(),
12296 )],
12297 )]
12298 .into_iter()
12299 .collect(),
12300 ),
12301 ..Default::default()
12302 }),
12303 ..Default::default()
12304 },
12305 )]))
12306 })
12307 .next()
12308 .await;
12309 cx.executor().start_waiting();
12310 format.await;
12311 assert_eq!(
12312 editor.update(cx, |editor, cx| editor.text(cx)),
12313 "import { a } from 'module';\n\nconst x = a;\n"
12314 );
12315
12316 editor.update_in(cx, |editor, window, cx| {
12317 editor.set_text(
12318 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12319 window,
12320 cx,
12321 )
12322 });
12323 // Ensure we don't lock if code action hangs.
12324 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12325 move |params, _| async move {
12326 assert_eq!(
12327 params.text_document.uri,
12328 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12329 );
12330 futures::future::pending::<()>().await;
12331 unreachable!()
12332 },
12333 );
12334 let format = editor
12335 .update_in(cx, |editor, window, cx| {
12336 editor.perform_code_action_kind(
12337 project,
12338 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12339 window,
12340 cx,
12341 )
12342 })
12343 .unwrap();
12344 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
12345 cx.executor().start_waiting();
12346 format.await;
12347 assert_eq!(
12348 editor.update(cx, |editor, cx| editor.text(cx)),
12349 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
12350 );
12351}
12352
12353#[gpui::test]
12354async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
12355 init_test(cx, |_| {});
12356
12357 let mut cx = EditorLspTestContext::new_rust(
12358 lsp::ServerCapabilities {
12359 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12360 ..Default::default()
12361 },
12362 cx,
12363 )
12364 .await;
12365
12366 cx.set_state(indoc! {"
12367 one.twoˇ
12368 "});
12369
12370 // The format request takes a long time. When it completes, it inserts
12371 // a newline and an indent before the `.`
12372 cx.lsp
12373 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
12374 let executor = cx.background_executor().clone();
12375 async move {
12376 executor.timer(Duration::from_millis(100)).await;
12377 Ok(Some(vec![lsp::TextEdit {
12378 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
12379 new_text: "\n ".into(),
12380 }]))
12381 }
12382 });
12383
12384 // Submit a format request.
12385 let format_1 = cx
12386 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12387 .unwrap();
12388 cx.executor().run_until_parked();
12389
12390 // Submit a second format request.
12391 let format_2 = cx
12392 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12393 .unwrap();
12394 cx.executor().run_until_parked();
12395
12396 // Wait for both format requests to complete
12397 cx.executor().advance_clock(Duration::from_millis(200));
12398 cx.executor().start_waiting();
12399 format_1.await.unwrap();
12400 cx.executor().start_waiting();
12401 format_2.await.unwrap();
12402
12403 // The formatting edits only happens once.
12404 cx.assert_editor_state(indoc! {"
12405 one
12406 .twoˇ
12407 "});
12408}
12409
12410#[gpui::test]
12411async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
12412 init_test(cx, |settings| {
12413 settings.defaults.formatter = Some(FormatterList::default())
12414 });
12415
12416 let mut cx = EditorLspTestContext::new_rust(
12417 lsp::ServerCapabilities {
12418 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12419 ..Default::default()
12420 },
12421 cx,
12422 )
12423 .await;
12424
12425 // Set up a buffer white some trailing whitespace and no trailing newline.
12426 cx.set_state(
12427 &[
12428 "one ", //
12429 "twoˇ", //
12430 "three ", //
12431 "four", //
12432 ]
12433 .join("\n"),
12434 );
12435
12436 // Submit a format request.
12437 let format = cx
12438 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12439 .unwrap();
12440
12441 // Record which buffer changes have been sent to the language server
12442 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
12443 cx.lsp
12444 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
12445 let buffer_changes = buffer_changes.clone();
12446 move |params, _| {
12447 buffer_changes.lock().extend(
12448 params
12449 .content_changes
12450 .into_iter()
12451 .map(|e| (e.range.unwrap(), e.text)),
12452 );
12453 }
12454 });
12455
12456 // Handle formatting requests to the language server.
12457 cx.lsp
12458 .set_request_handler::<lsp::request::Formatting, _, _>({
12459 let buffer_changes = buffer_changes.clone();
12460 move |_, _| {
12461 // When formatting is requested, trailing whitespace has already been stripped,
12462 // and the trailing newline has already been added.
12463 assert_eq!(
12464 &buffer_changes.lock()[1..],
12465 &[
12466 (
12467 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
12468 "".into()
12469 ),
12470 (
12471 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
12472 "".into()
12473 ),
12474 (
12475 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
12476 "\n".into()
12477 ),
12478 ]
12479 );
12480
12481 // Insert blank lines between each line of the buffer.
12482 async move {
12483 Ok(Some(vec![
12484 lsp::TextEdit {
12485 range: lsp::Range::new(
12486 lsp::Position::new(1, 0),
12487 lsp::Position::new(1, 0),
12488 ),
12489 new_text: "\n".into(),
12490 },
12491 lsp::TextEdit {
12492 range: lsp::Range::new(
12493 lsp::Position::new(2, 0),
12494 lsp::Position::new(2, 0),
12495 ),
12496 new_text: "\n".into(),
12497 },
12498 ]))
12499 }
12500 }
12501 });
12502
12503 // After formatting the buffer, the trailing whitespace is stripped,
12504 // a newline is appended, and the edits provided by the language server
12505 // have been applied.
12506 format.await.unwrap();
12507 cx.assert_editor_state(
12508 &[
12509 "one", //
12510 "", //
12511 "twoˇ", //
12512 "", //
12513 "three", //
12514 "four", //
12515 "", //
12516 ]
12517 .join("\n"),
12518 );
12519
12520 // Undoing the formatting undoes the trailing whitespace removal, the
12521 // trailing newline, and the LSP edits.
12522 cx.update_buffer(|buffer, cx| buffer.undo(cx));
12523 cx.assert_editor_state(
12524 &[
12525 "one ", //
12526 "twoˇ", //
12527 "three ", //
12528 "four", //
12529 ]
12530 .join("\n"),
12531 );
12532}
12533
12534#[gpui::test]
12535async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
12536 cx: &mut TestAppContext,
12537) {
12538 init_test(cx, |_| {});
12539
12540 cx.update(|cx| {
12541 cx.update_global::<SettingsStore, _>(|settings, cx| {
12542 settings.update_user_settings(cx, |settings| {
12543 settings.editor.auto_signature_help = Some(true);
12544 });
12545 });
12546 });
12547
12548 let mut cx = EditorLspTestContext::new_rust(
12549 lsp::ServerCapabilities {
12550 signature_help_provider: Some(lsp::SignatureHelpOptions {
12551 ..Default::default()
12552 }),
12553 ..Default::default()
12554 },
12555 cx,
12556 )
12557 .await;
12558
12559 let language = Language::new(
12560 LanguageConfig {
12561 name: "Rust".into(),
12562 brackets: BracketPairConfig {
12563 pairs: vec![
12564 BracketPair {
12565 start: "{".to_string(),
12566 end: "}".to_string(),
12567 close: true,
12568 surround: true,
12569 newline: true,
12570 },
12571 BracketPair {
12572 start: "(".to_string(),
12573 end: ")".to_string(),
12574 close: true,
12575 surround: true,
12576 newline: true,
12577 },
12578 BracketPair {
12579 start: "/*".to_string(),
12580 end: " */".to_string(),
12581 close: true,
12582 surround: true,
12583 newline: true,
12584 },
12585 BracketPair {
12586 start: "[".to_string(),
12587 end: "]".to_string(),
12588 close: false,
12589 surround: false,
12590 newline: true,
12591 },
12592 BracketPair {
12593 start: "\"".to_string(),
12594 end: "\"".to_string(),
12595 close: true,
12596 surround: true,
12597 newline: false,
12598 },
12599 BracketPair {
12600 start: "<".to_string(),
12601 end: ">".to_string(),
12602 close: false,
12603 surround: true,
12604 newline: true,
12605 },
12606 ],
12607 ..Default::default()
12608 },
12609 autoclose_before: "})]".to_string(),
12610 ..Default::default()
12611 },
12612 Some(tree_sitter_rust::LANGUAGE.into()),
12613 );
12614 let language = Arc::new(language);
12615
12616 cx.language_registry().add(language.clone());
12617 cx.update_buffer(|buffer, cx| {
12618 buffer.set_language(Some(language), cx);
12619 });
12620
12621 cx.set_state(
12622 &r#"
12623 fn main() {
12624 sampleˇ
12625 }
12626 "#
12627 .unindent(),
12628 );
12629
12630 cx.update_editor(|editor, window, cx| {
12631 editor.handle_input("(", window, cx);
12632 });
12633 cx.assert_editor_state(
12634 &"
12635 fn main() {
12636 sample(ˇ)
12637 }
12638 "
12639 .unindent(),
12640 );
12641
12642 let mocked_response = lsp::SignatureHelp {
12643 signatures: vec![lsp::SignatureInformation {
12644 label: "fn sample(param1: u8, param2: u8)".to_string(),
12645 documentation: None,
12646 parameters: Some(vec![
12647 lsp::ParameterInformation {
12648 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12649 documentation: None,
12650 },
12651 lsp::ParameterInformation {
12652 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12653 documentation: None,
12654 },
12655 ]),
12656 active_parameter: None,
12657 }],
12658 active_signature: Some(0),
12659 active_parameter: Some(0),
12660 };
12661 handle_signature_help_request(&mut cx, mocked_response).await;
12662
12663 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12664 .await;
12665
12666 cx.editor(|editor, _, _| {
12667 let signature_help_state = editor.signature_help_state.popover().cloned();
12668 let signature = signature_help_state.unwrap();
12669 assert_eq!(
12670 signature.signatures[signature.current_signature].label,
12671 "fn sample(param1: u8, param2: u8)"
12672 );
12673 });
12674}
12675
12676#[gpui::test]
12677async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
12678 init_test(cx, |_| {});
12679
12680 cx.update(|cx| {
12681 cx.update_global::<SettingsStore, _>(|settings, cx| {
12682 settings.update_user_settings(cx, |settings| {
12683 settings.editor.auto_signature_help = Some(false);
12684 settings.editor.show_signature_help_after_edits = Some(false);
12685 });
12686 });
12687 });
12688
12689 let mut cx = EditorLspTestContext::new_rust(
12690 lsp::ServerCapabilities {
12691 signature_help_provider: Some(lsp::SignatureHelpOptions {
12692 ..Default::default()
12693 }),
12694 ..Default::default()
12695 },
12696 cx,
12697 )
12698 .await;
12699
12700 let language = Language::new(
12701 LanguageConfig {
12702 name: "Rust".into(),
12703 brackets: BracketPairConfig {
12704 pairs: vec![
12705 BracketPair {
12706 start: "{".to_string(),
12707 end: "}".to_string(),
12708 close: true,
12709 surround: true,
12710 newline: true,
12711 },
12712 BracketPair {
12713 start: "(".to_string(),
12714 end: ")".to_string(),
12715 close: true,
12716 surround: true,
12717 newline: true,
12718 },
12719 BracketPair {
12720 start: "/*".to_string(),
12721 end: " */".to_string(),
12722 close: true,
12723 surround: true,
12724 newline: true,
12725 },
12726 BracketPair {
12727 start: "[".to_string(),
12728 end: "]".to_string(),
12729 close: false,
12730 surround: false,
12731 newline: true,
12732 },
12733 BracketPair {
12734 start: "\"".to_string(),
12735 end: "\"".to_string(),
12736 close: true,
12737 surround: true,
12738 newline: false,
12739 },
12740 BracketPair {
12741 start: "<".to_string(),
12742 end: ">".to_string(),
12743 close: false,
12744 surround: true,
12745 newline: true,
12746 },
12747 ],
12748 ..Default::default()
12749 },
12750 autoclose_before: "})]".to_string(),
12751 ..Default::default()
12752 },
12753 Some(tree_sitter_rust::LANGUAGE.into()),
12754 );
12755 let language = Arc::new(language);
12756
12757 cx.language_registry().add(language.clone());
12758 cx.update_buffer(|buffer, cx| {
12759 buffer.set_language(Some(language), cx);
12760 });
12761
12762 // Ensure that signature_help is not called when no signature help is enabled.
12763 cx.set_state(
12764 &r#"
12765 fn main() {
12766 sampleˇ
12767 }
12768 "#
12769 .unindent(),
12770 );
12771 cx.update_editor(|editor, window, cx| {
12772 editor.handle_input("(", window, cx);
12773 });
12774 cx.assert_editor_state(
12775 &"
12776 fn main() {
12777 sample(ˇ)
12778 }
12779 "
12780 .unindent(),
12781 );
12782 cx.editor(|editor, _, _| {
12783 assert!(editor.signature_help_state.task().is_none());
12784 });
12785
12786 let mocked_response = lsp::SignatureHelp {
12787 signatures: vec![lsp::SignatureInformation {
12788 label: "fn sample(param1: u8, param2: u8)".to_string(),
12789 documentation: None,
12790 parameters: Some(vec![
12791 lsp::ParameterInformation {
12792 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12793 documentation: None,
12794 },
12795 lsp::ParameterInformation {
12796 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12797 documentation: None,
12798 },
12799 ]),
12800 active_parameter: None,
12801 }],
12802 active_signature: Some(0),
12803 active_parameter: Some(0),
12804 };
12805
12806 // Ensure that signature_help is called when enabled afte edits
12807 cx.update(|_, cx| {
12808 cx.update_global::<SettingsStore, _>(|settings, cx| {
12809 settings.update_user_settings(cx, |settings| {
12810 settings.editor.auto_signature_help = Some(false);
12811 settings.editor.show_signature_help_after_edits = Some(true);
12812 });
12813 });
12814 });
12815 cx.set_state(
12816 &r#"
12817 fn main() {
12818 sampleˇ
12819 }
12820 "#
12821 .unindent(),
12822 );
12823 cx.update_editor(|editor, window, cx| {
12824 editor.handle_input("(", window, cx);
12825 });
12826 cx.assert_editor_state(
12827 &"
12828 fn main() {
12829 sample(ˇ)
12830 }
12831 "
12832 .unindent(),
12833 );
12834 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12835 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12836 .await;
12837 cx.update_editor(|editor, _, _| {
12838 let signature_help_state = editor.signature_help_state.popover().cloned();
12839 assert!(signature_help_state.is_some());
12840 let signature = signature_help_state.unwrap();
12841 assert_eq!(
12842 signature.signatures[signature.current_signature].label,
12843 "fn sample(param1: u8, param2: u8)"
12844 );
12845 editor.signature_help_state = SignatureHelpState::default();
12846 });
12847
12848 // Ensure that signature_help is called when auto signature help override is enabled
12849 cx.update(|_, cx| {
12850 cx.update_global::<SettingsStore, _>(|settings, cx| {
12851 settings.update_user_settings(cx, |settings| {
12852 settings.editor.auto_signature_help = Some(true);
12853 settings.editor.show_signature_help_after_edits = Some(false);
12854 });
12855 });
12856 });
12857 cx.set_state(
12858 &r#"
12859 fn main() {
12860 sampleˇ
12861 }
12862 "#
12863 .unindent(),
12864 );
12865 cx.update_editor(|editor, window, cx| {
12866 editor.handle_input("(", window, cx);
12867 });
12868 cx.assert_editor_state(
12869 &"
12870 fn main() {
12871 sample(ˇ)
12872 }
12873 "
12874 .unindent(),
12875 );
12876 handle_signature_help_request(&mut cx, mocked_response).await;
12877 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12878 .await;
12879 cx.editor(|editor, _, _| {
12880 let signature_help_state = editor.signature_help_state.popover().cloned();
12881 assert!(signature_help_state.is_some());
12882 let signature = signature_help_state.unwrap();
12883 assert_eq!(
12884 signature.signatures[signature.current_signature].label,
12885 "fn sample(param1: u8, param2: u8)"
12886 );
12887 });
12888}
12889
12890#[gpui::test]
12891async fn test_signature_help(cx: &mut TestAppContext) {
12892 init_test(cx, |_| {});
12893 cx.update(|cx| {
12894 cx.update_global::<SettingsStore, _>(|settings, cx| {
12895 settings.update_user_settings(cx, |settings| {
12896 settings.editor.auto_signature_help = Some(true);
12897 });
12898 });
12899 });
12900
12901 let mut cx = EditorLspTestContext::new_rust(
12902 lsp::ServerCapabilities {
12903 signature_help_provider: Some(lsp::SignatureHelpOptions {
12904 ..Default::default()
12905 }),
12906 ..Default::default()
12907 },
12908 cx,
12909 )
12910 .await;
12911
12912 // A test that directly calls `show_signature_help`
12913 cx.update_editor(|editor, window, cx| {
12914 editor.show_signature_help(&ShowSignatureHelp, window, cx);
12915 });
12916
12917 let mocked_response = lsp::SignatureHelp {
12918 signatures: vec![lsp::SignatureInformation {
12919 label: "fn sample(param1: u8, param2: u8)".to_string(),
12920 documentation: None,
12921 parameters: Some(vec![
12922 lsp::ParameterInformation {
12923 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12924 documentation: None,
12925 },
12926 lsp::ParameterInformation {
12927 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12928 documentation: None,
12929 },
12930 ]),
12931 active_parameter: None,
12932 }],
12933 active_signature: Some(0),
12934 active_parameter: Some(0),
12935 };
12936 handle_signature_help_request(&mut cx, mocked_response).await;
12937
12938 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12939 .await;
12940
12941 cx.editor(|editor, _, _| {
12942 let signature_help_state = editor.signature_help_state.popover().cloned();
12943 assert!(signature_help_state.is_some());
12944 let signature = signature_help_state.unwrap();
12945 assert_eq!(
12946 signature.signatures[signature.current_signature].label,
12947 "fn sample(param1: u8, param2: u8)"
12948 );
12949 });
12950
12951 // When exiting outside from inside the brackets, `signature_help` is closed.
12952 cx.set_state(indoc! {"
12953 fn main() {
12954 sample(ˇ);
12955 }
12956
12957 fn sample(param1: u8, param2: u8) {}
12958 "});
12959
12960 cx.update_editor(|editor, window, cx| {
12961 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12962 s.select_ranges([0..0])
12963 });
12964 });
12965
12966 let mocked_response = lsp::SignatureHelp {
12967 signatures: Vec::new(),
12968 active_signature: None,
12969 active_parameter: None,
12970 };
12971 handle_signature_help_request(&mut cx, mocked_response).await;
12972
12973 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
12974 .await;
12975
12976 cx.editor(|editor, _, _| {
12977 assert!(!editor.signature_help_state.is_shown());
12978 });
12979
12980 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
12981 cx.set_state(indoc! {"
12982 fn main() {
12983 sample(ˇ);
12984 }
12985
12986 fn sample(param1: u8, param2: u8) {}
12987 "});
12988
12989 let mocked_response = lsp::SignatureHelp {
12990 signatures: vec![lsp::SignatureInformation {
12991 label: "fn sample(param1: u8, param2: u8)".to_string(),
12992 documentation: None,
12993 parameters: Some(vec![
12994 lsp::ParameterInformation {
12995 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12996 documentation: None,
12997 },
12998 lsp::ParameterInformation {
12999 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13000 documentation: None,
13001 },
13002 ]),
13003 active_parameter: None,
13004 }],
13005 active_signature: Some(0),
13006 active_parameter: Some(0),
13007 };
13008 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13009 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13010 .await;
13011 cx.editor(|editor, _, _| {
13012 assert!(editor.signature_help_state.is_shown());
13013 });
13014
13015 // Restore the popover with more parameter input
13016 cx.set_state(indoc! {"
13017 fn main() {
13018 sample(param1, param2ˇ);
13019 }
13020
13021 fn sample(param1: u8, param2: u8) {}
13022 "});
13023
13024 let mocked_response = lsp::SignatureHelp {
13025 signatures: vec![lsp::SignatureInformation {
13026 label: "fn sample(param1: u8, param2: u8)".to_string(),
13027 documentation: None,
13028 parameters: Some(vec![
13029 lsp::ParameterInformation {
13030 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13031 documentation: None,
13032 },
13033 lsp::ParameterInformation {
13034 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13035 documentation: None,
13036 },
13037 ]),
13038 active_parameter: None,
13039 }],
13040 active_signature: Some(0),
13041 active_parameter: Some(1),
13042 };
13043 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13044 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13045 .await;
13046
13047 // When selecting a range, the popover is gone.
13048 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
13049 cx.update_editor(|editor, window, cx| {
13050 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13051 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13052 })
13053 });
13054 cx.assert_editor_state(indoc! {"
13055 fn main() {
13056 sample(param1, «ˇparam2»);
13057 }
13058
13059 fn sample(param1: u8, param2: u8) {}
13060 "});
13061 cx.editor(|editor, _, _| {
13062 assert!(!editor.signature_help_state.is_shown());
13063 });
13064
13065 // When unselecting again, the popover is back if within the brackets.
13066 cx.update_editor(|editor, window, cx| {
13067 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13068 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13069 })
13070 });
13071 cx.assert_editor_state(indoc! {"
13072 fn main() {
13073 sample(param1, ˇparam2);
13074 }
13075
13076 fn sample(param1: u8, param2: u8) {}
13077 "});
13078 handle_signature_help_request(&mut cx, mocked_response).await;
13079 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13080 .await;
13081 cx.editor(|editor, _, _| {
13082 assert!(editor.signature_help_state.is_shown());
13083 });
13084
13085 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
13086 cx.update_editor(|editor, window, cx| {
13087 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13088 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
13089 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13090 })
13091 });
13092 cx.assert_editor_state(indoc! {"
13093 fn main() {
13094 sample(param1, ˇparam2);
13095 }
13096
13097 fn sample(param1: u8, param2: u8) {}
13098 "});
13099
13100 let mocked_response = lsp::SignatureHelp {
13101 signatures: vec![lsp::SignatureInformation {
13102 label: "fn sample(param1: u8, param2: u8)".to_string(),
13103 documentation: None,
13104 parameters: Some(vec![
13105 lsp::ParameterInformation {
13106 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13107 documentation: None,
13108 },
13109 lsp::ParameterInformation {
13110 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13111 documentation: None,
13112 },
13113 ]),
13114 active_parameter: None,
13115 }],
13116 active_signature: Some(0),
13117 active_parameter: Some(1),
13118 };
13119 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13120 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13121 .await;
13122 cx.update_editor(|editor, _, cx| {
13123 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
13124 });
13125 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13126 .await;
13127 cx.update_editor(|editor, window, cx| {
13128 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13129 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13130 })
13131 });
13132 cx.assert_editor_state(indoc! {"
13133 fn main() {
13134 sample(param1, «ˇparam2»);
13135 }
13136
13137 fn sample(param1: u8, param2: u8) {}
13138 "});
13139 cx.update_editor(|editor, window, cx| {
13140 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13141 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13142 })
13143 });
13144 cx.assert_editor_state(indoc! {"
13145 fn main() {
13146 sample(param1, ˇparam2);
13147 }
13148
13149 fn sample(param1: u8, param2: u8) {}
13150 "});
13151 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
13152 .await;
13153}
13154
13155#[gpui::test]
13156async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
13157 init_test(cx, |_| {});
13158
13159 let mut cx = EditorLspTestContext::new_rust(
13160 lsp::ServerCapabilities {
13161 signature_help_provider: Some(lsp::SignatureHelpOptions {
13162 ..Default::default()
13163 }),
13164 ..Default::default()
13165 },
13166 cx,
13167 )
13168 .await;
13169
13170 cx.set_state(indoc! {"
13171 fn main() {
13172 overloadedˇ
13173 }
13174 "});
13175
13176 cx.update_editor(|editor, window, cx| {
13177 editor.handle_input("(", window, cx);
13178 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13179 });
13180
13181 // Mock response with 3 signatures
13182 let mocked_response = lsp::SignatureHelp {
13183 signatures: vec![
13184 lsp::SignatureInformation {
13185 label: "fn overloaded(x: i32)".to_string(),
13186 documentation: None,
13187 parameters: Some(vec![lsp::ParameterInformation {
13188 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13189 documentation: None,
13190 }]),
13191 active_parameter: None,
13192 },
13193 lsp::SignatureInformation {
13194 label: "fn overloaded(x: i32, y: i32)".to_string(),
13195 documentation: None,
13196 parameters: Some(vec![
13197 lsp::ParameterInformation {
13198 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13199 documentation: None,
13200 },
13201 lsp::ParameterInformation {
13202 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13203 documentation: None,
13204 },
13205 ]),
13206 active_parameter: None,
13207 },
13208 lsp::SignatureInformation {
13209 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
13210 documentation: None,
13211 parameters: Some(vec![
13212 lsp::ParameterInformation {
13213 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13214 documentation: None,
13215 },
13216 lsp::ParameterInformation {
13217 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13218 documentation: None,
13219 },
13220 lsp::ParameterInformation {
13221 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
13222 documentation: None,
13223 },
13224 ]),
13225 active_parameter: None,
13226 },
13227 ],
13228 active_signature: Some(1),
13229 active_parameter: Some(0),
13230 };
13231 handle_signature_help_request(&mut cx, mocked_response).await;
13232
13233 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13234 .await;
13235
13236 // Verify we have multiple signatures and the right one is selected
13237 cx.editor(|editor, _, _| {
13238 let popover = editor.signature_help_state.popover().cloned().unwrap();
13239 assert_eq!(popover.signatures.len(), 3);
13240 // active_signature was 1, so that should be the current
13241 assert_eq!(popover.current_signature, 1);
13242 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
13243 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
13244 assert_eq!(
13245 popover.signatures[2].label,
13246 "fn overloaded(x: i32, y: i32, z: i32)"
13247 );
13248 });
13249
13250 // Test navigation functionality
13251 cx.update_editor(|editor, window, cx| {
13252 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13253 });
13254
13255 cx.editor(|editor, _, _| {
13256 let popover = editor.signature_help_state.popover().cloned().unwrap();
13257 assert_eq!(popover.current_signature, 2);
13258 });
13259
13260 // Test wrap around
13261 cx.update_editor(|editor, window, cx| {
13262 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13263 });
13264
13265 cx.editor(|editor, _, _| {
13266 let popover = editor.signature_help_state.popover().cloned().unwrap();
13267 assert_eq!(popover.current_signature, 0);
13268 });
13269
13270 // Test previous navigation
13271 cx.update_editor(|editor, window, cx| {
13272 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
13273 });
13274
13275 cx.editor(|editor, _, _| {
13276 let popover = editor.signature_help_state.popover().cloned().unwrap();
13277 assert_eq!(popover.current_signature, 2);
13278 });
13279}
13280
13281#[gpui::test]
13282async fn test_completion_mode(cx: &mut TestAppContext) {
13283 init_test(cx, |_| {});
13284 let mut cx = EditorLspTestContext::new_rust(
13285 lsp::ServerCapabilities {
13286 completion_provider: Some(lsp::CompletionOptions {
13287 resolve_provider: Some(true),
13288 ..Default::default()
13289 }),
13290 ..Default::default()
13291 },
13292 cx,
13293 )
13294 .await;
13295
13296 struct Run {
13297 run_description: &'static str,
13298 initial_state: String,
13299 buffer_marked_text: String,
13300 completion_label: &'static str,
13301 completion_text: &'static str,
13302 expected_with_insert_mode: String,
13303 expected_with_replace_mode: String,
13304 expected_with_replace_subsequence_mode: String,
13305 expected_with_replace_suffix_mode: String,
13306 }
13307
13308 let runs = [
13309 Run {
13310 run_description: "Start of word matches completion text",
13311 initial_state: "before ediˇ after".into(),
13312 buffer_marked_text: "before <edi|> after".into(),
13313 completion_label: "editor",
13314 completion_text: "editor",
13315 expected_with_insert_mode: "before editorˇ after".into(),
13316 expected_with_replace_mode: "before editorˇ after".into(),
13317 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13318 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13319 },
13320 Run {
13321 run_description: "Accept same text at the middle of the word",
13322 initial_state: "before ediˇtor after".into(),
13323 buffer_marked_text: "before <edi|tor> after".into(),
13324 completion_label: "editor",
13325 completion_text: "editor",
13326 expected_with_insert_mode: "before editorˇtor after".into(),
13327 expected_with_replace_mode: "before editorˇ after".into(),
13328 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13329 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13330 },
13331 Run {
13332 run_description: "End of word matches completion text -- cursor at end",
13333 initial_state: "before torˇ after".into(),
13334 buffer_marked_text: "before <tor|> after".into(),
13335 completion_label: "editor",
13336 completion_text: "editor",
13337 expected_with_insert_mode: "before editorˇ after".into(),
13338 expected_with_replace_mode: "before editorˇ after".into(),
13339 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13340 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13341 },
13342 Run {
13343 run_description: "End of word matches completion text -- cursor at start",
13344 initial_state: "before ˇtor after".into(),
13345 buffer_marked_text: "before <|tor> after".into(),
13346 completion_label: "editor",
13347 completion_text: "editor",
13348 expected_with_insert_mode: "before editorˇtor after".into(),
13349 expected_with_replace_mode: "before editorˇ after".into(),
13350 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13351 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13352 },
13353 Run {
13354 run_description: "Prepend text containing whitespace",
13355 initial_state: "pˇfield: bool".into(),
13356 buffer_marked_text: "<p|field>: bool".into(),
13357 completion_label: "pub ",
13358 completion_text: "pub ",
13359 expected_with_insert_mode: "pub ˇfield: bool".into(),
13360 expected_with_replace_mode: "pub ˇ: bool".into(),
13361 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
13362 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
13363 },
13364 Run {
13365 run_description: "Add element to start of list",
13366 initial_state: "[element_ˇelement_2]".into(),
13367 buffer_marked_text: "[<element_|element_2>]".into(),
13368 completion_label: "element_1",
13369 completion_text: "element_1",
13370 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
13371 expected_with_replace_mode: "[element_1ˇ]".into(),
13372 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
13373 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
13374 },
13375 Run {
13376 run_description: "Add element to start of list -- first and second elements are equal",
13377 initial_state: "[elˇelement]".into(),
13378 buffer_marked_text: "[<el|element>]".into(),
13379 completion_label: "element",
13380 completion_text: "element",
13381 expected_with_insert_mode: "[elementˇelement]".into(),
13382 expected_with_replace_mode: "[elementˇ]".into(),
13383 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
13384 expected_with_replace_suffix_mode: "[elementˇ]".into(),
13385 },
13386 Run {
13387 run_description: "Ends with matching suffix",
13388 initial_state: "SubˇError".into(),
13389 buffer_marked_text: "<Sub|Error>".into(),
13390 completion_label: "SubscriptionError",
13391 completion_text: "SubscriptionError",
13392 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
13393 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13394 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13395 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
13396 },
13397 Run {
13398 run_description: "Suffix is a subsequence -- contiguous",
13399 initial_state: "SubˇErr".into(),
13400 buffer_marked_text: "<Sub|Err>".into(),
13401 completion_label: "SubscriptionError",
13402 completion_text: "SubscriptionError",
13403 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
13404 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13405 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13406 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
13407 },
13408 Run {
13409 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
13410 initial_state: "Suˇscrirr".into(),
13411 buffer_marked_text: "<Su|scrirr>".into(),
13412 completion_label: "SubscriptionError",
13413 completion_text: "SubscriptionError",
13414 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
13415 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13416 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13417 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
13418 },
13419 Run {
13420 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
13421 initial_state: "foo(indˇix)".into(),
13422 buffer_marked_text: "foo(<ind|ix>)".into(),
13423 completion_label: "node_index",
13424 completion_text: "node_index",
13425 expected_with_insert_mode: "foo(node_indexˇix)".into(),
13426 expected_with_replace_mode: "foo(node_indexˇ)".into(),
13427 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
13428 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
13429 },
13430 Run {
13431 run_description: "Replace range ends before cursor - should extend to cursor",
13432 initial_state: "before editˇo after".into(),
13433 buffer_marked_text: "before <{ed}>it|o after".into(),
13434 completion_label: "editor",
13435 completion_text: "editor",
13436 expected_with_insert_mode: "before editorˇo after".into(),
13437 expected_with_replace_mode: "before editorˇo after".into(),
13438 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
13439 expected_with_replace_suffix_mode: "before editorˇo after".into(),
13440 },
13441 Run {
13442 run_description: "Uses label for suffix matching",
13443 initial_state: "before ediˇtor after".into(),
13444 buffer_marked_text: "before <edi|tor> after".into(),
13445 completion_label: "editor",
13446 completion_text: "editor()",
13447 expected_with_insert_mode: "before editor()ˇtor after".into(),
13448 expected_with_replace_mode: "before editor()ˇ after".into(),
13449 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
13450 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
13451 },
13452 Run {
13453 run_description: "Case insensitive subsequence and suffix matching",
13454 initial_state: "before EDiˇtoR after".into(),
13455 buffer_marked_text: "before <EDi|toR> after".into(),
13456 completion_label: "editor",
13457 completion_text: "editor",
13458 expected_with_insert_mode: "before editorˇtoR after".into(),
13459 expected_with_replace_mode: "before editorˇ after".into(),
13460 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13461 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13462 },
13463 ];
13464
13465 for run in runs {
13466 let run_variations = [
13467 (LspInsertMode::Insert, run.expected_with_insert_mode),
13468 (LspInsertMode::Replace, run.expected_with_replace_mode),
13469 (
13470 LspInsertMode::ReplaceSubsequence,
13471 run.expected_with_replace_subsequence_mode,
13472 ),
13473 (
13474 LspInsertMode::ReplaceSuffix,
13475 run.expected_with_replace_suffix_mode,
13476 ),
13477 ];
13478
13479 for (lsp_insert_mode, expected_text) in run_variations {
13480 eprintln!(
13481 "run = {:?}, mode = {lsp_insert_mode:.?}",
13482 run.run_description,
13483 );
13484
13485 update_test_language_settings(&mut cx, |settings| {
13486 settings.defaults.completions = Some(CompletionSettingsContent {
13487 lsp_insert_mode: Some(lsp_insert_mode),
13488 words: Some(WordsCompletionMode::Disabled),
13489 words_min_length: Some(0),
13490 ..Default::default()
13491 });
13492 });
13493
13494 cx.set_state(&run.initial_state);
13495 cx.update_editor(|editor, window, cx| {
13496 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13497 });
13498
13499 let counter = Arc::new(AtomicUsize::new(0));
13500 handle_completion_request_with_insert_and_replace(
13501 &mut cx,
13502 &run.buffer_marked_text,
13503 vec![(run.completion_label, run.completion_text)],
13504 counter.clone(),
13505 )
13506 .await;
13507 cx.condition(|editor, _| editor.context_menu_visible())
13508 .await;
13509 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13510
13511 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13512 editor
13513 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13514 .unwrap()
13515 });
13516 cx.assert_editor_state(&expected_text);
13517 handle_resolve_completion_request(&mut cx, None).await;
13518 apply_additional_edits.await.unwrap();
13519 }
13520 }
13521}
13522
13523#[gpui::test]
13524async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
13525 init_test(cx, |_| {});
13526 let mut cx = EditorLspTestContext::new_rust(
13527 lsp::ServerCapabilities {
13528 completion_provider: Some(lsp::CompletionOptions {
13529 resolve_provider: Some(true),
13530 ..Default::default()
13531 }),
13532 ..Default::default()
13533 },
13534 cx,
13535 )
13536 .await;
13537
13538 let initial_state = "SubˇError";
13539 let buffer_marked_text = "<Sub|Error>";
13540 let completion_text = "SubscriptionError";
13541 let expected_with_insert_mode = "SubscriptionErrorˇError";
13542 let expected_with_replace_mode = "SubscriptionErrorˇ";
13543
13544 update_test_language_settings(&mut cx, |settings| {
13545 settings.defaults.completions = Some(CompletionSettingsContent {
13546 words: Some(WordsCompletionMode::Disabled),
13547 words_min_length: Some(0),
13548 // set the opposite here to ensure that the action is overriding the default behavior
13549 lsp_insert_mode: Some(LspInsertMode::Insert),
13550 ..Default::default()
13551 });
13552 });
13553
13554 cx.set_state(initial_state);
13555 cx.update_editor(|editor, window, cx| {
13556 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13557 });
13558
13559 let counter = Arc::new(AtomicUsize::new(0));
13560 handle_completion_request_with_insert_and_replace(
13561 &mut cx,
13562 buffer_marked_text,
13563 vec![(completion_text, completion_text)],
13564 counter.clone(),
13565 )
13566 .await;
13567 cx.condition(|editor, _| editor.context_menu_visible())
13568 .await;
13569 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13570
13571 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13572 editor
13573 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13574 .unwrap()
13575 });
13576 cx.assert_editor_state(expected_with_replace_mode);
13577 handle_resolve_completion_request(&mut cx, None).await;
13578 apply_additional_edits.await.unwrap();
13579
13580 update_test_language_settings(&mut cx, |settings| {
13581 settings.defaults.completions = Some(CompletionSettingsContent {
13582 words: Some(WordsCompletionMode::Disabled),
13583 words_min_length: Some(0),
13584 // set the opposite here to ensure that the action is overriding the default behavior
13585 lsp_insert_mode: Some(LspInsertMode::Replace),
13586 ..Default::default()
13587 });
13588 });
13589
13590 cx.set_state(initial_state);
13591 cx.update_editor(|editor, window, cx| {
13592 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13593 });
13594 handle_completion_request_with_insert_and_replace(
13595 &mut cx,
13596 buffer_marked_text,
13597 vec![(completion_text, completion_text)],
13598 counter.clone(),
13599 )
13600 .await;
13601 cx.condition(|editor, _| editor.context_menu_visible())
13602 .await;
13603 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
13604
13605 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13606 editor
13607 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
13608 .unwrap()
13609 });
13610 cx.assert_editor_state(expected_with_insert_mode);
13611 handle_resolve_completion_request(&mut cx, None).await;
13612 apply_additional_edits.await.unwrap();
13613}
13614
13615#[gpui::test]
13616async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
13617 init_test(cx, |_| {});
13618 let mut cx = EditorLspTestContext::new_rust(
13619 lsp::ServerCapabilities {
13620 completion_provider: Some(lsp::CompletionOptions {
13621 resolve_provider: Some(true),
13622 ..Default::default()
13623 }),
13624 ..Default::default()
13625 },
13626 cx,
13627 )
13628 .await;
13629
13630 // scenario: surrounding text matches completion text
13631 let completion_text = "to_offset";
13632 let initial_state = indoc! {"
13633 1. buf.to_offˇsuffix
13634 2. buf.to_offˇsuf
13635 3. buf.to_offˇfix
13636 4. buf.to_offˇ
13637 5. into_offˇensive
13638 6. ˇsuffix
13639 7. let ˇ //
13640 8. aaˇzz
13641 9. buf.to_off«zzzzzˇ»suffix
13642 10. buf.«ˇzzzzz»suffix
13643 11. to_off«ˇzzzzz»
13644
13645 buf.to_offˇsuffix // newest cursor
13646 "};
13647 let completion_marked_buffer = indoc! {"
13648 1. buf.to_offsuffix
13649 2. buf.to_offsuf
13650 3. buf.to_offfix
13651 4. buf.to_off
13652 5. into_offensive
13653 6. suffix
13654 7. let //
13655 8. aazz
13656 9. buf.to_offzzzzzsuffix
13657 10. buf.zzzzzsuffix
13658 11. to_offzzzzz
13659
13660 buf.<to_off|suffix> // newest cursor
13661 "};
13662 let expected = indoc! {"
13663 1. buf.to_offsetˇ
13664 2. buf.to_offsetˇsuf
13665 3. buf.to_offsetˇfix
13666 4. buf.to_offsetˇ
13667 5. into_offsetˇensive
13668 6. to_offsetˇsuffix
13669 7. let to_offsetˇ //
13670 8. aato_offsetˇzz
13671 9. buf.to_offsetˇ
13672 10. buf.to_offsetˇsuffix
13673 11. to_offsetˇ
13674
13675 buf.to_offsetˇ // newest cursor
13676 "};
13677 cx.set_state(initial_state);
13678 cx.update_editor(|editor, window, cx| {
13679 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13680 });
13681 handle_completion_request_with_insert_and_replace(
13682 &mut cx,
13683 completion_marked_buffer,
13684 vec![(completion_text, completion_text)],
13685 Arc::new(AtomicUsize::new(0)),
13686 )
13687 .await;
13688 cx.condition(|editor, _| editor.context_menu_visible())
13689 .await;
13690 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13691 editor
13692 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13693 .unwrap()
13694 });
13695 cx.assert_editor_state(expected);
13696 handle_resolve_completion_request(&mut cx, None).await;
13697 apply_additional_edits.await.unwrap();
13698
13699 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
13700 let completion_text = "foo_and_bar";
13701 let initial_state = indoc! {"
13702 1. ooanbˇ
13703 2. zooanbˇ
13704 3. ooanbˇz
13705 4. zooanbˇz
13706 5. ooanˇ
13707 6. oanbˇ
13708
13709 ooanbˇ
13710 "};
13711 let completion_marked_buffer = indoc! {"
13712 1. ooanb
13713 2. zooanb
13714 3. ooanbz
13715 4. zooanbz
13716 5. ooan
13717 6. oanb
13718
13719 <ooanb|>
13720 "};
13721 let expected = indoc! {"
13722 1. foo_and_barˇ
13723 2. zfoo_and_barˇ
13724 3. foo_and_barˇz
13725 4. zfoo_and_barˇz
13726 5. ooanfoo_and_barˇ
13727 6. oanbfoo_and_barˇ
13728
13729 foo_and_barˇ
13730 "};
13731 cx.set_state(initial_state);
13732 cx.update_editor(|editor, window, cx| {
13733 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13734 });
13735 handle_completion_request_with_insert_and_replace(
13736 &mut cx,
13737 completion_marked_buffer,
13738 vec![(completion_text, completion_text)],
13739 Arc::new(AtomicUsize::new(0)),
13740 )
13741 .await;
13742 cx.condition(|editor, _| editor.context_menu_visible())
13743 .await;
13744 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13745 editor
13746 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13747 .unwrap()
13748 });
13749 cx.assert_editor_state(expected);
13750 handle_resolve_completion_request(&mut cx, None).await;
13751 apply_additional_edits.await.unwrap();
13752
13753 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
13754 // (expects the same as if it was inserted at the end)
13755 let completion_text = "foo_and_bar";
13756 let initial_state = indoc! {"
13757 1. ooˇanb
13758 2. zooˇanb
13759 3. ooˇanbz
13760 4. zooˇanbz
13761
13762 ooˇanb
13763 "};
13764 let completion_marked_buffer = indoc! {"
13765 1. ooanb
13766 2. zooanb
13767 3. ooanbz
13768 4. zooanbz
13769
13770 <oo|anb>
13771 "};
13772 let expected = indoc! {"
13773 1. foo_and_barˇ
13774 2. zfoo_and_barˇ
13775 3. foo_and_barˇz
13776 4. zfoo_and_barˇz
13777
13778 foo_and_barˇ
13779 "};
13780 cx.set_state(initial_state);
13781 cx.update_editor(|editor, window, cx| {
13782 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13783 });
13784 handle_completion_request_with_insert_and_replace(
13785 &mut cx,
13786 completion_marked_buffer,
13787 vec![(completion_text, completion_text)],
13788 Arc::new(AtomicUsize::new(0)),
13789 )
13790 .await;
13791 cx.condition(|editor, _| editor.context_menu_visible())
13792 .await;
13793 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13794 editor
13795 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13796 .unwrap()
13797 });
13798 cx.assert_editor_state(expected);
13799 handle_resolve_completion_request(&mut cx, None).await;
13800 apply_additional_edits.await.unwrap();
13801}
13802
13803// This used to crash
13804#[gpui::test]
13805async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
13806 init_test(cx, |_| {});
13807
13808 let buffer_text = indoc! {"
13809 fn main() {
13810 10.satu;
13811
13812 //
13813 // separate cursors so they open in different excerpts (manually reproducible)
13814 //
13815
13816 10.satu20;
13817 }
13818 "};
13819 let multibuffer_text_with_selections = indoc! {"
13820 fn main() {
13821 10.satuˇ;
13822
13823 //
13824
13825 //
13826
13827 10.satuˇ20;
13828 }
13829 "};
13830 let expected_multibuffer = indoc! {"
13831 fn main() {
13832 10.saturating_sub()ˇ;
13833
13834 //
13835
13836 //
13837
13838 10.saturating_sub()ˇ;
13839 }
13840 "};
13841
13842 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
13843 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
13844
13845 let fs = FakeFs::new(cx.executor());
13846 fs.insert_tree(
13847 path!("/a"),
13848 json!({
13849 "main.rs": buffer_text,
13850 }),
13851 )
13852 .await;
13853
13854 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13855 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13856 language_registry.add(rust_lang());
13857 let mut fake_servers = language_registry.register_fake_lsp(
13858 "Rust",
13859 FakeLspAdapter {
13860 capabilities: lsp::ServerCapabilities {
13861 completion_provider: Some(lsp::CompletionOptions {
13862 resolve_provider: None,
13863 ..lsp::CompletionOptions::default()
13864 }),
13865 ..lsp::ServerCapabilities::default()
13866 },
13867 ..FakeLspAdapter::default()
13868 },
13869 );
13870 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13871 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13872 let buffer = project
13873 .update(cx, |project, cx| {
13874 project.open_local_buffer(path!("/a/main.rs"), cx)
13875 })
13876 .await
13877 .unwrap();
13878
13879 let multi_buffer = cx.new(|cx| {
13880 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
13881 multi_buffer.push_excerpts(
13882 buffer.clone(),
13883 [ExcerptRange::new(0..first_excerpt_end)],
13884 cx,
13885 );
13886 multi_buffer.push_excerpts(
13887 buffer.clone(),
13888 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
13889 cx,
13890 );
13891 multi_buffer
13892 });
13893
13894 let editor = workspace
13895 .update(cx, |_, window, cx| {
13896 cx.new(|cx| {
13897 Editor::new(
13898 EditorMode::Full {
13899 scale_ui_elements_with_buffer_font_size: false,
13900 show_active_line_background: false,
13901 sized_by_content: false,
13902 },
13903 multi_buffer.clone(),
13904 Some(project.clone()),
13905 window,
13906 cx,
13907 )
13908 })
13909 })
13910 .unwrap();
13911
13912 let pane = workspace
13913 .update(cx, |workspace, _, _| workspace.active_pane().clone())
13914 .unwrap();
13915 pane.update_in(cx, |pane, window, cx| {
13916 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
13917 });
13918
13919 let fake_server = fake_servers.next().await.unwrap();
13920
13921 editor.update_in(cx, |editor, window, cx| {
13922 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13923 s.select_ranges([
13924 Point::new(1, 11)..Point::new(1, 11),
13925 Point::new(7, 11)..Point::new(7, 11),
13926 ])
13927 });
13928
13929 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
13930 });
13931
13932 editor.update_in(cx, |editor, window, cx| {
13933 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13934 });
13935
13936 fake_server
13937 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13938 let completion_item = lsp::CompletionItem {
13939 label: "saturating_sub()".into(),
13940 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
13941 lsp::InsertReplaceEdit {
13942 new_text: "saturating_sub()".to_owned(),
13943 insert: lsp::Range::new(
13944 lsp::Position::new(7, 7),
13945 lsp::Position::new(7, 11),
13946 ),
13947 replace: lsp::Range::new(
13948 lsp::Position::new(7, 7),
13949 lsp::Position::new(7, 13),
13950 ),
13951 },
13952 )),
13953 ..lsp::CompletionItem::default()
13954 };
13955
13956 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
13957 })
13958 .next()
13959 .await
13960 .unwrap();
13961
13962 cx.condition(&editor, |editor, _| editor.context_menu_visible())
13963 .await;
13964
13965 editor
13966 .update_in(cx, |editor, window, cx| {
13967 editor
13968 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13969 .unwrap()
13970 })
13971 .await
13972 .unwrap();
13973
13974 editor.update(cx, |editor, cx| {
13975 assert_text_with_selections(editor, expected_multibuffer, cx);
13976 })
13977}
13978
13979#[gpui::test]
13980async fn test_completion(cx: &mut TestAppContext) {
13981 init_test(cx, |_| {});
13982
13983 let mut cx = EditorLspTestContext::new_rust(
13984 lsp::ServerCapabilities {
13985 completion_provider: Some(lsp::CompletionOptions {
13986 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
13987 resolve_provider: Some(true),
13988 ..Default::default()
13989 }),
13990 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13991 ..Default::default()
13992 },
13993 cx,
13994 )
13995 .await;
13996 let counter = Arc::new(AtomicUsize::new(0));
13997
13998 cx.set_state(indoc! {"
13999 oneˇ
14000 two
14001 three
14002 "});
14003 cx.simulate_keystroke(".");
14004 handle_completion_request(
14005 indoc! {"
14006 one.|<>
14007 two
14008 three
14009 "},
14010 vec!["first_completion", "second_completion"],
14011 true,
14012 counter.clone(),
14013 &mut cx,
14014 )
14015 .await;
14016 cx.condition(|editor, _| editor.context_menu_visible())
14017 .await;
14018 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14019
14020 let _handler = handle_signature_help_request(
14021 &mut cx,
14022 lsp::SignatureHelp {
14023 signatures: vec![lsp::SignatureInformation {
14024 label: "test signature".to_string(),
14025 documentation: None,
14026 parameters: Some(vec![lsp::ParameterInformation {
14027 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
14028 documentation: None,
14029 }]),
14030 active_parameter: None,
14031 }],
14032 active_signature: None,
14033 active_parameter: None,
14034 },
14035 );
14036 cx.update_editor(|editor, window, cx| {
14037 assert!(
14038 !editor.signature_help_state.is_shown(),
14039 "No signature help was called for"
14040 );
14041 editor.show_signature_help(&ShowSignatureHelp, window, cx);
14042 });
14043 cx.run_until_parked();
14044 cx.update_editor(|editor, _, _| {
14045 assert!(
14046 !editor.signature_help_state.is_shown(),
14047 "No signature help should be shown when completions menu is open"
14048 );
14049 });
14050
14051 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14052 editor.context_menu_next(&Default::default(), window, cx);
14053 editor
14054 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14055 .unwrap()
14056 });
14057 cx.assert_editor_state(indoc! {"
14058 one.second_completionˇ
14059 two
14060 three
14061 "});
14062
14063 handle_resolve_completion_request(
14064 &mut cx,
14065 Some(vec![
14066 (
14067 //This overlaps with the primary completion edit which is
14068 //misbehavior from the LSP spec, test that we filter it out
14069 indoc! {"
14070 one.second_ˇcompletion
14071 two
14072 threeˇ
14073 "},
14074 "overlapping additional edit",
14075 ),
14076 (
14077 indoc! {"
14078 one.second_completion
14079 two
14080 threeˇ
14081 "},
14082 "\nadditional edit",
14083 ),
14084 ]),
14085 )
14086 .await;
14087 apply_additional_edits.await.unwrap();
14088 cx.assert_editor_state(indoc! {"
14089 one.second_completionˇ
14090 two
14091 three
14092 additional edit
14093 "});
14094
14095 cx.set_state(indoc! {"
14096 one.second_completion
14097 twoˇ
14098 threeˇ
14099 additional edit
14100 "});
14101 cx.simulate_keystroke(" ");
14102 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14103 cx.simulate_keystroke("s");
14104 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14105
14106 cx.assert_editor_state(indoc! {"
14107 one.second_completion
14108 two sˇ
14109 three sˇ
14110 additional edit
14111 "});
14112 handle_completion_request(
14113 indoc! {"
14114 one.second_completion
14115 two s
14116 three <s|>
14117 additional edit
14118 "},
14119 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14120 true,
14121 counter.clone(),
14122 &mut cx,
14123 )
14124 .await;
14125 cx.condition(|editor, _| editor.context_menu_visible())
14126 .await;
14127 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14128
14129 cx.simulate_keystroke("i");
14130
14131 handle_completion_request(
14132 indoc! {"
14133 one.second_completion
14134 two si
14135 three <si|>
14136 additional edit
14137 "},
14138 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14139 true,
14140 counter.clone(),
14141 &mut cx,
14142 )
14143 .await;
14144 cx.condition(|editor, _| editor.context_menu_visible())
14145 .await;
14146 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14147
14148 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14149 editor
14150 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14151 .unwrap()
14152 });
14153 cx.assert_editor_state(indoc! {"
14154 one.second_completion
14155 two sixth_completionˇ
14156 three sixth_completionˇ
14157 additional edit
14158 "});
14159
14160 apply_additional_edits.await.unwrap();
14161
14162 update_test_language_settings(&mut cx, |settings| {
14163 settings.defaults.show_completions_on_input = Some(false);
14164 });
14165 cx.set_state("editorˇ");
14166 cx.simulate_keystroke(".");
14167 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14168 cx.simulate_keystrokes("c l o");
14169 cx.assert_editor_state("editor.cloˇ");
14170 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14171 cx.update_editor(|editor, window, cx| {
14172 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14173 });
14174 handle_completion_request(
14175 "editor.<clo|>",
14176 vec!["close", "clobber"],
14177 true,
14178 counter.clone(),
14179 &mut cx,
14180 )
14181 .await;
14182 cx.condition(|editor, _| editor.context_menu_visible())
14183 .await;
14184 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
14185
14186 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14187 editor
14188 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14189 .unwrap()
14190 });
14191 cx.assert_editor_state("editor.clobberˇ");
14192 handle_resolve_completion_request(&mut cx, None).await;
14193 apply_additional_edits.await.unwrap();
14194}
14195
14196#[gpui::test]
14197async fn test_completion_reuse(cx: &mut TestAppContext) {
14198 init_test(cx, |_| {});
14199
14200 let mut cx = EditorLspTestContext::new_rust(
14201 lsp::ServerCapabilities {
14202 completion_provider: Some(lsp::CompletionOptions {
14203 trigger_characters: Some(vec![".".to_string()]),
14204 ..Default::default()
14205 }),
14206 ..Default::default()
14207 },
14208 cx,
14209 )
14210 .await;
14211
14212 let counter = Arc::new(AtomicUsize::new(0));
14213 cx.set_state("objˇ");
14214 cx.simulate_keystroke(".");
14215
14216 // Initial completion request returns complete results
14217 let is_incomplete = false;
14218 handle_completion_request(
14219 "obj.|<>",
14220 vec!["a", "ab", "abc"],
14221 is_incomplete,
14222 counter.clone(),
14223 &mut cx,
14224 )
14225 .await;
14226 cx.run_until_parked();
14227 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14228 cx.assert_editor_state("obj.ˇ");
14229 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14230
14231 // Type "a" - filters existing completions
14232 cx.simulate_keystroke("a");
14233 cx.run_until_parked();
14234 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14235 cx.assert_editor_state("obj.aˇ");
14236 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14237
14238 // Type "b" - filters existing completions
14239 cx.simulate_keystroke("b");
14240 cx.run_until_parked();
14241 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14242 cx.assert_editor_state("obj.abˇ");
14243 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14244
14245 // Type "c" - filters existing completions
14246 cx.simulate_keystroke("c");
14247 cx.run_until_parked();
14248 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14249 cx.assert_editor_state("obj.abcˇ");
14250 check_displayed_completions(vec!["abc"], &mut cx);
14251
14252 // Backspace to delete "c" - filters existing completions
14253 cx.update_editor(|editor, window, cx| {
14254 editor.backspace(&Backspace, window, cx);
14255 });
14256 cx.run_until_parked();
14257 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14258 cx.assert_editor_state("obj.abˇ");
14259 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14260
14261 // Moving cursor to the left dismisses menu.
14262 cx.update_editor(|editor, window, cx| {
14263 editor.move_left(&MoveLeft, window, cx);
14264 });
14265 cx.run_until_parked();
14266 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14267 cx.assert_editor_state("obj.aˇb");
14268 cx.update_editor(|editor, _, _| {
14269 assert_eq!(editor.context_menu_visible(), false);
14270 });
14271
14272 // Type "b" - new request
14273 cx.simulate_keystroke("b");
14274 let is_incomplete = false;
14275 handle_completion_request(
14276 "obj.<ab|>a",
14277 vec!["ab", "abc"],
14278 is_incomplete,
14279 counter.clone(),
14280 &mut cx,
14281 )
14282 .await;
14283 cx.run_until_parked();
14284 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14285 cx.assert_editor_state("obj.abˇb");
14286 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14287
14288 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
14289 cx.update_editor(|editor, window, cx| {
14290 editor.backspace(&Backspace, window, cx);
14291 });
14292 let is_incomplete = false;
14293 handle_completion_request(
14294 "obj.<a|>b",
14295 vec!["a", "ab", "abc"],
14296 is_incomplete,
14297 counter.clone(),
14298 &mut cx,
14299 )
14300 .await;
14301 cx.run_until_parked();
14302 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14303 cx.assert_editor_state("obj.aˇb");
14304 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14305
14306 // Backspace to delete "a" - dismisses menu.
14307 cx.update_editor(|editor, window, cx| {
14308 editor.backspace(&Backspace, window, cx);
14309 });
14310 cx.run_until_parked();
14311 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14312 cx.assert_editor_state("obj.ˇb");
14313 cx.update_editor(|editor, _, _| {
14314 assert_eq!(editor.context_menu_visible(), false);
14315 });
14316}
14317
14318#[gpui::test]
14319async fn test_word_completion(cx: &mut TestAppContext) {
14320 let lsp_fetch_timeout_ms = 10;
14321 init_test(cx, |language_settings| {
14322 language_settings.defaults.completions = Some(CompletionSettingsContent {
14323 words_min_length: Some(0),
14324 lsp_fetch_timeout_ms: Some(10),
14325 lsp_insert_mode: Some(LspInsertMode::Insert),
14326 ..Default::default()
14327 });
14328 });
14329
14330 let mut cx = EditorLspTestContext::new_rust(
14331 lsp::ServerCapabilities {
14332 completion_provider: Some(lsp::CompletionOptions {
14333 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14334 ..lsp::CompletionOptions::default()
14335 }),
14336 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14337 ..lsp::ServerCapabilities::default()
14338 },
14339 cx,
14340 )
14341 .await;
14342
14343 let throttle_completions = Arc::new(AtomicBool::new(false));
14344
14345 let lsp_throttle_completions = throttle_completions.clone();
14346 let _completion_requests_handler =
14347 cx.lsp
14348 .server
14349 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
14350 let lsp_throttle_completions = lsp_throttle_completions.clone();
14351 let cx = cx.clone();
14352 async move {
14353 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
14354 cx.background_executor()
14355 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
14356 .await;
14357 }
14358 Ok(Some(lsp::CompletionResponse::Array(vec![
14359 lsp::CompletionItem {
14360 label: "first".into(),
14361 ..lsp::CompletionItem::default()
14362 },
14363 lsp::CompletionItem {
14364 label: "last".into(),
14365 ..lsp::CompletionItem::default()
14366 },
14367 ])))
14368 }
14369 });
14370
14371 cx.set_state(indoc! {"
14372 oneˇ
14373 two
14374 three
14375 "});
14376 cx.simulate_keystroke(".");
14377 cx.executor().run_until_parked();
14378 cx.condition(|editor, _| editor.context_menu_visible())
14379 .await;
14380 cx.update_editor(|editor, window, cx| {
14381 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14382 {
14383 assert_eq!(
14384 completion_menu_entries(menu),
14385 &["first", "last"],
14386 "When LSP server is fast to reply, no fallback word completions are used"
14387 );
14388 } else {
14389 panic!("expected completion menu to be open");
14390 }
14391 editor.cancel(&Cancel, window, cx);
14392 });
14393 cx.executor().run_until_parked();
14394 cx.condition(|editor, _| !editor.context_menu_visible())
14395 .await;
14396
14397 throttle_completions.store(true, atomic::Ordering::Release);
14398 cx.simulate_keystroke(".");
14399 cx.executor()
14400 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
14401 cx.executor().run_until_parked();
14402 cx.condition(|editor, _| editor.context_menu_visible())
14403 .await;
14404 cx.update_editor(|editor, _, _| {
14405 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14406 {
14407 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
14408 "When LSP server is slow, document words can be shown instead, if configured accordingly");
14409 } else {
14410 panic!("expected completion menu to be open");
14411 }
14412 });
14413}
14414
14415#[gpui::test]
14416async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
14417 init_test(cx, |language_settings| {
14418 language_settings.defaults.completions = Some(CompletionSettingsContent {
14419 words: Some(WordsCompletionMode::Enabled),
14420 words_min_length: Some(0),
14421 lsp_insert_mode: Some(LspInsertMode::Insert),
14422 ..Default::default()
14423 });
14424 });
14425
14426 let mut cx = EditorLspTestContext::new_rust(
14427 lsp::ServerCapabilities {
14428 completion_provider: Some(lsp::CompletionOptions {
14429 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14430 ..lsp::CompletionOptions::default()
14431 }),
14432 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14433 ..lsp::ServerCapabilities::default()
14434 },
14435 cx,
14436 )
14437 .await;
14438
14439 let _completion_requests_handler =
14440 cx.lsp
14441 .server
14442 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14443 Ok(Some(lsp::CompletionResponse::Array(vec![
14444 lsp::CompletionItem {
14445 label: "first".into(),
14446 ..lsp::CompletionItem::default()
14447 },
14448 lsp::CompletionItem {
14449 label: "last".into(),
14450 ..lsp::CompletionItem::default()
14451 },
14452 ])))
14453 });
14454
14455 cx.set_state(indoc! {"ˇ
14456 first
14457 last
14458 second
14459 "});
14460 cx.simulate_keystroke(".");
14461 cx.executor().run_until_parked();
14462 cx.condition(|editor, _| editor.context_menu_visible())
14463 .await;
14464 cx.update_editor(|editor, _, _| {
14465 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14466 {
14467 assert_eq!(
14468 completion_menu_entries(menu),
14469 &["first", "last", "second"],
14470 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
14471 );
14472 } else {
14473 panic!("expected completion menu to be open");
14474 }
14475 });
14476}
14477
14478#[gpui::test]
14479async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
14480 init_test(cx, |language_settings| {
14481 language_settings.defaults.completions = Some(CompletionSettingsContent {
14482 words: Some(WordsCompletionMode::Disabled),
14483 words_min_length: Some(0),
14484 lsp_insert_mode: Some(LspInsertMode::Insert),
14485 ..Default::default()
14486 });
14487 });
14488
14489 let mut cx = EditorLspTestContext::new_rust(
14490 lsp::ServerCapabilities {
14491 completion_provider: Some(lsp::CompletionOptions {
14492 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14493 ..lsp::CompletionOptions::default()
14494 }),
14495 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14496 ..lsp::ServerCapabilities::default()
14497 },
14498 cx,
14499 )
14500 .await;
14501
14502 let _completion_requests_handler =
14503 cx.lsp
14504 .server
14505 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14506 panic!("LSP completions should not be queried when dealing with word completions")
14507 });
14508
14509 cx.set_state(indoc! {"ˇ
14510 first
14511 last
14512 second
14513 "});
14514 cx.update_editor(|editor, window, cx| {
14515 editor.show_word_completions(&ShowWordCompletions, window, cx);
14516 });
14517 cx.executor().run_until_parked();
14518 cx.condition(|editor, _| editor.context_menu_visible())
14519 .await;
14520 cx.update_editor(|editor, _, _| {
14521 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14522 {
14523 assert_eq!(
14524 completion_menu_entries(menu),
14525 &["first", "last", "second"],
14526 "`ShowWordCompletions` action should show word completions"
14527 );
14528 } else {
14529 panic!("expected completion menu to be open");
14530 }
14531 });
14532
14533 cx.simulate_keystroke("l");
14534 cx.executor().run_until_parked();
14535 cx.condition(|editor, _| editor.context_menu_visible())
14536 .await;
14537 cx.update_editor(|editor, _, _| {
14538 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14539 {
14540 assert_eq!(
14541 completion_menu_entries(menu),
14542 &["last"],
14543 "After showing word completions, further editing should filter them and not query the LSP"
14544 );
14545 } else {
14546 panic!("expected completion menu to be open");
14547 }
14548 });
14549}
14550
14551#[gpui::test]
14552async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
14553 init_test(cx, |language_settings| {
14554 language_settings.defaults.completions = Some(CompletionSettingsContent {
14555 words_min_length: Some(0),
14556 lsp: Some(false),
14557 lsp_insert_mode: Some(LspInsertMode::Insert),
14558 ..Default::default()
14559 });
14560 });
14561
14562 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14563
14564 cx.set_state(indoc! {"ˇ
14565 0_usize
14566 let
14567 33
14568 4.5f32
14569 "});
14570 cx.update_editor(|editor, window, cx| {
14571 editor.show_completions(&ShowCompletions::default(), window, cx);
14572 });
14573 cx.executor().run_until_parked();
14574 cx.condition(|editor, _| editor.context_menu_visible())
14575 .await;
14576 cx.update_editor(|editor, window, cx| {
14577 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14578 {
14579 assert_eq!(
14580 completion_menu_entries(menu),
14581 &["let"],
14582 "With no digits in the completion query, no digits should be in the word completions"
14583 );
14584 } else {
14585 panic!("expected completion menu to be open");
14586 }
14587 editor.cancel(&Cancel, window, cx);
14588 });
14589
14590 cx.set_state(indoc! {"3ˇ
14591 0_usize
14592 let
14593 3
14594 33.35f32
14595 "});
14596 cx.update_editor(|editor, window, cx| {
14597 editor.show_completions(&ShowCompletions::default(), window, cx);
14598 });
14599 cx.executor().run_until_parked();
14600 cx.condition(|editor, _| editor.context_menu_visible())
14601 .await;
14602 cx.update_editor(|editor, _, _| {
14603 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14604 {
14605 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
14606 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
14607 } else {
14608 panic!("expected completion menu to be open");
14609 }
14610 });
14611}
14612
14613#[gpui::test]
14614async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
14615 init_test(cx, |language_settings| {
14616 language_settings.defaults.completions = Some(CompletionSettingsContent {
14617 words: Some(WordsCompletionMode::Enabled),
14618 words_min_length: Some(3),
14619 lsp_insert_mode: Some(LspInsertMode::Insert),
14620 ..Default::default()
14621 });
14622 });
14623
14624 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14625 cx.set_state(indoc! {"ˇ
14626 wow
14627 wowen
14628 wowser
14629 "});
14630 cx.simulate_keystroke("w");
14631 cx.executor().run_until_parked();
14632 cx.update_editor(|editor, _, _| {
14633 if editor.context_menu.borrow_mut().is_some() {
14634 panic!(
14635 "expected completion menu to be hidden, as words completion threshold is not met"
14636 );
14637 }
14638 });
14639
14640 cx.update_editor(|editor, window, cx| {
14641 editor.show_word_completions(&ShowWordCompletions, window, cx);
14642 });
14643 cx.executor().run_until_parked();
14644 cx.update_editor(|editor, window, cx| {
14645 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14646 {
14647 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");
14648 } else {
14649 panic!("expected completion menu to be open after the word completions are called with an action");
14650 }
14651
14652 editor.cancel(&Cancel, window, cx);
14653 });
14654 cx.update_editor(|editor, _, _| {
14655 if editor.context_menu.borrow_mut().is_some() {
14656 panic!("expected completion menu to be hidden after canceling");
14657 }
14658 });
14659
14660 cx.simulate_keystroke("o");
14661 cx.executor().run_until_parked();
14662 cx.update_editor(|editor, _, _| {
14663 if editor.context_menu.borrow_mut().is_some() {
14664 panic!(
14665 "expected completion menu to be hidden, as words completion threshold is not met still"
14666 );
14667 }
14668 });
14669
14670 cx.simulate_keystroke("w");
14671 cx.executor().run_until_parked();
14672 cx.update_editor(|editor, _, _| {
14673 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14674 {
14675 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
14676 } else {
14677 panic!("expected completion menu to be open after the word completions threshold is met");
14678 }
14679 });
14680}
14681
14682#[gpui::test]
14683async fn test_word_completions_disabled(cx: &mut TestAppContext) {
14684 init_test(cx, |language_settings| {
14685 language_settings.defaults.completions = Some(CompletionSettingsContent {
14686 words: Some(WordsCompletionMode::Enabled),
14687 words_min_length: Some(0),
14688 lsp_insert_mode: Some(LspInsertMode::Insert),
14689 ..Default::default()
14690 });
14691 });
14692
14693 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14694 cx.update_editor(|editor, _, _| {
14695 editor.disable_word_completions();
14696 });
14697 cx.set_state(indoc! {"ˇ
14698 wow
14699 wowen
14700 wowser
14701 "});
14702 cx.simulate_keystroke("w");
14703 cx.executor().run_until_parked();
14704 cx.update_editor(|editor, _, _| {
14705 if editor.context_menu.borrow_mut().is_some() {
14706 panic!(
14707 "expected completion menu to be hidden, as words completion are disabled for this editor"
14708 );
14709 }
14710 });
14711
14712 cx.update_editor(|editor, window, cx| {
14713 editor.show_word_completions(&ShowWordCompletions, window, cx);
14714 });
14715 cx.executor().run_until_parked();
14716 cx.update_editor(|editor, _, _| {
14717 if editor.context_menu.borrow_mut().is_some() {
14718 panic!(
14719 "expected completion menu to be hidden even if called for explicitly, as words completion are disabled for this editor"
14720 );
14721 }
14722 });
14723}
14724
14725fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
14726 let position = || lsp::Position {
14727 line: params.text_document_position.position.line,
14728 character: params.text_document_position.position.character,
14729 };
14730 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14731 range: lsp::Range {
14732 start: position(),
14733 end: position(),
14734 },
14735 new_text: text.to_string(),
14736 }))
14737}
14738
14739#[gpui::test]
14740async fn test_multiline_completion(cx: &mut TestAppContext) {
14741 init_test(cx, |_| {});
14742
14743 let fs = FakeFs::new(cx.executor());
14744 fs.insert_tree(
14745 path!("/a"),
14746 json!({
14747 "main.ts": "a",
14748 }),
14749 )
14750 .await;
14751
14752 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14753 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14754 let typescript_language = Arc::new(Language::new(
14755 LanguageConfig {
14756 name: "TypeScript".into(),
14757 matcher: LanguageMatcher {
14758 path_suffixes: vec!["ts".to_string()],
14759 ..LanguageMatcher::default()
14760 },
14761 line_comments: vec!["// ".into()],
14762 ..LanguageConfig::default()
14763 },
14764 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
14765 ));
14766 language_registry.add(typescript_language.clone());
14767 let mut fake_servers = language_registry.register_fake_lsp(
14768 "TypeScript",
14769 FakeLspAdapter {
14770 capabilities: lsp::ServerCapabilities {
14771 completion_provider: Some(lsp::CompletionOptions {
14772 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14773 ..lsp::CompletionOptions::default()
14774 }),
14775 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14776 ..lsp::ServerCapabilities::default()
14777 },
14778 // Emulate vtsls label generation
14779 label_for_completion: Some(Box::new(|item, _| {
14780 let text = if let Some(description) = item
14781 .label_details
14782 .as_ref()
14783 .and_then(|label_details| label_details.description.as_ref())
14784 {
14785 format!("{} {}", item.label, description)
14786 } else if let Some(detail) = &item.detail {
14787 format!("{} {}", item.label, detail)
14788 } else {
14789 item.label.clone()
14790 };
14791 let len = text.len();
14792 Some(language::CodeLabel {
14793 text,
14794 runs: Vec::new(),
14795 filter_range: 0..len,
14796 })
14797 })),
14798 ..FakeLspAdapter::default()
14799 },
14800 );
14801 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14802 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14803 let worktree_id = workspace
14804 .update(cx, |workspace, _window, cx| {
14805 workspace.project().update(cx, |project, cx| {
14806 project.worktrees(cx).next().unwrap().read(cx).id()
14807 })
14808 })
14809 .unwrap();
14810 let _buffer = project
14811 .update(cx, |project, cx| {
14812 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
14813 })
14814 .await
14815 .unwrap();
14816 let editor = workspace
14817 .update(cx, |workspace, window, cx| {
14818 workspace.open_path((worktree_id, rel_path("main.ts")), None, true, window, cx)
14819 })
14820 .unwrap()
14821 .await
14822 .unwrap()
14823 .downcast::<Editor>()
14824 .unwrap();
14825 let fake_server = fake_servers.next().await.unwrap();
14826
14827 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
14828 let multiline_label_2 = "a\nb\nc\n";
14829 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
14830 let multiline_description = "d\ne\nf\n";
14831 let multiline_detail_2 = "g\nh\ni\n";
14832
14833 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
14834 move |params, _| async move {
14835 Ok(Some(lsp::CompletionResponse::Array(vec![
14836 lsp::CompletionItem {
14837 label: multiline_label.to_string(),
14838 text_edit: gen_text_edit(¶ms, "new_text_1"),
14839 ..lsp::CompletionItem::default()
14840 },
14841 lsp::CompletionItem {
14842 label: "single line label 1".to_string(),
14843 detail: Some(multiline_detail.to_string()),
14844 text_edit: gen_text_edit(¶ms, "new_text_2"),
14845 ..lsp::CompletionItem::default()
14846 },
14847 lsp::CompletionItem {
14848 label: "single line label 2".to_string(),
14849 label_details: Some(lsp::CompletionItemLabelDetails {
14850 description: Some(multiline_description.to_string()),
14851 detail: None,
14852 }),
14853 text_edit: gen_text_edit(¶ms, "new_text_2"),
14854 ..lsp::CompletionItem::default()
14855 },
14856 lsp::CompletionItem {
14857 label: multiline_label_2.to_string(),
14858 detail: Some(multiline_detail_2.to_string()),
14859 text_edit: gen_text_edit(¶ms, "new_text_3"),
14860 ..lsp::CompletionItem::default()
14861 },
14862 lsp::CompletionItem {
14863 label: "Label with many spaces and \t but without newlines".to_string(),
14864 detail: Some(
14865 "Details with many spaces and \t but without newlines".to_string(),
14866 ),
14867 text_edit: gen_text_edit(¶ms, "new_text_4"),
14868 ..lsp::CompletionItem::default()
14869 },
14870 ])))
14871 },
14872 );
14873
14874 editor.update_in(cx, |editor, window, cx| {
14875 cx.focus_self(window);
14876 editor.move_to_end(&MoveToEnd, window, cx);
14877 editor.handle_input(".", window, cx);
14878 });
14879 cx.run_until_parked();
14880 completion_handle.next().await.unwrap();
14881
14882 editor.update(cx, |editor, _| {
14883 assert!(editor.context_menu_visible());
14884 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14885 {
14886 let completion_labels = menu
14887 .completions
14888 .borrow()
14889 .iter()
14890 .map(|c| c.label.text.clone())
14891 .collect::<Vec<_>>();
14892 assert_eq!(
14893 completion_labels,
14894 &[
14895 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
14896 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
14897 "single line label 2 d e f ",
14898 "a b c g h i ",
14899 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
14900 ],
14901 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
14902 );
14903
14904 for completion in menu
14905 .completions
14906 .borrow()
14907 .iter() {
14908 assert_eq!(
14909 completion.label.filter_range,
14910 0..completion.label.text.len(),
14911 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
14912 );
14913 }
14914 } else {
14915 panic!("expected completion menu to be open");
14916 }
14917 });
14918}
14919
14920#[gpui::test]
14921async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
14922 init_test(cx, |_| {});
14923 let mut cx = EditorLspTestContext::new_rust(
14924 lsp::ServerCapabilities {
14925 completion_provider: Some(lsp::CompletionOptions {
14926 trigger_characters: Some(vec![".".to_string()]),
14927 ..Default::default()
14928 }),
14929 ..Default::default()
14930 },
14931 cx,
14932 )
14933 .await;
14934 cx.lsp
14935 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14936 Ok(Some(lsp::CompletionResponse::Array(vec![
14937 lsp::CompletionItem {
14938 label: "first".into(),
14939 ..Default::default()
14940 },
14941 lsp::CompletionItem {
14942 label: "last".into(),
14943 ..Default::default()
14944 },
14945 ])))
14946 });
14947 cx.set_state("variableˇ");
14948 cx.simulate_keystroke(".");
14949 cx.executor().run_until_parked();
14950
14951 cx.update_editor(|editor, _, _| {
14952 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14953 {
14954 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
14955 } else {
14956 panic!("expected completion menu to be open");
14957 }
14958 });
14959
14960 cx.update_editor(|editor, window, cx| {
14961 editor.move_page_down(&MovePageDown::default(), window, cx);
14962 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14963 {
14964 assert!(
14965 menu.selected_item == 1,
14966 "expected PageDown to select the last item from the context menu"
14967 );
14968 } else {
14969 panic!("expected completion menu to stay open after PageDown");
14970 }
14971 });
14972
14973 cx.update_editor(|editor, window, cx| {
14974 editor.move_page_up(&MovePageUp::default(), window, cx);
14975 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14976 {
14977 assert!(
14978 menu.selected_item == 0,
14979 "expected PageUp to select the first item from the context menu"
14980 );
14981 } else {
14982 panic!("expected completion menu to stay open after PageUp");
14983 }
14984 });
14985}
14986
14987#[gpui::test]
14988async fn test_as_is_completions(cx: &mut TestAppContext) {
14989 init_test(cx, |_| {});
14990 let mut cx = EditorLspTestContext::new_rust(
14991 lsp::ServerCapabilities {
14992 completion_provider: Some(lsp::CompletionOptions {
14993 ..Default::default()
14994 }),
14995 ..Default::default()
14996 },
14997 cx,
14998 )
14999 .await;
15000 cx.lsp
15001 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15002 Ok(Some(lsp::CompletionResponse::Array(vec![
15003 lsp::CompletionItem {
15004 label: "unsafe".into(),
15005 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15006 range: lsp::Range {
15007 start: lsp::Position {
15008 line: 1,
15009 character: 2,
15010 },
15011 end: lsp::Position {
15012 line: 1,
15013 character: 3,
15014 },
15015 },
15016 new_text: "unsafe".to_string(),
15017 })),
15018 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
15019 ..Default::default()
15020 },
15021 ])))
15022 });
15023 cx.set_state("fn a() {}\n nˇ");
15024 cx.executor().run_until_parked();
15025 cx.update_editor(|editor, window, cx| {
15026 editor.show_completions(
15027 &ShowCompletions {
15028 trigger: Some("\n".into()),
15029 },
15030 window,
15031 cx,
15032 );
15033 });
15034 cx.executor().run_until_parked();
15035
15036 cx.update_editor(|editor, window, cx| {
15037 editor.confirm_completion(&Default::default(), window, cx)
15038 });
15039 cx.executor().run_until_parked();
15040 cx.assert_editor_state("fn a() {}\n unsafeˇ");
15041}
15042
15043#[gpui::test]
15044async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
15045 init_test(cx, |_| {});
15046 let language =
15047 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
15048 let mut cx = EditorLspTestContext::new(
15049 language,
15050 lsp::ServerCapabilities {
15051 completion_provider: Some(lsp::CompletionOptions {
15052 ..lsp::CompletionOptions::default()
15053 }),
15054 ..lsp::ServerCapabilities::default()
15055 },
15056 cx,
15057 )
15058 .await;
15059
15060 cx.set_state(
15061 "#ifndef BAR_H
15062#define BAR_H
15063
15064#include <stdbool.h>
15065
15066int fn_branch(bool do_branch1, bool do_branch2);
15067
15068#endif // BAR_H
15069ˇ",
15070 );
15071 cx.executor().run_until_parked();
15072 cx.update_editor(|editor, window, cx| {
15073 editor.handle_input("#", window, cx);
15074 });
15075 cx.executor().run_until_parked();
15076 cx.update_editor(|editor, window, cx| {
15077 editor.handle_input("i", window, cx);
15078 });
15079 cx.executor().run_until_parked();
15080 cx.update_editor(|editor, window, cx| {
15081 editor.handle_input("n", window, cx);
15082 });
15083 cx.executor().run_until_parked();
15084 cx.assert_editor_state(
15085 "#ifndef BAR_H
15086#define BAR_H
15087
15088#include <stdbool.h>
15089
15090int fn_branch(bool do_branch1, bool do_branch2);
15091
15092#endif // BAR_H
15093#inˇ",
15094 );
15095
15096 cx.lsp
15097 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15098 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15099 is_incomplete: false,
15100 item_defaults: None,
15101 items: vec![lsp::CompletionItem {
15102 kind: Some(lsp::CompletionItemKind::SNIPPET),
15103 label_details: Some(lsp::CompletionItemLabelDetails {
15104 detail: Some("header".to_string()),
15105 description: None,
15106 }),
15107 label: " include".to_string(),
15108 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15109 range: lsp::Range {
15110 start: lsp::Position {
15111 line: 8,
15112 character: 1,
15113 },
15114 end: lsp::Position {
15115 line: 8,
15116 character: 1,
15117 },
15118 },
15119 new_text: "include \"$0\"".to_string(),
15120 })),
15121 sort_text: Some("40b67681include".to_string()),
15122 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15123 filter_text: Some("include".to_string()),
15124 insert_text: Some("include \"$0\"".to_string()),
15125 ..lsp::CompletionItem::default()
15126 }],
15127 })))
15128 });
15129 cx.update_editor(|editor, window, cx| {
15130 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15131 });
15132 cx.executor().run_until_parked();
15133 cx.update_editor(|editor, window, cx| {
15134 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15135 });
15136 cx.executor().run_until_parked();
15137 cx.assert_editor_state(
15138 "#ifndef BAR_H
15139#define BAR_H
15140
15141#include <stdbool.h>
15142
15143int fn_branch(bool do_branch1, bool do_branch2);
15144
15145#endif // BAR_H
15146#include \"ˇ\"",
15147 );
15148
15149 cx.lsp
15150 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15151 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15152 is_incomplete: true,
15153 item_defaults: None,
15154 items: vec![lsp::CompletionItem {
15155 kind: Some(lsp::CompletionItemKind::FILE),
15156 label: "AGL/".to_string(),
15157 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15158 range: lsp::Range {
15159 start: lsp::Position {
15160 line: 8,
15161 character: 10,
15162 },
15163 end: lsp::Position {
15164 line: 8,
15165 character: 11,
15166 },
15167 },
15168 new_text: "AGL/".to_string(),
15169 })),
15170 sort_text: Some("40b67681AGL/".to_string()),
15171 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15172 filter_text: Some("AGL/".to_string()),
15173 insert_text: Some("AGL/".to_string()),
15174 ..lsp::CompletionItem::default()
15175 }],
15176 })))
15177 });
15178 cx.update_editor(|editor, window, cx| {
15179 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15180 });
15181 cx.executor().run_until_parked();
15182 cx.update_editor(|editor, window, cx| {
15183 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15184 });
15185 cx.executor().run_until_parked();
15186 cx.assert_editor_state(
15187 r##"#ifndef BAR_H
15188#define BAR_H
15189
15190#include <stdbool.h>
15191
15192int fn_branch(bool do_branch1, bool do_branch2);
15193
15194#endif // BAR_H
15195#include "AGL/ˇ"##,
15196 );
15197
15198 cx.update_editor(|editor, window, cx| {
15199 editor.handle_input("\"", window, cx);
15200 });
15201 cx.executor().run_until_parked();
15202 cx.assert_editor_state(
15203 r##"#ifndef BAR_H
15204#define BAR_H
15205
15206#include <stdbool.h>
15207
15208int fn_branch(bool do_branch1, bool do_branch2);
15209
15210#endif // BAR_H
15211#include "AGL/"ˇ"##,
15212 );
15213}
15214
15215#[gpui::test]
15216async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
15217 init_test(cx, |_| {});
15218
15219 let mut cx = EditorLspTestContext::new_rust(
15220 lsp::ServerCapabilities {
15221 completion_provider: Some(lsp::CompletionOptions {
15222 trigger_characters: Some(vec![".".to_string()]),
15223 resolve_provider: Some(true),
15224 ..Default::default()
15225 }),
15226 ..Default::default()
15227 },
15228 cx,
15229 )
15230 .await;
15231
15232 cx.set_state("fn main() { let a = 2ˇ; }");
15233 cx.simulate_keystroke(".");
15234 let completion_item = lsp::CompletionItem {
15235 label: "Some".into(),
15236 kind: Some(lsp::CompletionItemKind::SNIPPET),
15237 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15238 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15239 kind: lsp::MarkupKind::Markdown,
15240 value: "```rust\nSome(2)\n```".to_string(),
15241 })),
15242 deprecated: Some(false),
15243 sort_text: Some("Some".to_string()),
15244 filter_text: Some("Some".to_string()),
15245 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15246 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15247 range: lsp::Range {
15248 start: lsp::Position {
15249 line: 0,
15250 character: 22,
15251 },
15252 end: lsp::Position {
15253 line: 0,
15254 character: 22,
15255 },
15256 },
15257 new_text: "Some(2)".to_string(),
15258 })),
15259 additional_text_edits: Some(vec![lsp::TextEdit {
15260 range: lsp::Range {
15261 start: lsp::Position {
15262 line: 0,
15263 character: 20,
15264 },
15265 end: lsp::Position {
15266 line: 0,
15267 character: 22,
15268 },
15269 },
15270 new_text: "".to_string(),
15271 }]),
15272 ..Default::default()
15273 };
15274
15275 let closure_completion_item = completion_item.clone();
15276 let counter = Arc::new(AtomicUsize::new(0));
15277 let counter_clone = counter.clone();
15278 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15279 let task_completion_item = closure_completion_item.clone();
15280 counter_clone.fetch_add(1, atomic::Ordering::Release);
15281 async move {
15282 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15283 is_incomplete: true,
15284 item_defaults: None,
15285 items: vec![task_completion_item],
15286 })))
15287 }
15288 });
15289
15290 cx.condition(|editor, _| editor.context_menu_visible())
15291 .await;
15292 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
15293 assert!(request.next().await.is_some());
15294 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15295
15296 cx.simulate_keystrokes("S o m");
15297 cx.condition(|editor, _| editor.context_menu_visible())
15298 .await;
15299 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
15300 assert!(request.next().await.is_some());
15301 assert!(request.next().await.is_some());
15302 assert!(request.next().await.is_some());
15303 request.close();
15304 assert!(request.next().await.is_none());
15305 assert_eq!(
15306 counter.load(atomic::Ordering::Acquire),
15307 4,
15308 "With the completions menu open, only one LSP request should happen per input"
15309 );
15310}
15311
15312#[gpui::test]
15313async fn test_toggle_comment(cx: &mut TestAppContext) {
15314 init_test(cx, |_| {});
15315 let mut cx = EditorTestContext::new(cx).await;
15316 let language = Arc::new(Language::new(
15317 LanguageConfig {
15318 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15319 ..Default::default()
15320 },
15321 Some(tree_sitter_rust::LANGUAGE.into()),
15322 ));
15323 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15324
15325 // If multiple selections intersect a line, the line is only toggled once.
15326 cx.set_state(indoc! {"
15327 fn a() {
15328 «//b();
15329 ˇ»// «c();
15330 //ˇ» d();
15331 }
15332 "});
15333
15334 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15335
15336 cx.assert_editor_state(indoc! {"
15337 fn a() {
15338 «b();
15339 c();
15340 ˇ» d();
15341 }
15342 "});
15343
15344 // The comment prefix is inserted at the same column for every line in a
15345 // selection.
15346 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15347
15348 cx.assert_editor_state(indoc! {"
15349 fn a() {
15350 // «b();
15351 // c();
15352 ˇ»// d();
15353 }
15354 "});
15355
15356 // If a selection ends at the beginning of a line, that line is not toggled.
15357 cx.set_selections_state(indoc! {"
15358 fn a() {
15359 // b();
15360 «// c();
15361 ˇ» // d();
15362 }
15363 "});
15364
15365 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15366
15367 cx.assert_editor_state(indoc! {"
15368 fn a() {
15369 // b();
15370 «c();
15371 ˇ» // d();
15372 }
15373 "});
15374
15375 // If a selection span a single line and is empty, the line is toggled.
15376 cx.set_state(indoc! {"
15377 fn a() {
15378 a();
15379 b();
15380 ˇ
15381 }
15382 "});
15383
15384 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15385
15386 cx.assert_editor_state(indoc! {"
15387 fn a() {
15388 a();
15389 b();
15390 //•ˇ
15391 }
15392 "});
15393
15394 // If a selection span multiple lines, empty lines are not toggled.
15395 cx.set_state(indoc! {"
15396 fn a() {
15397 «a();
15398
15399 c();ˇ»
15400 }
15401 "});
15402
15403 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15404
15405 cx.assert_editor_state(indoc! {"
15406 fn a() {
15407 // «a();
15408
15409 // c();ˇ»
15410 }
15411 "});
15412
15413 // If a selection includes multiple comment prefixes, all lines are uncommented.
15414 cx.set_state(indoc! {"
15415 fn a() {
15416 «// a();
15417 /// b();
15418 //! c();ˇ»
15419 }
15420 "});
15421
15422 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15423
15424 cx.assert_editor_state(indoc! {"
15425 fn a() {
15426 «a();
15427 b();
15428 c();ˇ»
15429 }
15430 "});
15431}
15432
15433#[gpui::test]
15434async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
15435 init_test(cx, |_| {});
15436 let mut cx = EditorTestContext::new(cx).await;
15437 let language = Arc::new(Language::new(
15438 LanguageConfig {
15439 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15440 ..Default::default()
15441 },
15442 Some(tree_sitter_rust::LANGUAGE.into()),
15443 ));
15444 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15445
15446 let toggle_comments = &ToggleComments {
15447 advance_downwards: false,
15448 ignore_indent: true,
15449 };
15450
15451 // If multiple selections intersect a line, the line is only toggled once.
15452 cx.set_state(indoc! {"
15453 fn a() {
15454 // «b();
15455 // c();
15456 // ˇ» d();
15457 }
15458 "});
15459
15460 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15461
15462 cx.assert_editor_state(indoc! {"
15463 fn a() {
15464 «b();
15465 c();
15466 ˇ» d();
15467 }
15468 "});
15469
15470 // The comment prefix is inserted at the beginning of each line
15471 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15472
15473 cx.assert_editor_state(indoc! {"
15474 fn a() {
15475 // «b();
15476 // c();
15477 // ˇ» d();
15478 }
15479 "});
15480
15481 // If a selection ends at the beginning of a line, that line is not toggled.
15482 cx.set_selections_state(indoc! {"
15483 fn a() {
15484 // b();
15485 // «c();
15486 ˇ»// d();
15487 }
15488 "});
15489
15490 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15491
15492 cx.assert_editor_state(indoc! {"
15493 fn a() {
15494 // b();
15495 «c();
15496 ˇ»// d();
15497 }
15498 "});
15499
15500 // If a selection span a single line and is empty, the line is toggled.
15501 cx.set_state(indoc! {"
15502 fn a() {
15503 a();
15504 b();
15505 ˇ
15506 }
15507 "});
15508
15509 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15510
15511 cx.assert_editor_state(indoc! {"
15512 fn a() {
15513 a();
15514 b();
15515 //ˇ
15516 }
15517 "});
15518
15519 // If a selection span multiple lines, empty lines are not toggled.
15520 cx.set_state(indoc! {"
15521 fn a() {
15522 «a();
15523
15524 c();ˇ»
15525 }
15526 "});
15527
15528 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15529
15530 cx.assert_editor_state(indoc! {"
15531 fn a() {
15532 // «a();
15533
15534 // c();ˇ»
15535 }
15536 "});
15537
15538 // If a selection includes multiple comment prefixes, all lines are uncommented.
15539 cx.set_state(indoc! {"
15540 fn a() {
15541 // «a();
15542 /// b();
15543 //! c();ˇ»
15544 }
15545 "});
15546
15547 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15548
15549 cx.assert_editor_state(indoc! {"
15550 fn a() {
15551 «a();
15552 b();
15553 c();ˇ»
15554 }
15555 "});
15556}
15557
15558#[gpui::test]
15559async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
15560 init_test(cx, |_| {});
15561
15562 let language = Arc::new(Language::new(
15563 LanguageConfig {
15564 line_comments: vec!["// ".into()],
15565 ..Default::default()
15566 },
15567 Some(tree_sitter_rust::LANGUAGE.into()),
15568 ));
15569
15570 let mut cx = EditorTestContext::new(cx).await;
15571
15572 cx.language_registry().add(language.clone());
15573 cx.update_buffer(|buffer, cx| {
15574 buffer.set_language(Some(language), cx);
15575 });
15576
15577 let toggle_comments = &ToggleComments {
15578 advance_downwards: true,
15579 ignore_indent: false,
15580 };
15581
15582 // Single cursor on one line -> advance
15583 // Cursor moves horizontally 3 characters as well on non-blank line
15584 cx.set_state(indoc!(
15585 "fn a() {
15586 ˇdog();
15587 cat();
15588 }"
15589 ));
15590 cx.update_editor(|editor, window, cx| {
15591 editor.toggle_comments(toggle_comments, window, cx);
15592 });
15593 cx.assert_editor_state(indoc!(
15594 "fn a() {
15595 // dog();
15596 catˇ();
15597 }"
15598 ));
15599
15600 // Single selection on one line -> don't advance
15601 cx.set_state(indoc!(
15602 "fn a() {
15603 «dog()ˇ»;
15604 cat();
15605 }"
15606 ));
15607 cx.update_editor(|editor, window, cx| {
15608 editor.toggle_comments(toggle_comments, window, cx);
15609 });
15610 cx.assert_editor_state(indoc!(
15611 "fn a() {
15612 // «dog()ˇ»;
15613 cat();
15614 }"
15615 ));
15616
15617 // Multiple cursors on one line -> advance
15618 cx.set_state(indoc!(
15619 "fn a() {
15620 ˇdˇog();
15621 cat();
15622 }"
15623 ));
15624 cx.update_editor(|editor, window, cx| {
15625 editor.toggle_comments(toggle_comments, window, cx);
15626 });
15627 cx.assert_editor_state(indoc!(
15628 "fn a() {
15629 // dog();
15630 catˇ(ˇ);
15631 }"
15632 ));
15633
15634 // Multiple cursors on one line, with selection -> don't advance
15635 cx.set_state(indoc!(
15636 "fn a() {
15637 ˇdˇog«()ˇ»;
15638 cat();
15639 }"
15640 ));
15641 cx.update_editor(|editor, window, cx| {
15642 editor.toggle_comments(toggle_comments, window, cx);
15643 });
15644 cx.assert_editor_state(indoc!(
15645 "fn a() {
15646 // ˇdˇog«()ˇ»;
15647 cat();
15648 }"
15649 ));
15650
15651 // Single cursor on one line -> advance
15652 // Cursor moves to column 0 on blank line
15653 cx.set_state(indoc!(
15654 "fn a() {
15655 ˇdog();
15656
15657 cat();
15658 }"
15659 ));
15660 cx.update_editor(|editor, window, cx| {
15661 editor.toggle_comments(toggle_comments, window, cx);
15662 });
15663 cx.assert_editor_state(indoc!(
15664 "fn a() {
15665 // dog();
15666 ˇ
15667 cat();
15668 }"
15669 ));
15670
15671 // Single cursor on one line -> advance
15672 // Cursor starts and ends at column 0
15673 cx.set_state(indoc!(
15674 "fn a() {
15675 ˇ dog();
15676 cat();
15677 }"
15678 ));
15679 cx.update_editor(|editor, window, cx| {
15680 editor.toggle_comments(toggle_comments, window, cx);
15681 });
15682 cx.assert_editor_state(indoc!(
15683 "fn a() {
15684 // dog();
15685 ˇ cat();
15686 }"
15687 ));
15688}
15689
15690#[gpui::test]
15691async fn test_toggle_block_comment(cx: &mut TestAppContext) {
15692 init_test(cx, |_| {});
15693
15694 let mut cx = EditorTestContext::new(cx).await;
15695
15696 let html_language = Arc::new(
15697 Language::new(
15698 LanguageConfig {
15699 name: "HTML".into(),
15700 block_comment: Some(BlockCommentConfig {
15701 start: "<!-- ".into(),
15702 prefix: "".into(),
15703 end: " -->".into(),
15704 tab_size: 0,
15705 }),
15706 ..Default::default()
15707 },
15708 Some(tree_sitter_html::LANGUAGE.into()),
15709 )
15710 .with_injection_query(
15711 r#"
15712 (script_element
15713 (raw_text) @injection.content
15714 (#set! injection.language "javascript"))
15715 "#,
15716 )
15717 .unwrap(),
15718 );
15719
15720 let javascript_language = Arc::new(Language::new(
15721 LanguageConfig {
15722 name: "JavaScript".into(),
15723 line_comments: vec!["// ".into()],
15724 ..Default::default()
15725 },
15726 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
15727 ));
15728
15729 cx.language_registry().add(html_language.clone());
15730 cx.language_registry().add(javascript_language);
15731 cx.update_buffer(|buffer, cx| {
15732 buffer.set_language(Some(html_language), cx);
15733 });
15734
15735 // Toggle comments for empty selections
15736 cx.set_state(
15737 &r#"
15738 <p>A</p>ˇ
15739 <p>B</p>ˇ
15740 <p>C</p>ˇ
15741 "#
15742 .unindent(),
15743 );
15744 cx.update_editor(|editor, window, cx| {
15745 editor.toggle_comments(&ToggleComments::default(), window, cx)
15746 });
15747 cx.assert_editor_state(
15748 &r#"
15749 <!-- <p>A</p>ˇ -->
15750 <!-- <p>B</p>ˇ -->
15751 <!-- <p>C</p>ˇ -->
15752 "#
15753 .unindent(),
15754 );
15755 cx.update_editor(|editor, window, cx| {
15756 editor.toggle_comments(&ToggleComments::default(), window, cx)
15757 });
15758 cx.assert_editor_state(
15759 &r#"
15760 <p>A</p>ˇ
15761 <p>B</p>ˇ
15762 <p>C</p>ˇ
15763 "#
15764 .unindent(),
15765 );
15766
15767 // Toggle comments for mixture of empty and non-empty selections, where
15768 // multiple selections occupy a given line.
15769 cx.set_state(
15770 &r#"
15771 <p>A«</p>
15772 <p>ˇ»B</p>ˇ
15773 <p>C«</p>
15774 <p>ˇ»D</p>ˇ
15775 "#
15776 .unindent(),
15777 );
15778
15779 cx.update_editor(|editor, window, cx| {
15780 editor.toggle_comments(&ToggleComments::default(), window, cx)
15781 });
15782 cx.assert_editor_state(
15783 &r#"
15784 <!-- <p>A«</p>
15785 <p>ˇ»B</p>ˇ -->
15786 <!-- <p>C«</p>
15787 <p>ˇ»D</p>ˇ -->
15788 "#
15789 .unindent(),
15790 );
15791 cx.update_editor(|editor, window, cx| {
15792 editor.toggle_comments(&ToggleComments::default(), window, cx)
15793 });
15794 cx.assert_editor_state(
15795 &r#"
15796 <p>A«</p>
15797 <p>ˇ»B</p>ˇ
15798 <p>C«</p>
15799 <p>ˇ»D</p>ˇ
15800 "#
15801 .unindent(),
15802 );
15803
15804 // Toggle comments when different languages are active for different
15805 // selections.
15806 cx.set_state(
15807 &r#"
15808 ˇ<script>
15809 ˇvar x = new Y();
15810 ˇ</script>
15811 "#
15812 .unindent(),
15813 );
15814 cx.executor().run_until_parked();
15815 cx.update_editor(|editor, window, cx| {
15816 editor.toggle_comments(&ToggleComments::default(), window, cx)
15817 });
15818 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
15819 // Uncommenting and commenting from this position brings in even more wrong artifacts.
15820 cx.assert_editor_state(
15821 &r#"
15822 <!-- ˇ<script> -->
15823 // ˇvar x = new Y();
15824 <!-- ˇ</script> -->
15825 "#
15826 .unindent(),
15827 );
15828}
15829
15830#[gpui::test]
15831fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
15832 init_test(cx, |_| {});
15833
15834 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15835 let multibuffer = cx.new(|cx| {
15836 let mut multibuffer = MultiBuffer::new(ReadWrite);
15837 multibuffer.push_excerpts(
15838 buffer.clone(),
15839 [
15840 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
15841 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
15842 ],
15843 cx,
15844 );
15845 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
15846 multibuffer
15847 });
15848
15849 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
15850 editor.update_in(cx, |editor, window, cx| {
15851 assert_eq!(editor.text(cx), "aaaa\nbbbb");
15852 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15853 s.select_ranges([
15854 Point::new(0, 0)..Point::new(0, 0),
15855 Point::new(1, 0)..Point::new(1, 0),
15856 ])
15857 });
15858
15859 editor.handle_input("X", window, cx);
15860 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
15861 assert_eq!(
15862 editor.selections.ranges(cx),
15863 [
15864 Point::new(0, 1)..Point::new(0, 1),
15865 Point::new(1, 1)..Point::new(1, 1),
15866 ]
15867 );
15868
15869 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
15870 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15871 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
15872 });
15873 editor.backspace(&Default::default(), window, cx);
15874 assert_eq!(editor.text(cx), "Xa\nbbb");
15875 assert_eq!(
15876 editor.selections.ranges(cx),
15877 [Point::new(1, 0)..Point::new(1, 0)]
15878 );
15879
15880 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15881 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
15882 });
15883 editor.backspace(&Default::default(), window, cx);
15884 assert_eq!(editor.text(cx), "X\nbb");
15885 assert_eq!(
15886 editor.selections.ranges(cx),
15887 [Point::new(0, 1)..Point::new(0, 1)]
15888 );
15889 });
15890}
15891
15892#[gpui::test]
15893fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
15894 init_test(cx, |_| {});
15895
15896 let markers = vec![('[', ']').into(), ('(', ')').into()];
15897 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
15898 indoc! {"
15899 [aaaa
15900 (bbbb]
15901 cccc)",
15902 },
15903 markers.clone(),
15904 );
15905 let excerpt_ranges = markers.into_iter().map(|marker| {
15906 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
15907 ExcerptRange::new(context)
15908 });
15909 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
15910 let multibuffer = cx.new(|cx| {
15911 let mut multibuffer = MultiBuffer::new(ReadWrite);
15912 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
15913 multibuffer
15914 });
15915
15916 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
15917 editor.update_in(cx, |editor, window, cx| {
15918 let (expected_text, selection_ranges) = marked_text_ranges(
15919 indoc! {"
15920 aaaa
15921 bˇbbb
15922 bˇbbˇb
15923 cccc"
15924 },
15925 true,
15926 );
15927 assert_eq!(editor.text(cx), expected_text);
15928 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15929 s.select_ranges(selection_ranges)
15930 });
15931
15932 editor.handle_input("X", window, cx);
15933
15934 let (expected_text, expected_selections) = marked_text_ranges(
15935 indoc! {"
15936 aaaa
15937 bXˇbbXb
15938 bXˇbbXˇb
15939 cccc"
15940 },
15941 false,
15942 );
15943 assert_eq!(editor.text(cx), expected_text);
15944 assert_eq!(editor.selections.ranges(cx), expected_selections);
15945
15946 editor.newline(&Newline, window, cx);
15947 let (expected_text, expected_selections) = marked_text_ranges(
15948 indoc! {"
15949 aaaa
15950 bX
15951 ˇbbX
15952 b
15953 bX
15954 ˇbbX
15955 ˇb
15956 cccc"
15957 },
15958 false,
15959 );
15960 assert_eq!(editor.text(cx), expected_text);
15961 assert_eq!(editor.selections.ranges(cx), expected_selections);
15962 });
15963}
15964
15965#[gpui::test]
15966fn test_refresh_selections(cx: &mut TestAppContext) {
15967 init_test(cx, |_| {});
15968
15969 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15970 let mut excerpt1_id = None;
15971 let multibuffer = cx.new(|cx| {
15972 let mut multibuffer = MultiBuffer::new(ReadWrite);
15973 excerpt1_id = multibuffer
15974 .push_excerpts(
15975 buffer.clone(),
15976 [
15977 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
15978 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
15979 ],
15980 cx,
15981 )
15982 .into_iter()
15983 .next();
15984 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
15985 multibuffer
15986 });
15987
15988 let editor = cx.add_window(|window, cx| {
15989 let mut editor = build_editor(multibuffer.clone(), window, cx);
15990 let snapshot = editor.snapshot(window, cx);
15991 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15992 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
15993 });
15994 editor.begin_selection(
15995 Point::new(2, 1).to_display_point(&snapshot),
15996 true,
15997 1,
15998 window,
15999 cx,
16000 );
16001 assert_eq!(
16002 editor.selections.ranges(cx),
16003 [
16004 Point::new(1, 3)..Point::new(1, 3),
16005 Point::new(2, 1)..Point::new(2, 1),
16006 ]
16007 );
16008 editor
16009 });
16010
16011 // Refreshing selections is a no-op when excerpts haven't changed.
16012 _ = editor.update(cx, |editor, window, cx| {
16013 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16014 assert_eq!(
16015 editor.selections.ranges(cx),
16016 [
16017 Point::new(1, 3)..Point::new(1, 3),
16018 Point::new(2, 1)..Point::new(2, 1),
16019 ]
16020 );
16021 });
16022
16023 multibuffer.update(cx, |multibuffer, cx| {
16024 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16025 });
16026 _ = editor.update(cx, |editor, window, cx| {
16027 // Removing an excerpt causes the first selection to become degenerate.
16028 assert_eq!(
16029 editor.selections.ranges(cx),
16030 [
16031 Point::new(0, 0)..Point::new(0, 0),
16032 Point::new(0, 1)..Point::new(0, 1)
16033 ]
16034 );
16035
16036 // Refreshing selections will relocate the first selection to the original buffer
16037 // location.
16038 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16039 assert_eq!(
16040 editor.selections.ranges(cx),
16041 [
16042 Point::new(0, 1)..Point::new(0, 1),
16043 Point::new(0, 3)..Point::new(0, 3)
16044 ]
16045 );
16046 assert!(editor.selections.pending_anchor().is_some());
16047 });
16048}
16049
16050#[gpui::test]
16051fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
16052 init_test(cx, |_| {});
16053
16054 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16055 let mut excerpt1_id = None;
16056 let multibuffer = cx.new(|cx| {
16057 let mut multibuffer = MultiBuffer::new(ReadWrite);
16058 excerpt1_id = multibuffer
16059 .push_excerpts(
16060 buffer.clone(),
16061 [
16062 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
16063 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
16064 ],
16065 cx,
16066 )
16067 .into_iter()
16068 .next();
16069 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16070 multibuffer
16071 });
16072
16073 let editor = cx.add_window(|window, cx| {
16074 let mut editor = build_editor(multibuffer.clone(), window, cx);
16075 let snapshot = editor.snapshot(window, cx);
16076 editor.begin_selection(
16077 Point::new(1, 3).to_display_point(&snapshot),
16078 false,
16079 1,
16080 window,
16081 cx,
16082 );
16083 assert_eq!(
16084 editor.selections.ranges(cx),
16085 [Point::new(1, 3)..Point::new(1, 3)]
16086 );
16087 editor
16088 });
16089
16090 multibuffer.update(cx, |multibuffer, cx| {
16091 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16092 });
16093 _ = editor.update(cx, |editor, window, cx| {
16094 assert_eq!(
16095 editor.selections.ranges(cx),
16096 [Point::new(0, 0)..Point::new(0, 0)]
16097 );
16098
16099 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
16100 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16101 assert_eq!(
16102 editor.selections.ranges(cx),
16103 [Point::new(0, 3)..Point::new(0, 3)]
16104 );
16105 assert!(editor.selections.pending_anchor().is_some());
16106 });
16107}
16108
16109#[gpui::test]
16110async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
16111 init_test(cx, |_| {});
16112
16113 let language = Arc::new(
16114 Language::new(
16115 LanguageConfig {
16116 brackets: BracketPairConfig {
16117 pairs: vec![
16118 BracketPair {
16119 start: "{".to_string(),
16120 end: "}".to_string(),
16121 close: true,
16122 surround: true,
16123 newline: true,
16124 },
16125 BracketPair {
16126 start: "/* ".to_string(),
16127 end: " */".to_string(),
16128 close: true,
16129 surround: true,
16130 newline: true,
16131 },
16132 ],
16133 ..Default::default()
16134 },
16135 ..Default::default()
16136 },
16137 Some(tree_sitter_rust::LANGUAGE.into()),
16138 )
16139 .with_indents_query("")
16140 .unwrap(),
16141 );
16142
16143 let text = concat!(
16144 "{ }\n", //
16145 " x\n", //
16146 " /* */\n", //
16147 "x\n", //
16148 "{{} }\n", //
16149 );
16150
16151 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
16152 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16153 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16154 editor
16155 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
16156 .await;
16157
16158 editor.update_in(cx, |editor, window, cx| {
16159 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16160 s.select_display_ranges([
16161 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
16162 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
16163 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
16164 ])
16165 });
16166 editor.newline(&Newline, window, cx);
16167
16168 assert_eq!(
16169 editor.buffer().read(cx).read(cx).text(),
16170 concat!(
16171 "{ \n", // Suppress rustfmt
16172 "\n", //
16173 "}\n", //
16174 " x\n", //
16175 " /* \n", //
16176 " \n", //
16177 " */\n", //
16178 "x\n", //
16179 "{{} \n", //
16180 "}\n", //
16181 )
16182 );
16183 });
16184}
16185
16186#[gpui::test]
16187fn test_highlighted_ranges(cx: &mut TestAppContext) {
16188 init_test(cx, |_| {});
16189
16190 let editor = cx.add_window(|window, cx| {
16191 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
16192 build_editor(buffer, window, cx)
16193 });
16194
16195 _ = editor.update(cx, |editor, window, cx| {
16196 struct Type1;
16197 struct Type2;
16198
16199 let buffer = editor.buffer.read(cx).snapshot(cx);
16200
16201 let anchor_range =
16202 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
16203
16204 editor.highlight_background::<Type1>(
16205 &[
16206 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
16207 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
16208 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
16209 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
16210 ],
16211 |_| Hsla::red(),
16212 cx,
16213 );
16214 editor.highlight_background::<Type2>(
16215 &[
16216 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
16217 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
16218 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
16219 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
16220 ],
16221 |_| Hsla::green(),
16222 cx,
16223 );
16224
16225 let snapshot = editor.snapshot(window, cx);
16226 let highlighted_ranges = editor.sorted_background_highlights_in_range(
16227 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
16228 &snapshot,
16229 cx.theme(),
16230 );
16231 assert_eq!(
16232 highlighted_ranges,
16233 &[
16234 (
16235 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
16236 Hsla::green(),
16237 ),
16238 (
16239 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
16240 Hsla::red(),
16241 ),
16242 (
16243 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
16244 Hsla::green(),
16245 ),
16246 (
16247 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16248 Hsla::red(),
16249 ),
16250 ]
16251 );
16252 assert_eq!(
16253 editor.sorted_background_highlights_in_range(
16254 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
16255 &snapshot,
16256 cx.theme(),
16257 ),
16258 &[(
16259 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16260 Hsla::red(),
16261 )]
16262 );
16263 });
16264}
16265
16266#[gpui::test]
16267async fn test_following(cx: &mut TestAppContext) {
16268 init_test(cx, |_| {});
16269
16270 let fs = FakeFs::new(cx.executor());
16271 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16272
16273 let buffer = project.update(cx, |project, cx| {
16274 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, false, cx);
16275 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
16276 });
16277 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
16278 let follower = cx.update(|cx| {
16279 cx.open_window(
16280 WindowOptions {
16281 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
16282 gpui::Point::new(px(0.), px(0.)),
16283 gpui::Point::new(px(10.), px(80.)),
16284 ))),
16285 ..Default::default()
16286 },
16287 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
16288 )
16289 .unwrap()
16290 });
16291
16292 let is_still_following = Rc::new(RefCell::new(true));
16293 let follower_edit_event_count = Rc::new(RefCell::new(0));
16294 let pending_update = Rc::new(RefCell::new(None));
16295 let leader_entity = leader.root(cx).unwrap();
16296 let follower_entity = follower.root(cx).unwrap();
16297 _ = follower.update(cx, {
16298 let update = pending_update.clone();
16299 let is_still_following = is_still_following.clone();
16300 let follower_edit_event_count = follower_edit_event_count.clone();
16301 |_, window, cx| {
16302 cx.subscribe_in(
16303 &leader_entity,
16304 window,
16305 move |_, leader, event, window, cx| {
16306 leader.read(cx).add_event_to_update_proto(
16307 event,
16308 &mut update.borrow_mut(),
16309 window,
16310 cx,
16311 );
16312 },
16313 )
16314 .detach();
16315
16316 cx.subscribe_in(
16317 &follower_entity,
16318 window,
16319 move |_, _, event: &EditorEvent, _window, _cx| {
16320 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
16321 *is_still_following.borrow_mut() = false;
16322 }
16323
16324 if let EditorEvent::BufferEdited = event {
16325 *follower_edit_event_count.borrow_mut() += 1;
16326 }
16327 },
16328 )
16329 .detach();
16330 }
16331 });
16332
16333 // Update the selections only
16334 _ = leader.update(cx, |leader, window, cx| {
16335 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16336 s.select_ranges([1..1])
16337 });
16338 });
16339 follower
16340 .update(cx, |follower, window, cx| {
16341 follower.apply_update_proto(
16342 &project,
16343 pending_update.borrow_mut().take().unwrap(),
16344 window,
16345 cx,
16346 )
16347 })
16348 .unwrap()
16349 .await
16350 .unwrap();
16351 _ = follower.update(cx, |follower, _, cx| {
16352 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
16353 });
16354 assert!(*is_still_following.borrow());
16355 assert_eq!(*follower_edit_event_count.borrow(), 0);
16356
16357 // Update the scroll position only
16358 _ = leader.update(cx, |leader, window, cx| {
16359 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16360 });
16361 follower
16362 .update(cx, |follower, window, cx| {
16363 follower.apply_update_proto(
16364 &project,
16365 pending_update.borrow_mut().take().unwrap(),
16366 window,
16367 cx,
16368 )
16369 })
16370 .unwrap()
16371 .await
16372 .unwrap();
16373 assert_eq!(
16374 follower
16375 .update(cx, |follower, _, cx| follower.scroll_position(cx))
16376 .unwrap(),
16377 gpui::Point::new(1.5, 3.5)
16378 );
16379 assert!(*is_still_following.borrow());
16380 assert_eq!(*follower_edit_event_count.borrow(), 0);
16381
16382 // Update the selections and scroll position. The follower's scroll position is updated
16383 // via autoscroll, not via the leader's exact scroll position.
16384 _ = leader.update(cx, |leader, window, cx| {
16385 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16386 s.select_ranges([0..0])
16387 });
16388 leader.request_autoscroll(Autoscroll::newest(), cx);
16389 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16390 });
16391 follower
16392 .update(cx, |follower, window, cx| {
16393 follower.apply_update_proto(
16394 &project,
16395 pending_update.borrow_mut().take().unwrap(),
16396 window,
16397 cx,
16398 )
16399 })
16400 .unwrap()
16401 .await
16402 .unwrap();
16403 _ = follower.update(cx, |follower, _, cx| {
16404 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
16405 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
16406 });
16407 assert!(*is_still_following.borrow());
16408
16409 // Creating a pending selection that precedes another selection
16410 _ = leader.update(cx, |leader, window, cx| {
16411 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16412 s.select_ranges([1..1])
16413 });
16414 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
16415 });
16416 follower
16417 .update(cx, |follower, window, cx| {
16418 follower.apply_update_proto(
16419 &project,
16420 pending_update.borrow_mut().take().unwrap(),
16421 window,
16422 cx,
16423 )
16424 })
16425 .unwrap()
16426 .await
16427 .unwrap();
16428 _ = follower.update(cx, |follower, _, cx| {
16429 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
16430 });
16431 assert!(*is_still_following.borrow());
16432
16433 // Extend the pending selection so that it surrounds another selection
16434 _ = leader.update(cx, |leader, window, cx| {
16435 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
16436 });
16437 follower
16438 .update(cx, |follower, window, cx| {
16439 follower.apply_update_proto(
16440 &project,
16441 pending_update.borrow_mut().take().unwrap(),
16442 window,
16443 cx,
16444 )
16445 })
16446 .unwrap()
16447 .await
16448 .unwrap();
16449 _ = follower.update(cx, |follower, _, cx| {
16450 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
16451 });
16452
16453 // Scrolling locally breaks the follow
16454 _ = follower.update(cx, |follower, window, cx| {
16455 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
16456 follower.set_scroll_anchor(
16457 ScrollAnchor {
16458 anchor: top_anchor,
16459 offset: gpui::Point::new(0.0, 0.5),
16460 },
16461 window,
16462 cx,
16463 );
16464 });
16465 assert!(!(*is_still_following.borrow()));
16466}
16467
16468#[gpui::test]
16469async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
16470 init_test(cx, |_| {});
16471
16472 let fs = FakeFs::new(cx.executor());
16473 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16474 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16475 let pane = workspace
16476 .update(cx, |workspace, _, _| workspace.active_pane().clone())
16477 .unwrap();
16478
16479 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16480
16481 let leader = pane.update_in(cx, |_, window, cx| {
16482 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
16483 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
16484 });
16485
16486 // Start following the editor when it has no excerpts.
16487 let mut state_message =
16488 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16489 let workspace_entity = workspace.root(cx).unwrap();
16490 let follower_1 = cx
16491 .update_window(*workspace.deref(), |_, window, cx| {
16492 Editor::from_state_proto(
16493 workspace_entity,
16494 ViewId {
16495 creator: CollaboratorId::PeerId(PeerId::default()),
16496 id: 0,
16497 },
16498 &mut state_message,
16499 window,
16500 cx,
16501 )
16502 })
16503 .unwrap()
16504 .unwrap()
16505 .await
16506 .unwrap();
16507
16508 let update_message = Rc::new(RefCell::new(None));
16509 follower_1.update_in(cx, {
16510 let update = update_message.clone();
16511 |_, window, cx| {
16512 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
16513 leader.read(cx).add_event_to_update_proto(
16514 event,
16515 &mut update.borrow_mut(),
16516 window,
16517 cx,
16518 );
16519 })
16520 .detach();
16521 }
16522 });
16523
16524 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
16525 (
16526 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, false, cx),
16527 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, false, cx),
16528 )
16529 });
16530
16531 // Insert some excerpts.
16532 leader.update(cx, |leader, cx| {
16533 leader.buffer.update(cx, |multibuffer, cx| {
16534 multibuffer.set_excerpts_for_path(
16535 PathKey::namespaced(1, rel_path("b.txt").into_arc()),
16536 buffer_1.clone(),
16537 vec![
16538 Point::row_range(0..3),
16539 Point::row_range(1..6),
16540 Point::row_range(12..15),
16541 ],
16542 0,
16543 cx,
16544 );
16545 multibuffer.set_excerpts_for_path(
16546 PathKey::namespaced(1, rel_path("a.txt").into_arc()),
16547 buffer_2.clone(),
16548 vec![Point::row_range(0..6), Point::row_range(8..12)],
16549 0,
16550 cx,
16551 );
16552 });
16553 });
16554
16555 // Apply the update of adding the excerpts.
16556 follower_1
16557 .update_in(cx, |follower, window, cx| {
16558 follower.apply_update_proto(
16559 &project,
16560 update_message.borrow().clone().unwrap(),
16561 window,
16562 cx,
16563 )
16564 })
16565 .await
16566 .unwrap();
16567 assert_eq!(
16568 follower_1.update(cx, |editor, cx| editor.text(cx)),
16569 leader.update(cx, |editor, cx| editor.text(cx))
16570 );
16571 update_message.borrow_mut().take();
16572
16573 // Start following separately after it already has excerpts.
16574 let mut state_message =
16575 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16576 let workspace_entity = workspace.root(cx).unwrap();
16577 let follower_2 = cx
16578 .update_window(*workspace.deref(), |_, window, cx| {
16579 Editor::from_state_proto(
16580 workspace_entity,
16581 ViewId {
16582 creator: CollaboratorId::PeerId(PeerId::default()),
16583 id: 0,
16584 },
16585 &mut state_message,
16586 window,
16587 cx,
16588 )
16589 })
16590 .unwrap()
16591 .unwrap()
16592 .await
16593 .unwrap();
16594 assert_eq!(
16595 follower_2.update(cx, |editor, cx| editor.text(cx)),
16596 leader.update(cx, |editor, cx| editor.text(cx))
16597 );
16598
16599 // Remove some excerpts.
16600 leader.update(cx, |leader, cx| {
16601 leader.buffer.update(cx, |multibuffer, cx| {
16602 let excerpt_ids = multibuffer.excerpt_ids();
16603 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
16604 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
16605 });
16606 });
16607
16608 // Apply the update of removing the excerpts.
16609 follower_1
16610 .update_in(cx, |follower, window, cx| {
16611 follower.apply_update_proto(
16612 &project,
16613 update_message.borrow().clone().unwrap(),
16614 window,
16615 cx,
16616 )
16617 })
16618 .await
16619 .unwrap();
16620 follower_2
16621 .update_in(cx, |follower, window, cx| {
16622 follower.apply_update_proto(
16623 &project,
16624 update_message.borrow().clone().unwrap(),
16625 window,
16626 cx,
16627 )
16628 })
16629 .await
16630 .unwrap();
16631 update_message.borrow_mut().take();
16632 assert_eq!(
16633 follower_1.update(cx, |editor, cx| editor.text(cx)),
16634 leader.update(cx, |editor, cx| editor.text(cx))
16635 );
16636}
16637
16638#[gpui::test]
16639async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16640 init_test(cx, |_| {});
16641
16642 let mut cx = EditorTestContext::new(cx).await;
16643 let lsp_store =
16644 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
16645
16646 cx.set_state(indoc! {"
16647 ˇfn func(abc def: i32) -> u32 {
16648 }
16649 "});
16650
16651 cx.update(|_, cx| {
16652 lsp_store.update(cx, |lsp_store, cx| {
16653 lsp_store
16654 .update_diagnostics(
16655 LanguageServerId(0),
16656 lsp::PublishDiagnosticsParams {
16657 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
16658 version: None,
16659 diagnostics: vec![
16660 lsp::Diagnostic {
16661 range: lsp::Range::new(
16662 lsp::Position::new(0, 11),
16663 lsp::Position::new(0, 12),
16664 ),
16665 severity: Some(lsp::DiagnosticSeverity::ERROR),
16666 ..Default::default()
16667 },
16668 lsp::Diagnostic {
16669 range: lsp::Range::new(
16670 lsp::Position::new(0, 12),
16671 lsp::Position::new(0, 15),
16672 ),
16673 severity: Some(lsp::DiagnosticSeverity::ERROR),
16674 ..Default::default()
16675 },
16676 lsp::Diagnostic {
16677 range: lsp::Range::new(
16678 lsp::Position::new(0, 25),
16679 lsp::Position::new(0, 28),
16680 ),
16681 severity: Some(lsp::DiagnosticSeverity::ERROR),
16682 ..Default::default()
16683 },
16684 ],
16685 },
16686 None,
16687 DiagnosticSourceKind::Pushed,
16688 &[],
16689 cx,
16690 )
16691 .unwrap()
16692 });
16693 });
16694
16695 executor.run_until_parked();
16696
16697 cx.update_editor(|editor, window, cx| {
16698 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16699 });
16700
16701 cx.assert_editor_state(indoc! {"
16702 fn func(abc def: i32) -> ˇu32 {
16703 }
16704 "});
16705
16706 cx.update_editor(|editor, window, cx| {
16707 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16708 });
16709
16710 cx.assert_editor_state(indoc! {"
16711 fn func(abc ˇdef: i32) -> u32 {
16712 }
16713 "});
16714
16715 cx.update_editor(|editor, window, cx| {
16716 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16717 });
16718
16719 cx.assert_editor_state(indoc! {"
16720 fn func(abcˇ def: i32) -> u32 {
16721 }
16722 "});
16723
16724 cx.update_editor(|editor, window, cx| {
16725 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16726 });
16727
16728 cx.assert_editor_state(indoc! {"
16729 fn func(abc def: i32) -> ˇu32 {
16730 }
16731 "});
16732}
16733
16734#[gpui::test]
16735async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16736 init_test(cx, |_| {});
16737
16738 let mut cx = EditorTestContext::new(cx).await;
16739
16740 let diff_base = r#"
16741 use some::mod;
16742
16743 const A: u32 = 42;
16744
16745 fn main() {
16746 println!("hello");
16747
16748 println!("world");
16749 }
16750 "#
16751 .unindent();
16752
16753 // Edits are modified, removed, modified, added
16754 cx.set_state(
16755 &r#"
16756 use some::modified;
16757
16758 ˇ
16759 fn main() {
16760 println!("hello there");
16761
16762 println!("around the");
16763 println!("world");
16764 }
16765 "#
16766 .unindent(),
16767 );
16768
16769 cx.set_head_text(&diff_base);
16770 executor.run_until_parked();
16771
16772 cx.update_editor(|editor, window, cx| {
16773 //Wrap around the bottom of the buffer
16774 for _ in 0..3 {
16775 editor.go_to_next_hunk(&GoToHunk, window, cx);
16776 }
16777 });
16778
16779 cx.assert_editor_state(
16780 &r#"
16781 ˇuse some::modified;
16782
16783
16784 fn main() {
16785 println!("hello there");
16786
16787 println!("around the");
16788 println!("world");
16789 }
16790 "#
16791 .unindent(),
16792 );
16793
16794 cx.update_editor(|editor, window, cx| {
16795 //Wrap around the top of the buffer
16796 for _ in 0..2 {
16797 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16798 }
16799 });
16800
16801 cx.assert_editor_state(
16802 &r#"
16803 use some::modified;
16804
16805
16806 fn main() {
16807 ˇ println!("hello there");
16808
16809 println!("around the");
16810 println!("world");
16811 }
16812 "#
16813 .unindent(),
16814 );
16815
16816 cx.update_editor(|editor, window, cx| {
16817 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16818 });
16819
16820 cx.assert_editor_state(
16821 &r#"
16822 use some::modified;
16823
16824 ˇ
16825 fn main() {
16826 println!("hello there");
16827
16828 println!("around the");
16829 println!("world");
16830 }
16831 "#
16832 .unindent(),
16833 );
16834
16835 cx.update_editor(|editor, window, cx| {
16836 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16837 });
16838
16839 cx.assert_editor_state(
16840 &r#"
16841 ˇuse some::modified;
16842
16843
16844 fn main() {
16845 println!("hello there");
16846
16847 println!("around the");
16848 println!("world");
16849 }
16850 "#
16851 .unindent(),
16852 );
16853
16854 cx.update_editor(|editor, window, cx| {
16855 for _ in 0..2 {
16856 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16857 }
16858 });
16859
16860 cx.assert_editor_state(
16861 &r#"
16862 use some::modified;
16863
16864
16865 fn main() {
16866 ˇ println!("hello there");
16867
16868 println!("around the");
16869 println!("world");
16870 }
16871 "#
16872 .unindent(),
16873 );
16874
16875 cx.update_editor(|editor, window, cx| {
16876 editor.fold(&Fold, window, cx);
16877 });
16878
16879 cx.update_editor(|editor, window, cx| {
16880 editor.go_to_next_hunk(&GoToHunk, window, cx);
16881 });
16882
16883 cx.assert_editor_state(
16884 &r#"
16885 ˇuse some::modified;
16886
16887
16888 fn main() {
16889 println!("hello there");
16890
16891 println!("around the");
16892 println!("world");
16893 }
16894 "#
16895 .unindent(),
16896 );
16897}
16898
16899#[test]
16900fn test_split_words() {
16901 fn split(text: &str) -> Vec<&str> {
16902 split_words(text).collect()
16903 }
16904
16905 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
16906 assert_eq!(split("hello_world"), &["hello_", "world"]);
16907 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
16908 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
16909 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
16910 assert_eq!(split("helloworld"), &["helloworld"]);
16911
16912 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
16913}
16914
16915#[gpui::test]
16916async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
16917 init_test(cx, |_| {});
16918
16919 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
16920 let mut assert = |before, after| {
16921 let _state_context = cx.set_state(before);
16922 cx.run_until_parked();
16923 cx.update_editor(|editor, window, cx| {
16924 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
16925 });
16926 cx.run_until_parked();
16927 cx.assert_editor_state(after);
16928 };
16929
16930 // Outside bracket jumps to outside of matching bracket
16931 assert("console.logˇ(var);", "console.log(var)ˇ;");
16932 assert("console.log(var)ˇ;", "console.logˇ(var);");
16933
16934 // Inside bracket jumps to inside of matching bracket
16935 assert("console.log(ˇvar);", "console.log(varˇ);");
16936 assert("console.log(varˇ);", "console.log(ˇvar);");
16937
16938 // When outside a bracket and inside, favor jumping to the inside bracket
16939 assert(
16940 "console.log('foo', [1, 2, 3]ˇ);",
16941 "console.log(ˇ'foo', [1, 2, 3]);",
16942 );
16943 assert(
16944 "console.log(ˇ'foo', [1, 2, 3]);",
16945 "console.log('foo', [1, 2, 3]ˇ);",
16946 );
16947
16948 // Bias forward if two options are equally likely
16949 assert(
16950 "let result = curried_fun()ˇ();",
16951 "let result = curried_fun()()ˇ;",
16952 );
16953
16954 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
16955 assert(
16956 indoc! {"
16957 function test() {
16958 console.log('test')ˇ
16959 }"},
16960 indoc! {"
16961 function test() {
16962 console.logˇ('test')
16963 }"},
16964 );
16965}
16966
16967#[gpui::test]
16968async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
16969 init_test(cx, |_| {});
16970
16971 let fs = FakeFs::new(cx.executor());
16972 fs.insert_tree(
16973 path!("/a"),
16974 json!({
16975 "main.rs": "fn main() { let a = 5; }",
16976 "other.rs": "// Test file",
16977 }),
16978 )
16979 .await;
16980 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16981
16982 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
16983 language_registry.add(Arc::new(Language::new(
16984 LanguageConfig {
16985 name: "Rust".into(),
16986 matcher: LanguageMatcher {
16987 path_suffixes: vec!["rs".to_string()],
16988 ..Default::default()
16989 },
16990 brackets: BracketPairConfig {
16991 pairs: vec![BracketPair {
16992 start: "{".to_string(),
16993 end: "}".to_string(),
16994 close: true,
16995 surround: true,
16996 newline: true,
16997 }],
16998 disabled_scopes_by_bracket_ix: Vec::new(),
16999 },
17000 ..Default::default()
17001 },
17002 Some(tree_sitter_rust::LANGUAGE.into()),
17003 )));
17004 let mut fake_servers = language_registry.register_fake_lsp(
17005 "Rust",
17006 FakeLspAdapter {
17007 capabilities: lsp::ServerCapabilities {
17008 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
17009 first_trigger_character: "{".to_string(),
17010 more_trigger_character: None,
17011 }),
17012 ..Default::default()
17013 },
17014 ..Default::default()
17015 },
17016 );
17017
17018 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17019
17020 let cx = &mut VisualTestContext::from_window(*workspace, cx);
17021
17022 let worktree_id = workspace
17023 .update(cx, |workspace, _, cx| {
17024 workspace.project().update(cx, |project, cx| {
17025 project.worktrees(cx).next().unwrap().read(cx).id()
17026 })
17027 })
17028 .unwrap();
17029
17030 let buffer = project
17031 .update(cx, |project, cx| {
17032 project.open_local_buffer(path!("/a/main.rs"), cx)
17033 })
17034 .await
17035 .unwrap();
17036 let editor_handle = workspace
17037 .update(cx, |workspace, window, cx| {
17038 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
17039 })
17040 .unwrap()
17041 .await
17042 .unwrap()
17043 .downcast::<Editor>()
17044 .unwrap();
17045
17046 cx.executor().start_waiting();
17047 let fake_server = fake_servers.next().await.unwrap();
17048
17049 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
17050 |params, _| async move {
17051 assert_eq!(
17052 params.text_document_position.text_document.uri,
17053 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
17054 );
17055 assert_eq!(
17056 params.text_document_position.position,
17057 lsp::Position::new(0, 21),
17058 );
17059
17060 Ok(Some(vec![lsp::TextEdit {
17061 new_text: "]".to_string(),
17062 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17063 }]))
17064 },
17065 );
17066
17067 editor_handle.update_in(cx, |editor, window, cx| {
17068 window.focus(&editor.focus_handle(cx));
17069 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17070 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
17071 });
17072 editor.handle_input("{", window, cx);
17073 });
17074
17075 cx.executor().run_until_parked();
17076
17077 buffer.update(cx, |buffer, _| {
17078 assert_eq!(
17079 buffer.text(),
17080 "fn main() { let a = {5}; }",
17081 "No extra braces from on type formatting should appear in the buffer"
17082 )
17083 });
17084}
17085
17086#[gpui::test(iterations = 20, seeds(31))]
17087async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
17088 init_test(cx, |_| {});
17089
17090 let mut cx = EditorLspTestContext::new_rust(
17091 lsp::ServerCapabilities {
17092 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
17093 first_trigger_character: ".".to_string(),
17094 more_trigger_character: None,
17095 }),
17096 ..Default::default()
17097 },
17098 cx,
17099 )
17100 .await;
17101
17102 cx.update_buffer(|buffer, _| {
17103 // This causes autoindent to be async.
17104 buffer.set_sync_parse_timeout(Duration::ZERO)
17105 });
17106
17107 cx.set_state("fn c() {\n d()ˇ\n}\n");
17108 cx.simulate_keystroke("\n");
17109 cx.run_until_parked();
17110
17111 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
17112 let mut request =
17113 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
17114 let buffer_cloned = buffer_cloned.clone();
17115 async move {
17116 buffer_cloned.update(&mut cx, |buffer, _| {
17117 assert_eq!(
17118 buffer.text(),
17119 "fn c() {\n d()\n .\n}\n",
17120 "OnTypeFormatting should triggered after autoindent applied"
17121 )
17122 })?;
17123
17124 Ok(Some(vec![]))
17125 }
17126 });
17127
17128 cx.simulate_keystroke(".");
17129 cx.run_until_parked();
17130
17131 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
17132 assert!(request.next().await.is_some());
17133 request.close();
17134 assert!(request.next().await.is_none());
17135}
17136
17137#[gpui::test]
17138async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
17139 init_test(cx, |_| {});
17140
17141 let fs = FakeFs::new(cx.executor());
17142 fs.insert_tree(
17143 path!("/a"),
17144 json!({
17145 "main.rs": "fn main() { let a = 5; }",
17146 "other.rs": "// Test file",
17147 }),
17148 )
17149 .await;
17150
17151 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17152
17153 let server_restarts = Arc::new(AtomicUsize::new(0));
17154 let closure_restarts = Arc::clone(&server_restarts);
17155 let language_server_name = "test language server";
17156 let language_name: LanguageName = "Rust".into();
17157
17158 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17159 language_registry.add(Arc::new(Language::new(
17160 LanguageConfig {
17161 name: language_name.clone(),
17162 matcher: LanguageMatcher {
17163 path_suffixes: vec!["rs".to_string()],
17164 ..Default::default()
17165 },
17166 ..Default::default()
17167 },
17168 Some(tree_sitter_rust::LANGUAGE.into()),
17169 )));
17170 let mut fake_servers = language_registry.register_fake_lsp(
17171 "Rust",
17172 FakeLspAdapter {
17173 name: language_server_name,
17174 initialization_options: Some(json!({
17175 "testOptionValue": true
17176 })),
17177 initializer: Some(Box::new(move |fake_server| {
17178 let task_restarts = Arc::clone(&closure_restarts);
17179 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
17180 task_restarts.fetch_add(1, atomic::Ordering::Release);
17181 futures::future::ready(Ok(()))
17182 });
17183 })),
17184 ..Default::default()
17185 },
17186 );
17187
17188 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17189 let _buffer = project
17190 .update(cx, |project, cx| {
17191 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
17192 })
17193 .await
17194 .unwrap();
17195 let _fake_server = fake_servers.next().await.unwrap();
17196 update_test_language_settings(cx, |language_settings| {
17197 language_settings.languages.0.insert(
17198 language_name.clone().0,
17199 LanguageSettingsContent {
17200 tab_size: NonZeroU32::new(8),
17201 ..Default::default()
17202 },
17203 );
17204 });
17205 cx.executor().run_until_parked();
17206 assert_eq!(
17207 server_restarts.load(atomic::Ordering::Acquire),
17208 0,
17209 "Should not restart LSP server on an unrelated change"
17210 );
17211
17212 update_test_project_settings(cx, |project_settings| {
17213 project_settings.lsp.insert(
17214 "Some other server name".into(),
17215 LspSettings {
17216 binary: None,
17217 settings: None,
17218 initialization_options: Some(json!({
17219 "some other init value": false
17220 })),
17221 enable_lsp_tasks: false,
17222 fetch: None,
17223 },
17224 );
17225 });
17226 cx.executor().run_until_parked();
17227 assert_eq!(
17228 server_restarts.load(atomic::Ordering::Acquire),
17229 0,
17230 "Should not restart LSP server on an unrelated LSP settings change"
17231 );
17232
17233 update_test_project_settings(cx, |project_settings| {
17234 project_settings.lsp.insert(
17235 language_server_name.into(),
17236 LspSettings {
17237 binary: None,
17238 settings: None,
17239 initialization_options: Some(json!({
17240 "anotherInitValue": false
17241 })),
17242 enable_lsp_tasks: false,
17243 fetch: None,
17244 },
17245 );
17246 });
17247 cx.executor().run_until_parked();
17248 assert_eq!(
17249 server_restarts.load(atomic::Ordering::Acquire),
17250 1,
17251 "Should restart LSP server on a related LSP settings change"
17252 );
17253
17254 update_test_project_settings(cx, |project_settings| {
17255 project_settings.lsp.insert(
17256 language_server_name.into(),
17257 LspSettings {
17258 binary: None,
17259 settings: None,
17260 initialization_options: Some(json!({
17261 "anotherInitValue": false
17262 })),
17263 enable_lsp_tasks: false,
17264 fetch: None,
17265 },
17266 );
17267 });
17268 cx.executor().run_until_parked();
17269 assert_eq!(
17270 server_restarts.load(atomic::Ordering::Acquire),
17271 1,
17272 "Should not restart LSP server on a related LSP settings change that is the same"
17273 );
17274
17275 update_test_project_settings(cx, |project_settings| {
17276 project_settings.lsp.insert(
17277 language_server_name.into(),
17278 LspSettings {
17279 binary: None,
17280 settings: None,
17281 initialization_options: None,
17282 enable_lsp_tasks: false,
17283 fetch: None,
17284 },
17285 );
17286 });
17287 cx.executor().run_until_parked();
17288 assert_eq!(
17289 server_restarts.load(atomic::Ordering::Acquire),
17290 2,
17291 "Should restart LSP server on another related LSP settings change"
17292 );
17293}
17294
17295#[gpui::test]
17296async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
17297 init_test(cx, |_| {});
17298
17299 let mut cx = EditorLspTestContext::new_rust(
17300 lsp::ServerCapabilities {
17301 completion_provider: Some(lsp::CompletionOptions {
17302 trigger_characters: Some(vec![".".to_string()]),
17303 resolve_provider: Some(true),
17304 ..Default::default()
17305 }),
17306 ..Default::default()
17307 },
17308 cx,
17309 )
17310 .await;
17311
17312 cx.set_state("fn main() { let a = 2ˇ; }");
17313 cx.simulate_keystroke(".");
17314 let completion_item = lsp::CompletionItem {
17315 label: "some".into(),
17316 kind: Some(lsp::CompletionItemKind::SNIPPET),
17317 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
17318 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
17319 kind: lsp::MarkupKind::Markdown,
17320 value: "```rust\nSome(2)\n```".to_string(),
17321 })),
17322 deprecated: Some(false),
17323 sort_text: Some("fffffff2".to_string()),
17324 filter_text: Some("some".to_string()),
17325 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
17326 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17327 range: lsp::Range {
17328 start: lsp::Position {
17329 line: 0,
17330 character: 22,
17331 },
17332 end: lsp::Position {
17333 line: 0,
17334 character: 22,
17335 },
17336 },
17337 new_text: "Some(2)".to_string(),
17338 })),
17339 additional_text_edits: Some(vec![lsp::TextEdit {
17340 range: lsp::Range {
17341 start: lsp::Position {
17342 line: 0,
17343 character: 20,
17344 },
17345 end: lsp::Position {
17346 line: 0,
17347 character: 22,
17348 },
17349 },
17350 new_text: "".to_string(),
17351 }]),
17352 ..Default::default()
17353 };
17354
17355 let closure_completion_item = completion_item.clone();
17356 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17357 let task_completion_item = closure_completion_item.clone();
17358 async move {
17359 Ok(Some(lsp::CompletionResponse::Array(vec![
17360 task_completion_item,
17361 ])))
17362 }
17363 });
17364
17365 request.next().await;
17366
17367 cx.condition(|editor, _| editor.context_menu_visible())
17368 .await;
17369 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
17370 editor
17371 .confirm_completion(&ConfirmCompletion::default(), window, cx)
17372 .unwrap()
17373 });
17374 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
17375
17376 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
17377 let task_completion_item = completion_item.clone();
17378 async move { Ok(task_completion_item) }
17379 })
17380 .next()
17381 .await
17382 .unwrap();
17383 apply_additional_edits.await.unwrap();
17384 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
17385}
17386
17387#[gpui::test]
17388async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
17389 init_test(cx, |_| {});
17390
17391 let mut cx = EditorLspTestContext::new_rust(
17392 lsp::ServerCapabilities {
17393 completion_provider: Some(lsp::CompletionOptions {
17394 trigger_characters: Some(vec![".".to_string()]),
17395 resolve_provider: Some(true),
17396 ..Default::default()
17397 }),
17398 ..Default::default()
17399 },
17400 cx,
17401 )
17402 .await;
17403
17404 cx.set_state("fn main() { let a = 2ˇ; }");
17405 cx.simulate_keystroke(".");
17406
17407 let item1 = lsp::CompletionItem {
17408 label: "method id()".to_string(),
17409 filter_text: Some("id".to_string()),
17410 detail: None,
17411 documentation: None,
17412 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17413 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17414 new_text: ".id".to_string(),
17415 })),
17416 ..lsp::CompletionItem::default()
17417 };
17418
17419 let item2 = lsp::CompletionItem {
17420 label: "other".to_string(),
17421 filter_text: Some("other".to_string()),
17422 detail: None,
17423 documentation: None,
17424 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17425 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17426 new_text: ".other".to_string(),
17427 })),
17428 ..lsp::CompletionItem::default()
17429 };
17430
17431 let item1 = item1.clone();
17432 cx.set_request_handler::<lsp::request::Completion, _, _>({
17433 let item1 = item1.clone();
17434 move |_, _, _| {
17435 let item1 = item1.clone();
17436 let item2 = item2.clone();
17437 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
17438 }
17439 })
17440 .next()
17441 .await;
17442
17443 cx.condition(|editor, _| editor.context_menu_visible())
17444 .await;
17445 cx.update_editor(|editor, _, _| {
17446 let context_menu = editor.context_menu.borrow_mut();
17447 let context_menu = context_menu
17448 .as_ref()
17449 .expect("Should have the context menu deployed");
17450 match context_menu {
17451 CodeContextMenu::Completions(completions_menu) => {
17452 let completions = completions_menu.completions.borrow_mut();
17453 assert_eq!(
17454 completions
17455 .iter()
17456 .map(|completion| &completion.label.text)
17457 .collect::<Vec<_>>(),
17458 vec!["method id()", "other"]
17459 )
17460 }
17461 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17462 }
17463 });
17464
17465 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
17466 let item1 = item1.clone();
17467 move |_, item_to_resolve, _| {
17468 let item1 = item1.clone();
17469 async move {
17470 if item1 == item_to_resolve {
17471 Ok(lsp::CompletionItem {
17472 label: "method id()".to_string(),
17473 filter_text: Some("id".to_string()),
17474 detail: Some("Now resolved!".to_string()),
17475 documentation: Some(lsp::Documentation::String("Docs".to_string())),
17476 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17477 range: lsp::Range::new(
17478 lsp::Position::new(0, 22),
17479 lsp::Position::new(0, 22),
17480 ),
17481 new_text: ".id".to_string(),
17482 })),
17483 ..lsp::CompletionItem::default()
17484 })
17485 } else {
17486 Ok(item_to_resolve)
17487 }
17488 }
17489 }
17490 })
17491 .next()
17492 .await
17493 .unwrap();
17494 cx.run_until_parked();
17495
17496 cx.update_editor(|editor, window, cx| {
17497 editor.context_menu_next(&Default::default(), window, cx);
17498 });
17499
17500 cx.update_editor(|editor, _, _| {
17501 let context_menu = editor.context_menu.borrow_mut();
17502 let context_menu = context_menu
17503 .as_ref()
17504 .expect("Should have the context menu deployed");
17505 match context_menu {
17506 CodeContextMenu::Completions(completions_menu) => {
17507 let completions = completions_menu.completions.borrow_mut();
17508 assert_eq!(
17509 completions
17510 .iter()
17511 .map(|completion| &completion.label.text)
17512 .collect::<Vec<_>>(),
17513 vec!["method id() Now resolved!", "other"],
17514 "Should update first completion label, but not second as the filter text did not match."
17515 );
17516 }
17517 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17518 }
17519 });
17520}
17521
17522#[gpui::test]
17523async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
17524 init_test(cx, |_| {});
17525 let mut cx = EditorLspTestContext::new_rust(
17526 lsp::ServerCapabilities {
17527 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
17528 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
17529 completion_provider: Some(lsp::CompletionOptions {
17530 resolve_provider: Some(true),
17531 ..Default::default()
17532 }),
17533 ..Default::default()
17534 },
17535 cx,
17536 )
17537 .await;
17538 cx.set_state(indoc! {"
17539 struct TestStruct {
17540 field: i32
17541 }
17542
17543 fn mainˇ() {
17544 let unused_var = 42;
17545 let test_struct = TestStruct { field: 42 };
17546 }
17547 "});
17548 let symbol_range = cx.lsp_range(indoc! {"
17549 struct TestStruct {
17550 field: i32
17551 }
17552
17553 «fn main»() {
17554 let unused_var = 42;
17555 let test_struct = TestStruct { field: 42 };
17556 }
17557 "});
17558 let mut hover_requests =
17559 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
17560 Ok(Some(lsp::Hover {
17561 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
17562 kind: lsp::MarkupKind::Markdown,
17563 value: "Function documentation".to_string(),
17564 }),
17565 range: Some(symbol_range),
17566 }))
17567 });
17568
17569 // Case 1: Test that code action menu hide hover popover
17570 cx.dispatch_action(Hover);
17571 hover_requests.next().await;
17572 cx.condition(|editor, _| editor.hover_state.visible()).await;
17573 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
17574 move |_, _, _| async move {
17575 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
17576 lsp::CodeAction {
17577 title: "Remove unused variable".to_string(),
17578 kind: Some(CodeActionKind::QUICKFIX),
17579 edit: Some(lsp::WorkspaceEdit {
17580 changes: Some(
17581 [(
17582 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
17583 vec![lsp::TextEdit {
17584 range: lsp::Range::new(
17585 lsp::Position::new(5, 4),
17586 lsp::Position::new(5, 27),
17587 ),
17588 new_text: "".to_string(),
17589 }],
17590 )]
17591 .into_iter()
17592 .collect(),
17593 ),
17594 ..Default::default()
17595 }),
17596 ..Default::default()
17597 },
17598 )]))
17599 },
17600 );
17601 cx.update_editor(|editor, window, cx| {
17602 editor.toggle_code_actions(
17603 &ToggleCodeActions {
17604 deployed_from: None,
17605 quick_launch: false,
17606 },
17607 window,
17608 cx,
17609 );
17610 });
17611 code_action_requests.next().await;
17612 cx.run_until_parked();
17613 cx.condition(|editor, _| editor.context_menu_visible())
17614 .await;
17615 cx.update_editor(|editor, _, _| {
17616 assert!(
17617 !editor.hover_state.visible(),
17618 "Hover popover should be hidden when code action menu is shown"
17619 );
17620 // Hide code actions
17621 editor.context_menu.take();
17622 });
17623
17624 // Case 2: Test that code completions hide hover popover
17625 cx.dispatch_action(Hover);
17626 hover_requests.next().await;
17627 cx.condition(|editor, _| editor.hover_state.visible()).await;
17628 let counter = Arc::new(AtomicUsize::new(0));
17629 let mut completion_requests =
17630 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17631 let counter = counter.clone();
17632 async move {
17633 counter.fetch_add(1, atomic::Ordering::Release);
17634 Ok(Some(lsp::CompletionResponse::Array(vec![
17635 lsp::CompletionItem {
17636 label: "main".into(),
17637 kind: Some(lsp::CompletionItemKind::FUNCTION),
17638 detail: Some("() -> ()".to_string()),
17639 ..Default::default()
17640 },
17641 lsp::CompletionItem {
17642 label: "TestStruct".into(),
17643 kind: Some(lsp::CompletionItemKind::STRUCT),
17644 detail: Some("struct TestStruct".to_string()),
17645 ..Default::default()
17646 },
17647 ])))
17648 }
17649 });
17650 cx.update_editor(|editor, window, cx| {
17651 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
17652 });
17653 completion_requests.next().await;
17654 cx.condition(|editor, _| editor.context_menu_visible())
17655 .await;
17656 cx.update_editor(|editor, _, _| {
17657 assert!(
17658 !editor.hover_state.visible(),
17659 "Hover popover should be hidden when completion menu is shown"
17660 );
17661 });
17662}
17663
17664#[gpui::test]
17665async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
17666 init_test(cx, |_| {});
17667
17668 let mut cx = EditorLspTestContext::new_rust(
17669 lsp::ServerCapabilities {
17670 completion_provider: Some(lsp::CompletionOptions {
17671 trigger_characters: Some(vec![".".to_string()]),
17672 resolve_provider: Some(true),
17673 ..Default::default()
17674 }),
17675 ..Default::default()
17676 },
17677 cx,
17678 )
17679 .await;
17680
17681 cx.set_state("fn main() { let a = 2ˇ; }");
17682 cx.simulate_keystroke(".");
17683
17684 let unresolved_item_1 = lsp::CompletionItem {
17685 label: "id".to_string(),
17686 filter_text: Some("id".to_string()),
17687 detail: None,
17688 documentation: None,
17689 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17690 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17691 new_text: ".id".to_string(),
17692 })),
17693 ..lsp::CompletionItem::default()
17694 };
17695 let resolved_item_1 = lsp::CompletionItem {
17696 additional_text_edits: Some(vec![lsp::TextEdit {
17697 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17698 new_text: "!!".to_string(),
17699 }]),
17700 ..unresolved_item_1.clone()
17701 };
17702 let unresolved_item_2 = lsp::CompletionItem {
17703 label: "other".to_string(),
17704 filter_text: Some("other".to_string()),
17705 detail: None,
17706 documentation: None,
17707 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17708 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17709 new_text: ".other".to_string(),
17710 })),
17711 ..lsp::CompletionItem::default()
17712 };
17713 let resolved_item_2 = lsp::CompletionItem {
17714 additional_text_edits: Some(vec![lsp::TextEdit {
17715 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17716 new_text: "??".to_string(),
17717 }]),
17718 ..unresolved_item_2.clone()
17719 };
17720
17721 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
17722 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
17723 cx.lsp
17724 .server
17725 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
17726 let unresolved_item_1 = unresolved_item_1.clone();
17727 let resolved_item_1 = resolved_item_1.clone();
17728 let unresolved_item_2 = unresolved_item_2.clone();
17729 let resolved_item_2 = resolved_item_2.clone();
17730 let resolve_requests_1 = resolve_requests_1.clone();
17731 let resolve_requests_2 = resolve_requests_2.clone();
17732 move |unresolved_request, _| {
17733 let unresolved_item_1 = unresolved_item_1.clone();
17734 let resolved_item_1 = resolved_item_1.clone();
17735 let unresolved_item_2 = unresolved_item_2.clone();
17736 let resolved_item_2 = resolved_item_2.clone();
17737 let resolve_requests_1 = resolve_requests_1.clone();
17738 let resolve_requests_2 = resolve_requests_2.clone();
17739 async move {
17740 if unresolved_request == unresolved_item_1 {
17741 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
17742 Ok(resolved_item_1.clone())
17743 } else if unresolved_request == unresolved_item_2 {
17744 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
17745 Ok(resolved_item_2.clone())
17746 } else {
17747 panic!("Unexpected completion item {unresolved_request:?}")
17748 }
17749 }
17750 }
17751 })
17752 .detach();
17753
17754 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17755 let unresolved_item_1 = unresolved_item_1.clone();
17756 let unresolved_item_2 = unresolved_item_2.clone();
17757 async move {
17758 Ok(Some(lsp::CompletionResponse::Array(vec![
17759 unresolved_item_1,
17760 unresolved_item_2,
17761 ])))
17762 }
17763 })
17764 .next()
17765 .await;
17766
17767 cx.condition(|editor, _| editor.context_menu_visible())
17768 .await;
17769 cx.update_editor(|editor, _, _| {
17770 let context_menu = editor.context_menu.borrow_mut();
17771 let context_menu = context_menu
17772 .as_ref()
17773 .expect("Should have the context menu deployed");
17774 match context_menu {
17775 CodeContextMenu::Completions(completions_menu) => {
17776 let completions = completions_menu.completions.borrow_mut();
17777 assert_eq!(
17778 completions
17779 .iter()
17780 .map(|completion| &completion.label.text)
17781 .collect::<Vec<_>>(),
17782 vec!["id", "other"]
17783 )
17784 }
17785 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17786 }
17787 });
17788 cx.run_until_parked();
17789
17790 cx.update_editor(|editor, window, cx| {
17791 editor.context_menu_next(&ContextMenuNext, window, cx);
17792 });
17793 cx.run_until_parked();
17794 cx.update_editor(|editor, window, cx| {
17795 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
17796 });
17797 cx.run_until_parked();
17798 cx.update_editor(|editor, window, cx| {
17799 editor.context_menu_next(&ContextMenuNext, window, cx);
17800 });
17801 cx.run_until_parked();
17802 cx.update_editor(|editor, window, cx| {
17803 editor
17804 .compose_completion(&ComposeCompletion::default(), window, cx)
17805 .expect("No task returned")
17806 })
17807 .await
17808 .expect("Completion failed");
17809 cx.run_until_parked();
17810
17811 cx.update_editor(|editor, _, cx| {
17812 assert_eq!(
17813 resolve_requests_1.load(atomic::Ordering::Acquire),
17814 1,
17815 "Should always resolve once despite multiple selections"
17816 );
17817 assert_eq!(
17818 resolve_requests_2.load(atomic::Ordering::Acquire),
17819 1,
17820 "Should always resolve once after multiple selections and applying the completion"
17821 );
17822 assert_eq!(
17823 editor.text(cx),
17824 "fn main() { let a = ??.other; }",
17825 "Should use resolved data when applying the completion"
17826 );
17827 });
17828}
17829
17830#[gpui::test]
17831async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
17832 init_test(cx, |_| {});
17833
17834 let item_0 = lsp::CompletionItem {
17835 label: "abs".into(),
17836 insert_text: Some("abs".into()),
17837 data: Some(json!({ "very": "special"})),
17838 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
17839 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
17840 lsp::InsertReplaceEdit {
17841 new_text: "abs".to_string(),
17842 insert: lsp::Range::default(),
17843 replace: lsp::Range::default(),
17844 },
17845 )),
17846 ..lsp::CompletionItem::default()
17847 };
17848 let items = iter::once(item_0.clone())
17849 .chain((11..51).map(|i| lsp::CompletionItem {
17850 label: format!("item_{}", i),
17851 insert_text: Some(format!("item_{}", i)),
17852 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
17853 ..lsp::CompletionItem::default()
17854 }))
17855 .collect::<Vec<_>>();
17856
17857 let default_commit_characters = vec!["?".to_string()];
17858 let default_data = json!({ "default": "data"});
17859 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
17860 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
17861 let default_edit_range = lsp::Range {
17862 start: lsp::Position {
17863 line: 0,
17864 character: 5,
17865 },
17866 end: lsp::Position {
17867 line: 0,
17868 character: 5,
17869 },
17870 };
17871
17872 let mut cx = EditorLspTestContext::new_rust(
17873 lsp::ServerCapabilities {
17874 completion_provider: Some(lsp::CompletionOptions {
17875 trigger_characters: Some(vec![".".to_string()]),
17876 resolve_provider: Some(true),
17877 ..Default::default()
17878 }),
17879 ..Default::default()
17880 },
17881 cx,
17882 )
17883 .await;
17884
17885 cx.set_state("fn main() { let a = 2ˇ; }");
17886 cx.simulate_keystroke(".");
17887
17888 let completion_data = default_data.clone();
17889 let completion_characters = default_commit_characters.clone();
17890 let completion_items = items.clone();
17891 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17892 let default_data = completion_data.clone();
17893 let default_commit_characters = completion_characters.clone();
17894 let items = completion_items.clone();
17895 async move {
17896 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
17897 items,
17898 item_defaults: Some(lsp::CompletionListItemDefaults {
17899 data: Some(default_data.clone()),
17900 commit_characters: Some(default_commit_characters.clone()),
17901 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
17902 default_edit_range,
17903 )),
17904 insert_text_format: Some(default_insert_text_format),
17905 insert_text_mode: Some(default_insert_text_mode),
17906 }),
17907 ..lsp::CompletionList::default()
17908 })))
17909 }
17910 })
17911 .next()
17912 .await;
17913
17914 let resolved_items = Arc::new(Mutex::new(Vec::new()));
17915 cx.lsp
17916 .server
17917 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
17918 let closure_resolved_items = resolved_items.clone();
17919 move |item_to_resolve, _| {
17920 let closure_resolved_items = closure_resolved_items.clone();
17921 async move {
17922 closure_resolved_items.lock().push(item_to_resolve.clone());
17923 Ok(item_to_resolve)
17924 }
17925 }
17926 })
17927 .detach();
17928
17929 cx.condition(|editor, _| editor.context_menu_visible())
17930 .await;
17931 cx.run_until_parked();
17932 cx.update_editor(|editor, _, _| {
17933 let menu = editor.context_menu.borrow_mut();
17934 match menu.as_ref().expect("should have the completions menu") {
17935 CodeContextMenu::Completions(completions_menu) => {
17936 assert_eq!(
17937 completions_menu
17938 .entries
17939 .borrow()
17940 .iter()
17941 .map(|mat| mat.string.clone())
17942 .collect::<Vec<String>>(),
17943 items
17944 .iter()
17945 .map(|completion| completion.label.clone())
17946 .collect::<Vec<String>>()
17947 );
17948 }
17949 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
17950 }
17951 });
17952 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
17953 // with 4 from the end.
17954 assert_eq!(
17955 *resolved_items.lock(),
17956 [&items[0..16], &items[items.len() - 4..items.len()]]
17957 .concat()
17958 .iter()
17959 .cloned()
17960 .map(|mut item| {
17961 if item.data.is_none() {
17962 item.data = Some(default_data.clone());
17963 }
17964 item
17965 })
17966 .collect::<Vec<lsp::CompletionItem>>(),
17967 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
17968 );
17969 resolved_items.lock().clear();
17970
17971 cx.update_editor(|editor, window, cx| {
17972 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
17973 });
17974 cx.run_until_parked();
17975 // Completions that have already been resolved are skipped.
17976 assert_eq!(
17977 *resolved_items.lock(),
17978 items[items.len() - 17..items.len() - 4]
17979 .iter()
17980 .cloned()
17981 .map(|mut item| {
17982 if item.data.is_none() {
17983 item.data = Some(default_data.clone());
17984 }
17985 item
17986 })
17987 .collect::<Vec<lsp::CompletionItem>>()
17988 );
17989 resolved_items.lock().clear();
17990}
17991
17992#[gpui::test]
17993async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
17994 init_test(cx, |_| {});
17995
17996 let mut cx = EditorLspTestContext::new(
17997 Language::new(
17998 LanguageConfig {
17999 matcher: LanguageMatcher {
18000 path_suffixes: vec!["jsx".into()],
18001 ..Default::default()
18002 },
18003 overrides: [(
18004 "element".into(),
18005 LanguageConfigOverride {
18006 completion_query_characters: Override::Set(['-'].into_iter().collect()),
18007 ..Default::default()
18008 },
18009 )]
18010 .into_iter()
18011 .collect(),
18012 ..Default::default()
18013 },
18014 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
18015 )
18016 .with_override_query("(jsx_self_closing_element) @element")
18017 .unwrap(),
18018 lsp::ServerCapabilities {
18019 completion_provider: Some(lsp::CompletionOptions {
18020 trigger_characters: Some(vec![":".to_string()]),
18021 ..Default::default()
18022 }),
18023 ..Default::default()
18024 },
18025 cx,
18026 )
18027 .await;
18028
18029 cx.lsp
18030 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
18031 Ok(Some(lsp::CompletionResponse::Array(vec![
18032 lsp::CompletionItem {
18033 label: "bg-blue".into(),
18034 ..Default::default()
18035 },
18036 lsp::CompletionItem {
18037 label: "bg-red".into(),
18038 ..Default::default()
18039 },
18040 lsp::CompletionItem {
18041 label: "bg-yellow".into(),
18042 ..Default::default()
18043 },
18044 ])))
18045 });
18046
18047 cx.set_state(r#"<p class="bgˇ" />"#);
18048
18049 // Trigger completion when typing a dash, because the dash is an extra
18050 // word character in the 'element' scope, which contains the cursor.
18051 cx.simulate_keystroke("-");
18052 cx.executor().run_until_parked();
18053 cx.update_editor(|editor, _, _| {
18054 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18055 {
18056 assert_eq!(
18057 completion_menu_entries(menu),
18058 &["bg-blue", "bg-red", "bg-yellow"]
18059 );
18060 } else {
18061 panic!("expected completion menu to be open");
18062 }
18063 });
18064
18065 cx.simulate_keystroke("l");
18066 cx.executor().run_until_parked();
18067 cx.update_editor(|editor, _, _| {
18068 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18069 {
18070 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
18071 } else {
18072 panic!("expected completion menu to be open");
18073 }
18074 });
18075
18076 // When filtering completions, consider the character after the '-' to
18077 // be the start of a subword.
18078 cx.set_state(r#"<p class="yelˇ" />"#);
18079 cx.simulate_keystroke("l");
18080 cx.executor().run_until_parked();
18081 cx.update_editor(|editor, _, _| {
18082 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18083 {
18084 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
18085 } else {
18086 panic!("expected completion menu to be open");
18087 }
18088 });
18089}
18090
18091fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
18092 let entries = menu.entries.borrow();
18093 entries.iter().map(|mat| mat.string.clone()).collect()
18094}
18095
18096#[gpui::test]
18097async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
18098 init_test(cx, |settings| {
18099 settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
18100 });
18101
18102 let fs = FakeFs::new(cx.executor());
18103 fs.insert_file(path!("/file.ts"), Default::default()).await;
18104
18105 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
18106 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18107
18108 language_registry.add(Arc::new(Language::new(
18109 LanguageConfig {
18110 name: "TypeScript".into(),
18111 matcher: LanguageMatcher {
18112 path_suffixes: vec!["ts".to_string()],
18113 ..Default::default()
18114 },
18115 ..Default::default()
18116 },
18117 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
18118 )));
18119 update_test_language_settings(cx, |settings| {
18120 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
18121 });
18122
18123 let test_plugin = "test_plugin";
18124 let _ = language_registry.register_fake_lsp(
18125 "TypeScript",
18126 FakeLspAdapter {
18127 prettier_plugins: vec![test_plugin],
18128 ..Default::default()
18129 },
18130 );
18131
18132 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
18133 let buffer = project
18134 .update(cx, |project, cx| {
18135 project.open_local_buffer(path!("/file.ts"), cx)
18136 })
18137 .await
18138 .unwrap();
18139
18140 let buffer_text = "one\ntwo\nthree\n";
18141 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
18142 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
18143 editor.update_in(cx, |editor, window, cx| {
18144 editor.set_text(buffer_text, window, cx)
18145 });
18146
18147 editor
18148 .update_in(cx, |editor, window, cx| {
18149 editor.perform_format(
18150 project.clone(),
18151 FormatTrigger::Manual,
18152 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18153 window,
18154 cx,
18155 )
18156 })
18157 .unwrap()
18158 .await;
18159 assert_eq!(
18160 editor.update(cx, |editor, cx| editor.text(cx)),
18161 buffer_text.to_string() + prettier_format_suffix,
18162 "Test prettier formatting was not applied to the original buffer text",
18163 );
18164
18165 update_test_language_settings(cx, |settings| {
18166 settings.defaults.formatter = Some(FormatterList::default())
18167 });
18168 let format = editor.update_in(cx, |editor, window, cx| {
18169 editor.perform_format(
18170 project.clone(),
18171 FormatTrigger::Manual,
18172 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18173 window,
18174 cx,
18175 )
18176 });
18177 format.await.unwrap();
18178 assert_eq!(
18179 editor.update(cx, |editor, cx| editor.text(cx)),
18180 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
18181 "Autoformatting (via test prettier) was not applied to the original buffer text",
18182 );
18183}
18184
18185#[gpui::test]
18186async fn test_addition_reverts(cx: &mut TestAppContext) {
18187 init_test(cx, |_| {});
18188 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18189 let base_text = indoc! {r#"
18190 struct Row;
18191 struct Row1;
18192 struct Row2;
18193
18194 struct Row4;
18195 struct Row5;
18196 struct Row6;
18197
18198 struct Row8;
18199 struct Row9;
18200 struct Row10;"#};
18201
18202 // When addition hunks are not adjacent to carets, no hunk revert is performed
18203 assert_hunk_revert(
18204 indoc! {r#"struct Row;
18205 struct Row1;
18206 struct Row1.1;
18207 struct Row1.2;
18208 struct Row2;ˇ
18209
18210 struct Row4;
18211 struct Row5;
18212 struct Row6;
18213
18214 struct Row8;
18215 ˇstruct Row9;
18216 struct Row9.1;
18217 struct Row9.2;
18218 struct Row9.3;
18219 struct Row10;"#},
18220 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18221 indoc! {r#"struct Row;
18222 struct Row1;
18223 struct Row1.1;
18224 struct Row1.2;
18225 struct Row2;ˇ
18226
18227 struct Row4;
18228 struct Row5;
18229 struct Row6;
18230
18231 struct Row8;
18232 ˇstruct Row9;
18233 struct Row9.1;
18234 struct Row9.2;
18235 struct Row9.3;
18236 struct Row10;"#},
18237 base_text,
18238 &mut cx,
18239 );
18240 // Same for selections
18241 assert_hunk_revert(
18242 indoc! {r#"struct Row;
18243 struct Row1;
18244 struct Row2;
18245 struct Row2.1;
18246 struct Row2.2;
18247 «ˇ
18248 struct Row4;
18249 struct» Row5;
18250 «struct Row6;
18251 ˇ»
18252 struct Row9.1;
18253 struct Row9.2;
18254 struct Row9.3;
18255 struct Row8;
18256 struct Row9;
18257 struct Row10;"#},
18258 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18259 indoc! {r#"struct Row;
18260 struct Row1;
18261 struct Row2;
18262 struct Row2.1;
18263 struct Row2.2;
18264 «ˇ
18265 struct Row4;
18266 struct» Row5;
18267 «struct Row6;
18268 ˇ»
18269 struct Row9.1;
18270 struct Row9.2;
18271 struct Row9.3;
18272 struct Row8;
18273 struct Row9;
18274 struct Row10;"#},
18275 base_text,
18276 &mut cx,
18277 );
18278
18279 // When carets and selections intersect the addition hunks, those are reverted.
18280 // Adjacent carets got merged.
18281 assert_hunk_revert(
18282 indoc! {r#"struct Row;
18283 ˇ// something on the top
18284 struct Row1;
18285 struct Row2;
18286 struct Roˇw3.1;
18287 struct Row2.2;
18288 struct Row2.3;ˇ
18289
18290 struct Row4;
18291 struct ˇRow5.1;
18292 struct Row5.2;
18293 struct «Rowˇ»5.3;
18294 struct Row5;
18295 struct Row6;
18296 ˇ
18297 struct Row9.1;
18298 struct «Rowˇ»9.2;
18299 struct «ˇRow»9.3;
18300 struct Row8;
18301 struct Row9;
18302 «ˇ// something on bottom»
18303 struct Row10;"#},
18304 vec![
18305 DiffHunkStatusKind::Added,
18306 DiffHunkStatusKind::Added,
18307 DiffHunkStatusKind::Added,
18308 DiffHunkStatusKind::Added,
18309 DiffHunkStatusKind::Added,
18310 ],
18311 indoc! {r#"struct Row;
18312 ˇstruct Row1;
18313 struct Row2;
18314 ˇ
18315 struct Row4;
18316 ˇstruct Row5;
18317 struct Row6;
18318 ˇ
18319 ˇstruct Row8;
18320 struct Row9;
18321 ˇstruct Row10;"#},
18322 base_text,
18323 &mut cx,
18324 );
18325}
18326
18327#[gpui::test]
18328async fn test_modification_reverts(cx: &mut TestAppContext) {
18329 init_test(cx, |_| {});
18330 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18331 let base_text = indoc! {r#"
18332 struct Row;
18333 struct Row1;
18334 struct Row2;
18335
18336 struct Row4;
18337 struct Row5;
18338 struct Row6;
18339
18340 struct Row8;
18341 struct Row9;
18342 struct Row10;"#};
18343
18344 // Modification hunks behave the same as the addition ones.
18345 assert_hunk_revert(
18346 indoc! {r#"struct Row;
18347 struct Row1;
18348 struct Row33;
18349 ˇ
18350 struct Row4;
18351 struct Row5;
18352 struct Row6;
18353 ˇ
18354 struct Row99;
18355 struct Row9;
18356 struct Row10;"#},
18357 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18358 indoc! {r#"struct Row;
18359 struct Row1;
18360 struct Row33;
18361 ˇ
18362 struct Row4;
18363 struct Row5;
18364 struct Row6;
18365 ˇ
18366 struct Row99;
18367 struct Row9;
18368 struct Row10;"#},
18369 base_text,
18370 &mut cx,
18371 );
18372 assert_hunk_revert(
18373 indoc! {r#"struct Row;
18374 struct Row1;
18375 struct Row33;
18376 «ˇ
18377 struct Row4;
18378 struct» Row5;
18379 «struct Row6;
18380 ˇ»
18381 struct Row99;
18382 struct Row9;
18383 struct Row10;"#},
18384 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18385 indoc! {r#"struct Row;
18386 struct Row1;
18387 struct Row33;
18388 «ˇ
18389 struct Row4;
18390 struct» Row5;
18391 «struct Row6;
18392 ˇ»
18393 struct Row99;
18394 struct Row9;
18395 struct Row10;"#},
18396 base_text,
18397 &mut cx,
18398 );
18399
18400 assert_hunk_revert(
18401 indoc! {r#"ˇstruct Row1.1;
18402 struct Row1;
18403 «ˇstr»uct Row22;
18404
18405 struct ˇRow44;
18406 struct Row5;
18407 struct «Rˇ»ow66;ˇ
18408
18409 «struˇ»ct Row88;
18410 struct Row9;
18411 struct Row1011;ˇ"#},
18412 vec![
18413 DiffHunkStatusKind::Modified,
18414 DiffHunkStatusKind::Modified,
18415 DiffHunkStatusKind::Modified,
18416 DiffHunkStatusKind::Modified,
18417 DiffHunkStatusKind::Modified,
18418 DiffHunkStatusKind::Modified,
18419 ],
18420 indoc! {r#"struct Row;
18421 ˇstruct Row1;
18422 struct Row2;
18423 ˇ
18424 struct Row4;
18425 ˇstruct Row5;
18426 struct Row6;
18427 ˇ
18428 struct Row8;
18429 ˇstruct Row9;
18430 struct Row10;ˇ"#},
18431 base_text,
18432 &mut cx,
18433 );
18434}
18435
18436#[gpui::test]
18437async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
18438 init_test(cx, |_| {});
18439 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18440 let base_text = indoc! {r#"
18441 one
18442
18443 two
18444 three
18445 "#};
18446
18447 cx.set_head_text(base_text);
18448 cx.set_state("\nˇ\n");
18449 cx.executor().run_until_parked();
18450 cx.update_editor(|editor, _window, cx| {
18451 editor.expand_selected_diff_hunks(cx);
18452 });
18453 cx.executor().run_until_parked();
18454 cx.update_editor(|editor, window, cx| {
18455 editor.backspace(&Default::default(), window, cx);
18456 });
18457 cx.run_until_parked();
18458 cx.assert_state_with_diff(
18459 indoc! {r#"
18460
18461 - two
18462 - threeˇ
18463 +
18464 "#}
18465 .to_string(),
18466 );
18467}
18468
18469#[gpui::test]
18470async fn test_deletion_reverts(cx: &mut TestAppContext) {
18471 init_test(cx, |_| {});
18472 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18473 let base_text = indoc! {r#"struct Row;
18474struct Row1;
18475struct Row2;
18476
18477struct Row4;
18478struct Row5;
18479struct Row6;
18480
18481struct Row8;
18482struct Row9;
18483struct Row10;"#};
18484
18485 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
18486 assert_hunk_revert(
18487 indoc! {r#"struct Row;
18488 struct Row2;
18489
18490 ˇstruct Row4;
18491 struct Row5;
18492 struct Row6;
18493 ˇ
18494 struct Row8;
18495 struct Row10;"#},
18496 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18497 indoc! {r#"struct Row;
18498 struct Row2;
18499
18500 ˇstruct Row4;
18501 struct Row5;
18502 struct Row6;
18503 ˇ
18504 struct Row8;
18505 struct Row10;"#},
18506 base_text,
18507 &mut cx,
18508 );
18509 assert_hunk_revert(
18510 indoc! {r#"struct Row;
18511 struct Row2;
18512
18513 «ˇstruct Row4;
18514 struct» Row5;
18515 «struct Row6;
18516 ˇ»
18517 struct Row8;
18518 struct Row10;"#},
18519 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18520 indoc! {r#"struct Row;
18521 struct Row2;
18522
18523 «ˇstruct Row4;
18524 struct» Row5;
18525 «struct Row6;
18526 ˇ»
18527 struct Row8;
18528 struct Row10;"#},
18529 base_text,
18530 &mut cx,
18531 );
18532
18533 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
18534 assert_hunk_revert(
18535 indoc! {r#"struct Row;
18536 ˇstruct Row2;
18537
18538 struct Row4;
18539 struct Row5;
18540 struct Row6;
18541
18542 struct Row8;ˇ
18543 struct Row10;"#},
18544 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18545 indoc! {r#"struct Row;
18546 struct Row1;
18547 ˇstruct Row2;
18548
18549 struct Row4;
18550 struct Row5;
18551 struct Row6;
18552
18553 struct Row8;ˇ
18554 struct Row9;
18555 struct Row10;"#},
18556 base_text,
18557 &mut cx,
18558 );
18559 assert_hunk_revert(
18560 indoc! {r#"struct Row;
18561 struct Row2«ˇ;
18562 struct Row4;
18563 struct» Row5;
18564 «struct Row6;
18565
18566 struct Row8;ˇ»
18567 struct Row10;"#},
18568 vec![
18569 DiffHunkStatusKind::Deleted,
18570 DiffHunkStatusKind::Deleted,
18571 DiffHunkStatusKind::Deleted,
18572 ],
18573 indoc! {r#"struct Row;
18574 struct Row1;
18575 struct Row2«ˇ;
18576
18577 struct Row4;
18578 struct» Row5;
18579 «struct Row6;
18580
18581 struct Row8;ˇ»
18582 struct Row9;
18583 struct Row10;"#},
18584 base_text,
18585 &mut cx,
18586 );
18587}
18588
18589#[gpui::test]
18590async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
18591 init_test(cx, |_| {});
18592
18593 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
18594 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
18595 let base_text_3 =
18596 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
18597
18598 let text_1 = edit_first_char_of_every_line(base_text_1);
18599 let text_2 = edit_first_char_of_every_line(base_text_2);
18600 let text_3 = edit_first_char_of_every_line(base_text_3);
18601
18602 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
18603 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
18604 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
18605
18606 let multibuffer = cx.new(|cx| {
18607 let mut multibuffer = MultiBuffer::new(ReadWrite);
18608 multibuffer.push_excerpts(
18609 buffer_1.clone(),
18610 [
18611 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18612 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18613 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18614 ],
18615 cx,
18616 );
18617 multibuffer.push_excerpts(
18618 buffer_2.clone(),
18619 [
18620 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18621 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18622 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18623 ],
18624 cx,
18625 );
18626 multibuffer.push_excerpts(
18627 buffer_3.clone(),
18628 [
18629 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18630 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18631 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18632 ],
18633 cx,
18634 );
18635 multibuffer
18636 });
18637
18638 let fs = FakeFs::new(cx.executor());
18639 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
18640 let (editor, cx) = cx
18641 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
18642 editor.update_in(cx, |editor, _window, cx| {
18643 for (buffer, diff_base) in [
18644 (buffer_1.clone(), base_text_1),
18645 (buffer_2.clone(), base_text_2),
18646 (buffer_3.clone(), base_text_3),
18647 ] {
18648 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
18649 editor
18650 .buffer
18651 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
18652 }
18653 });
18654 cx.executor().run_until_parked();
18655
18656 editor.update_in(cx, |editor, window, cx| {
18657 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}");
18658 editor.select_all(&SelectAll, window, cx);
18659 editor.git_restore(&Default::default(), window, cx);
18660 });
18661 cx.executor().run_until_parked();
18662
18663 // When all ranges are selected, all buffer hunks are reverted.
18664 editor.update(cx, |editor, cx| {
18665 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");
18666 });
18667 buffer_1.update(cx, |buffer, _| {
18668 assert_eq!(buffer.text(), base_text_1);
18669 });
18670 buffer_2.update(cx, |buffer, _| {
18671 assert_eq!(buffer.text(), base_text_2);
18672 });
18673 buffer_3.update(cx, |buffer, _| {
18674 assert_eq!(buffer.text(), base_text_3);
18675 });
18676
18677 editor.update_in(cx, |editor, window, cx| {
18678 editor.undo(&Default::default(), window, cx);
18679 });
18680
18681 editor.update_in(cx, |editor, window, cx| {
18682 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18683 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
18684 });
18685 editor.git_restore(&Default::default(), window, cx);
18686 });
18687
18688 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
18689 // but not affect buffer_2 and its related excerpts.
18690 editor.update(cx, |editor, cx| {
18691 assert_eq!(
18692 editor.text(cx),
18693 "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}"
18694 );
18695 });
18696 buffer_1.update(cx, |buffer, _| {
18697 assert_eq!(buffer.text(), base_text_1);
18698 });
18699 buffer_2.update(cx, |buffer, _| {
18700 assert_eq!(
18701 buffer.text(),
18702 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
18703 );
18704 });
18705 buffer_3.update(cx, |buffer, _| {
18706 assert_eq!(
18707 buffer.text(),
18708 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
18709 );
18710 });
18711
18712 fn edit_first_char_of_every_line(text: &str) -> String {
18713 text.split('\n')
18714 .map(|line| format!("X{}", &line[1..]))
18715 .collect::<Vec<_>>()
18716 .join("\n")
18717 }
18718}
18719
18720#[gpui::test]
18721async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
18722 init_test(cx, |_| {});
18723
18724 let cols = 4;
18725 let rows = 10;
18726 let sample_text_1 = sample_text(rows, cols, 'a');
18727 assert_eq!(
18728 sample_text_1,
18729 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
18730 );
18731 let sample_text_2 = sample_text(rows, cols, 'l');
18732 assert_eq!(
18733 sample_text_2,
18734 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
18735 );
18736 let sample_text_3 = sample_text(rows, cols, 'v');
18737 assert_eq!(
18738 sample_text_3,
18739 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
18740 );
18741
18742 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
18743 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
18744 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
18745
18746 let multi_buffer = cx.new(|cx| {
18747 let mut multibuffer = MultiBuffer::new(ReadWrite);
18748 multibuffer.push_excerpts(
18749 buffer_1.clone(),
18750 [
18751 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18752 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18753 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18754 ],
18755 cx,
18756 );
18757 multibuffer.push_excerpts(
18758 buffer_2.clone(),
18759 [
18760 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18761 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18762 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18763 ],
18764 cx,
18765 );
18766 multibuffer.push_excerpts(
18767 buffer_3.clone(),
18768 [
18769 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18770 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18771 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18772 ],
18773 cx,
18774 );
18775 multibuffer
18776 });
18777
18778 let fs = FakeFs::new(cx.executor());
18779 fs.insert_tree(
18780 "/a",
18781 json!({
18782 "main.rs": sample_text_1,
18783 "other.rs": sample_text_2,
18784 "lib.rs": sample_text_3,
18785 }),
18786 )
18787 .await;
18788 let project = Project::test(fs, ["/a".as_ref()], cx).await;
18789 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18790 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18791 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18792 Editor::new(
18793 EditorMode::full(),
18794 multi_buffer,
18795 Some(project.clone()),
18796 window,
18797 cx,
18798 )
18799 });
18800 let multibuffer_item_id = workspace
18801 .update(cx, |workspace, window, cx| {
18802 assert!(
18803 workspace.active_item(cx).is_none(),
18804 "active item should be None before the first item is added"
18805 );
18806 workspace.add_item_to_active_pane(
18807 Box::new(multi_buffer_editor.clone()),
18808 None,
18809 true,
18810 window,
18811 cx,
18812 );
18813 let active_item = workspace
18814 .active_item(cx)
18815 .expect("should have an active item after adding the multi buffer");
18816 assert_eq!(
18817 active_item.buffer_kind(cx),
18818 ItemBufferKind::Multibuffer,
18819 "A multi buffer was expected to active after adding"
18820 );
18821 active_item.item_id()
18822 })
18823 .unwrap();
18824 cx.executor().run_until_parked();
18825
18826 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18827 editor.change_selections(
18828 SelectionEffects::scroll(Autoscroll::Next),
18829 window,
18830 cx,
18831 |s| s.select_ranges(Some(1..2)),
18832 );
18833 editor.open_excerpts(&OpenExcerpts, window, cx);
18834 });
18835 cx.executor().run_until_parked();
18836 let first_item_id = workspace
18837 .update(cx, |workspace, window, cx| {
18838 let active_item = workspace
18839 .active_item(cx)
18840 .expect("should have an active item after navigating into the 1st buffer");
18841 let first_item_id = active_item.item_id();
18842 assert_ne!(
18843 first_item_id, multibuffer_item_id,
18844 "Should navigate into the 1st buffer and activate it"
18845 );
18846 assert_eq!(
18847 active_item.buffer_kind(cx),
18848 ItemBufferKind::Singleton,
18849 "New active item should be a singleton buffer"
18850 );
18851 assert_eq!(
18852 active_item
18853 .act_as::<Editor>(cx)
18854 .expect("should have navigated into an editor for the 1st buffer")
18855 .read(cx)
18856 .text(cx),
18857 sample_text_1
18858 );
18859
18860 workspace
18861 .go_back(workspace.active_pane().downgrade(), window, cx)
18862 .detach_and_log_err(cx);
18863
18864 first_item_id
18865 })
18866 .unwrap();
18867 cx.executor().run_until_parked();
18868 workspace
18869 .update(cx, |workspace, _, cx| {
18870 let active_item = workspace
18871 .active_item(cx)
18872 .expect("should have an active item after navigating back");
18873 assert_eq!(
18874 active_item.item_id(),
18875 multibuffer_item_id,
18876 "Should navigate back to the multi buffer"
18877 );
18878 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
18879 })
18880 .unwrap();
18881
18882 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18883 editor.change_selections(
18884 SelectionEffects::scroll(Autoscroll::Next),
18885 window,
18886 cx,
18887 |s| s.select_ranges(Some(39..40)),
18888 );
18889 editor.open_excerpts(&OpenExcerpts, window, cx);
18890 });
18891 cx.executor().run_until_parked();
18892 let second_item_id = workspace
18893 .update(cx, |workspace, window, cx| {
18894 let active_item = workspace
18895 .active_item(cx)
18896 .expect("should have an active item after navigating into the 2nd buffer");
18897 let second_item_id = active_item.item_id();
18898 assert_ne!(
18899 second_item_id, multibuffer_item_id,
18900 "Should navigate away from the multibuffer"
18901 );
18902 assert_ne!(
18903 second_item_id, first_item_id,
18904 "Should navigate into the 2nd buffer and activate it"
18905 );
18906 assert_eq!(
18907 active_item.buffer_kind(cx),
18908 ItemBufferKind::Singleton,
18909 "New active item should be a singleton buffer"
18910 );
18911 assert_eq!(
18912 active_item
18913 .act_as::<Editor>(cx)
18914 .expect("should have navigated into an editor")
18915 .read(cx)
18916 .text(cx),
18917 sample_text_2
18918 );
18919
18920 workspace
18921 .go_back(workspace.active_pane().downgrade(), window, cx)
18922 .detach_and_log_err(cx);
18923
18924 second_item_id
18925 })
18926 .unwrap();
18927 cx.executor().run_until_parked();
18928 workspace
18929 .update(cx, |workspace, _, cx| {
18930 let active_item = workspace
18931 .active_item(cx)
18932 .expect("should have an active item after navigating back from the 2nd buffer");
18933 assert_eq!(
18934 active_item.item_id(),
18935 multibuffer_item_id,
18936 "Should navigate back from the 2nd buffer to the multi buffer"
18937 );
18938 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
18939 })
18940 .unwrap();
18941
18942 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18943 editor.change_selections(
18944 SelectionEffects::scroll(Autoscroll::Next),
18945 window,
18946 cx,
18947 |s| s.select_ranges(Some(70..70)),
18948 );
18949 editor.open_excerpts(&OpenExcerpts, window, cx);
18950 });
18951 cx.executor().run_until_parked();
18952 workspace
18953 .update(cx, |workspace, window, cx| {
18954 let active_item = workspace
18955 .active_item(cx)
18956 .expect("should have an active item after navigating into the 3rd buffer");
18957 let third_item_id = active_item.item_id();
18958 assert_ne!(
18959 third_item_id, multibuffer_item_id,
18960 "Should navigate into the 3rd buffer and activate it"
18961 );
18962 assert_ne!(third_item_id, first_item_id);
18963 assert_ne!(third_item_id, second_item_id);
18964 assert_eq!(
18965 active_item.buffer_kind(cx),
18966 ItemBufferKind::Singleton,
18967 "New active item should be a singleton buffer"
18968 );
18969 assert_eq!(
18970 active_item
18971 .act_as::<Editor>(cx)
18972 .expect("should have navigated into an editor")
18973 .read(cx)
18974 .text(cx),
18975 sample_text_3
18976 );
18977
18978 workspace
18979 .go_back(workspace.active_pane().downgrade(), window, cx)
18980 .detach_and_log_err(cx);
18981 })
18982 .unwrap();
18983 cx.executor().run_until_parked();
18984 workspace
18985 .update(cx, |workspace, _, cx| {
18986 let active_item = workspace
18987 .active_item(cx)
18988 .expect("should have an active item after navigating back from the 3rd buffer");
18989 assert_eq!(
18990 active_item.item_id(),
18991 multibuffer_item_id,
18992 "Should navigate back from the 3rd buffer to the multi buffer"
18993 );
18994 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
18995 })
18996 .unwrap();
18997}
18998
18999#[gpui::test]
19000async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19001 init_test(cx, |_| {});
19002
19003 let mut cx = EditorTestContext::new(cx).await;
19004
19005 let diff_base = r#"
19006 use some::mod;
19007
19008 const A: u32 = 42;
19009
19010 fn main() {
19011 println!("hello");
19012
19013 println!("world");
19014 }
19015 "#
19016 .unindent();
19017
19018 cx.set_state(
19019 &r#"
19020 use some::modified;
19021
19022 ˇ
19023 fn main() {
19024 println!("hello there");
19025
19026 println!("around the");
19027 println!("world");
19028 }
19029 "#
19030 .unindent(),
19031 );
19032
19033 cx.set_head_text(&diff_base);
19034 executor.run_until_parked();
19035
19036 cx.update_editor(|editor, window, cx| {
19037 editor.go_to_next_hunk(&GoToHunk, window, cx);
19038 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19039 });
19040 executor.run_until_parked();
19041 cx.assert_state_with_diff(
19042 r#"
19043 use some::modified;
19044
19045
19046 fn main() {
19047 - println!("hello");
19048 + ˇ println!("hello there");
19049
19050 println!("around the");
19051 println!("world");
19052 }
19053 "#
19054 .unindent(),
19055 );
19056
19057 cx.update_editor(|editor, window, cx| {
19058 for _ in 0..2 {
19059 editor.go_to_next_hunk(&GoToHunk, window, cx);
19060 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19061 }
19062 });
19063 executor.run_until_parked();
19064 cx.assert_state_with_diff(
19065 r#"
19066 - use some::mod;
19067 + ˇuse some::modified;
19068
19069
19070 fn main() {
19071 - println!("hello");
19072 + println!("hello there");
19073
19074 + println!("around the");
19075 println!("world");
19076 }
19077 "#
19078 .unindent(),
19079 );
19080
19081 cx.update_editor(|editor, window, cx| {
19082 editor.go_to_next_hunk(&GoToHunk, window, cx);
19083 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19084 });
19085 executor.run_until_parked();
19086 cx.assert_state_with_diff(
19087 r#"
19088 - use some::mod;
19089 + use some::modified;
19090
19091 - const A: u32 = 42;
19092 ˇ
19093 fn main() {
19094 - println!("hello");
19095 + println!("hello there");
19096
19097 + println!("around the");
19098 println!("world");
19099 }
19100 "#
19101 .unindent(),
19102 );
19103
19104 cx.update_editor(|editor, window, cx| {
19105 editor.cancel(&Cancel, window, cx);
19106 });
19107
19108 cx.assert_state_with_diff(
19109 r#"
19110 use some::modified;
19111
19112 ˇ
19113 fn main() {
19114 println!("hello there");
19115
19116 println!("around the");
19117 println!("world");
19118 }
19119 "#
19120 .unindent(),
19121 );
19122}
19123
19124#[gpui::test]
19125async fn test_diff_base_change_with_expanded_diff_hunks(
19126 executor: BackgroundExecutor,
19127 cx: &mut TestAppContext,
19128) {
19129 init_test(cx, |_| {});
19130
19131 let mut cx = EditorTestContext::new(cx).await;
19132
19133 let diff_base = r#"
19134 use some::mod1;
19135 use some::mod2;
19136
19137 const A: u32 = 42;
19138 const B: u32 = 42;
19139 const C: u32 = 42;
19140
19141 fn main() {
19142 println!("hello");
19143
19144 println!("world");
19145 }
19146 "#
19147 .unindent();
19148
19149 cx.set_state(
19150 &r#"
19151 use some::mod2;
19152
19153 const A: u32 = 42;
19154 const C: u32 = 42;
19155
19156 fn main(ˇ) {
19157 //println!("hello");
19158
19159 println!("world");
19160 //
19161 //
19162 }
19163 "#
19164 .unindent(),
19165 );
19166
19167 cx.set_head_text(&diff_base);
19168 executor.run_until_parked();
19169
19170 cx.update_editor(|editor, window, cx| {
19171 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19172 });
19173 executor.run_until_parked();
19174 cx.assert_state_with_diff(
19175 r#"
19176 - use some::mod1;
19177 use some::mod2;
19178
19179 const A: u32 = 42;
19180 - const B: u32 = 42;
19181 const C: u32 = 42;
19182
19183 fn main(ˇ) {
19184 - println!("hello");
19185 + //println!("hello");
19186
19187 println!("world");
19188 + //
19189 + //
19190 }
19191 "#
19192 .unindent(),
19193 );
19194
19195 cx.set_head_text("new diff base!");
19196 executor.run_until_parked();
19197 cx.assert_state_with_diff(
19198 r#"
19199 - new diff base!
19200 + use some::mod2;
19201 +
19202 + const A: u32 = 42;
19203 + const C: u32 = 42;
19204 +
19205 + fn main(ˇ) {
19206 + //println!("hello");
19207 +
19208 + println!("world");
19209 + //
19210 + //
19211 + }
19212 "#
19213 .unindent(),
19214 );
19215}
19216
19217#[gpui::test]
19218async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
19219 init_test(cx, |_| {});
19220
19221 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19222 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19223 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19224 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19225 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
19226 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
19227
19228 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
19229 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
19230 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
19231
19232 let multi_buffer = cx.new(|cx| {
19233 let mut multibuffer = MultiBuffer::new(ReadWrite);
19234 multibuffer.push_excerpts(
19235 buffer_1.clone(),
19236 [
19237 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19238 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19239 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19240 ],
19241 cx,
19242 );
19243 multibuffer.push_excerpts(
19244 buffer_2.clone(),
19245 [
19246 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19247 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19248 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19249 ],
19250 cx,
19251 );
19252 multibuffer.push_excerpts(
19253 buffer_3.clone(),
19254 [
19255 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19256 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19257 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19258 ],
19259 cx,
19260 );
19261 multibuffer
19262 });
19263
19264 let editor =
19265 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19266 editor
19267 .update(cx, |editor, _window, cx| {
19268 for (buffer, diff_base) in [
19269 (buffer_1.clone(), file_1_old),
19270 (buffer_2.clone(), file_2_old),
19271 (buffer_3.clone(), file_3_old),
19272 ] {
19273 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
19274 editor
19275 .buffer
19276 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
19277 }
19278 })
19279 .unwrap();
19280
19281 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19282 cx.run_until_parked();
19283
19284 cx.assert_editor_state(
19285 &"
19286 ˇaaa
19287 ccc
19288 ddd
19289
19290 ggg
19291 hhh
19292
19293
19294 lll
19295 mmm
19296 NNN
19297
19298 qqq
19299 rrr
19300
19301 uuu
19302 111
19303 222
19304 333
19305
19306 666
19307 777
19308
19309 000
19310 !!!"
19311 .unindent(),
19312 );
19313
19314 cx.update_editor(|editor, window, cx| {
19315 editor.select_all(&SelectAll, window, cx);
19316 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19317 });
19318 cx.executor().run_until_parked();
19319
19320 cx.assert_state_with_diff(
19321 "
19322 «aaa
19323 - bbb
19324 ccc
19325 ddd
19326
19327 ggg
19328 hhh
19329
19330
19331 lll
19332 mmm
19333 - nnn
19334 + NNN
19335
19336 qqq
19337 rrr
19338
19339 uuu
19340 111
19341 222
19342 333
19343
19344 + 666
19345 777
19346
19347 000
19348 !!!ˇ»"
19349 .unindent(),
19350 );
19351}
19352
19353#[gpui::test]
19354async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
19355 init_test(cx, |_| {});
19356
19357 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
19358 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
19359
19360 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
19361 let multi_buffer = cx.new(|cx| {
19362 let mut multibuffer = MultiBuffer::new(ReadWrite);
19363 multibuffer.push_excerpts(
19364 buffer.clone(),
19365 [
19366 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
19367 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
19368 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
19369 ],
19370 cx,
19371 );
19372 multibuffer
19373 });
19374
19375 let editor =
19376 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19377 editor
19378 .update(cx, |editor, _window, cx| {
19379 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
19380 editor
19381 .buffer
19382 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
19383 })
19384 .unwrap();
19385
19386 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19387 cx.run_until_parked();
19388
19389 cx.update_editor(|editor, window, cx| {
19390 editor.expand_all_diff_hunks(&Default::default(), window, cx)
19391 });
19392 cx.executor().run_until_parked();
19393
19394 // When the start of a hunk coincides with the start of its excerpt,
19395 // the hunk is expanded. When the start of a hunk is earlier than
19396 // the start of its excerpt, the hunk is not expanded.
19397 cx.assert_state_with_diff(
19398 "
19399 ˇaaa
19400 - bbb
19401 + BBB
19402
19403 - ddd
19404 - eee
19405 + DDD
19406 + EEE
19407 fff
19408
19409 iii
19410 "
19411 .unindent(),
19412 );
19413}
19414
19415#[gpui::test]
19416async fn test_edits_around_expanded_insertion_hunks(
19417 executor: BackgroundExecutor,
19418 cx: &mut TestAppContext,
19419) {
19420 init_test(cx, |_| {});
19421
19422 let mut cx = EditorTestContext::new(cx).await;
19423
19424 let diff_base = r#"
19425 use some::mod1;
19426 use some::mod2;
19427
19428 const A: u32 = 42;
19429
19430 fn main() {
19431 println!("hello");
19432
19433 println!("world");
19434 }
19435 "#
19436 .unindent();
19437 executor.run_until_parked();
19438 cx.set_state(
19439 &r#"
19440 use some::mod1;
19441 use some::mod2;
19442
19443 const A: u32 = 42;
19444 const B: u32 = 42;
19445 const C: u32 = 42;
19446 ˇ
19447
19448 fn main() {
19449 println!("hello");
19450
19451 println!("world");
19452 }
19453 "#
19454 .unindent(),
19455 );
19456
19457 cx.set_head_text(&diff_base);
19458 executor.run_until_parked();
19459
19460 cx.update_editor(|editor, window, cx| {
19461 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19462 });
19463 executor.run_until_parked();
19464
19465 cx.assert_state_with_diff(
19466 r#"
19467 use some::mod1;
19468 use some::mod2;
19469
19470 const A: u32 = 42;
19471 + const B: u32 = 42;
19472 + const C: u32 = 42;
19473 + ˇ
19474
19475 fn main() {
19476 println!("hello");
19477
19478 println!("world");
19479 }
19480 "#
19481 .unindent(),
19482 );
19483
19484 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
19485 executor.run_until_parked();
19486
19487 cx.assert_state_with_diff(
19488 r#"
19489 use some::mod1;
19490 use some::mod2;
19491
19492 const A: u32 = 42;
19493 + const B: u32 = 42;
19494 + const C: u32 = 42;
19495 + const D: u32 = 42;
19496 + ˇ
19497
19498 fn main() {
19499 println!("hello");
19500
19501 println!("world");
19502 }
19503 "#
19504 .unindent(),
19505 );
19506
19507 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
19508 executor.run_until_parked();
19509
19510 cx.assert_state_with_diff(
19511 r#"
19512 use some::mod1;
19513 use some::mod2;
19514
19515 const A: u32 = 42;
19516 + const B: u32 = 42;
19517 + const C: u32 = 42;
19518 + const D: u32 = 42;
19519 + const E: u32 = 42;
19520 + ˇ
19521
19522 fn main() {
19523 println!("hello");
19524
19525 println!("world");
19526 }
19527 "#
19528 .unindent(),
19529 );
19530
19531 cx.update_editor(|editor, window, cx| {
19532 editor.delete_line(&DeleteLine, window, cx);
19533 });
19534 executor.run_until_parked();
19535
19536 cx.assert_state_with_diff(
19537 r#"
19538 use some::mod1;
19539 use some::mod2;
19540
19541 const A: u32 = 42;
19542 + const B: u32 = 42;
19543 + const C: u32 = 42;
19544 + const D: u32 = 42;
19545 + const E: u32 = 42;
19546 ˇ
19547 fn main() {
19548 println!("hello");
19549
19550 println!("world");
19551 }
19552 "#
19553 .unindent(),
19554 );
19555
19556 cx.update_editor(|editor, window, cx| {
19557 editor.move_up(&MoveUp, window, cx);
19558 editor.delete_line(&DeleteLine, window, cx);
19559 editor.move_up(&MoveUp, window, cx);
19560 editor.delete_line(&DeleteLine, window, cx);
19561 editor.move_up(&MoveUp, window, cx);
19562 editor.delete_line(&DeleteLine, window, cx);
19563 });
19564 executor.run_until_parked();
19565 cx.assert_state_with_diff(
19566 r#"
19567 use some::mod1;
19568 use some::mod2;
19569
19570 const A: u32 = 42;
19571 + const B: u32 = 42;
19572 ˇ
19573 fn main() {
19574 println!("hello");
19575
19576 println!("world");
19577 }
19578 "#
19579 .unindent(),
19580 );
19581
19582 cx.update_editor(|editor, window, cx| {
19583 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
19584 editor.delete_line(&DeleteLine, window, cx);
19585 });
19586 executor.run_until_parked();
19587 cx.assert_state_with_diff(
19588 r#"
19589 ˇ
19590 fn main() {
19591 println!("hello");
19592
19593 println!("world");
19594 }
19595 "#
19596 .unindent(),
19597 );
19598}
19599
19600#[gpui::test]
19601async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
19602 init_test(cx, |_| {});
19603
19604 let mut cx = EditorTestContext::new(cx).await;
19605 cx.set_head_text(indoc! { "
19606 one
19607 two
19608 three
19609 four
19610 five
19611 "
19612 });
19613 cx.set_state(indoc! { "
19614 one
19615 ˇthree
19616 five
19617 "});
19618 cx.run_until_parked();
19619 cx.update_editor(|editor, window, cx| {
19620 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19621 });
19622 cx.assert_state_with_diff(
19623 indoc! { "
19624 one
19625 - two
19626 ˇthree
19627 - four
19628 five
19629 "}
19630 .to_string(),
19631 );
19632 cx.update_editor(|editor, window, cx| {
19633 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19634 });
19635
19636 cx.assert_state_with_diff(
19637 indoc! { "
19638 one
19639 ˇthree
19640 five
19641 "}
19642 .to_string(),
19643 );
19644
19645 cx.set_state(indoc! { "
19646 one
19647 ˇTWO
19648 three
19649 four
19650 five
19651 "});
19652 cx.run_until_parked();
19653 cx.update_editor(|editor, window, cx| {
19654 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19655 });
19656
19657 cx.assert_state_with_diff(
19658 indoc! { "
19659 one
19660 - two
19661 + ˇTWO
19662 three
19663 four
19664 five
19665 "}
19666 .to_string(),
19667 );
19668 cx.update_editor(|editor, window, cx| {
19669 editor.move_up(&Default::default(), window, cx);
19670 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19671 });
19672 cx.assert_state_with_diff(
19673 indoc! { "
19674 one
19675 ˇTWO
19676 three
19677 four
19678 five
19679 "}
19680 .to_string(),
19681 );
19682}
19683
19684#[gpui::test]
19685async fn test_edits_around_expanded_deletion_hunks(
19686 executor: BackgroundExecutor,
19687 cx: &mut TestAppContext,
19688) {
19689 init_test(cx, |_| {});
19690
19691 let mut cx = EditorTestContext::new(cx).await;
19692
19693 let diff_base = r#"
19694 use some::mod1;
19695 use some::mod2;
19696
19697 const A: u32 = 42;
19698 const B: u32 = 42;
19699 const C: u32 = 42;
19700
19701
19702 fn main() {
19703 println!("hello");
19704
19705 println!("world");
19706 }
19707 "#
19708 .unindent();
19709 executor.run_until_parked();
19710 cx.set_state(
19711 &r#"
19712 use some::mod1;
19713 use some::mod2;
19714
19715 ˇconst B: u32 = 42;
19716 const C: u32 = 42;
19717
19718
19719 fn main() {
19720 println!("hello");
19721
19722 println!("world");
19723 }
19724 "#
19725 .unindent(),
19726 );
19727
19728 cx.set_head_text(&diff_base);
19729 executor.run_until_parked();
19730
19731 cx.update_editor(|editor, window, cx| {
19732 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19733 });
19734 executor.run_until_parked();
19735
19736 cx.assert_state_with_diff(
19737 r#"
19738 use some::mod1;
19739 use some::mod2;
19740
19741 - const A: u32 = 42;
19742 ˇconst B: u32 = 42;
19743 const C: u32 = 42;
19744
19745
19746 fn main() {
19747 println!("hello");
19748
19749 println!("world");
19750 }
19751 "#
19752 .unindent(),
19753 );
19754
19755 cx.update_editor(|editor, window, cx| {
19756 editor.delete_line(&DeleteLine, window, cx);
19757 });
19758 executor.run_until_parked();
19759 cx.assert_state_with_diff(
19760 r#"
19761 use some::mod1;
19762 use some::mod2;
19763
19764 - const A: u32 = 42;
19765 - const B: u32 = 42;
19766 ˇconst C: u32 = 42;
19767
19768
19769 fn main() {
19770 println!("hello");
19771
19772 println!("world");
19773 }
19774 "#
19775 .unindent(),
19776 );
19777
19778 cx.update_editor(|editor, window, cx| {
19779 editor.delete_line(&DeleteLine, window, cx);
19780 });
19781 executor.run_until_parked();
19782 cx.assert_state_with_diff(
19783 r#"
19784 use some::mod1;
19785 use some::mod2;
19786
19787 - const A: u32 = 42;
19788 - const B: u32 = 42;
19789 - const C: u32 = 42;
19790 ˇ
19791
19792 fn main() {
19793 println!("hello");
19794
19795 println!("world");
19796 }
19797 "#
19798 .unindent(),
19799 );
19800
19801 cx.update_editor(|editor, window, cx| {
19802 editor.handle_input("replacement", window, cx);
19803 });
19804 executor.run_until_parked();
19805 cx.assert_state_with_diff(
19806 r#"
19807 use some::mod1;
19808 use some::mod2;
19809
19810 - const A: u32 = 42;
19811 - const B: u32 = 42;
19812 - const C: u32 = 42;
19813 -
19814 + replacementˇ
19815
19816 fn main() {
19817 println!("hello");
19818
19819 println!("world");
19820 }
19821 "#
19822 .unindent(),
19823 );
19824}
19825
19826#[gpui::test]
19827async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19828 init_test(cx, |_| {});
19829
19830 let mut cx = EditorTestContext::new(cx).await;
19831
19832 let base_text = r#"
19833 one
19834 two
19835 three
19836 four
19837 five
19838 "#
19839 .unindent();
19840 executor.run_until_parked();
19841 cx.set_state(
19842 &r#"
19843 one
19844 two
19845 fˇour
19846 five
19847 "#
19848 .unindent(),
19849 );
19850
19851 cx.set_head_text(&base_text);
19852 executor.run_until_parked();
19853
19854 cx.update_editor(|editor, window, cx| {
19855 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19856 });
19857 executor.run_until_parked();
19858
19859 cx.assert_state_with_diff(
19860 r#"
19861 one
19862 two
19863 - three
19864 fˇour
19865 five
19866 "#
19867 .unindent(),
19868 );
19869
19870 cx.update_editor(|editor, window, cx| {
19871 editor.backspace(&Backspace, window, cx);
19872 editor.backspace(&Backspace, window, cx);
19873 });
19874 executor.run_until_parked();
19875 cx.assert_state_with_diff(
19876 r#"
19877 one
19878 two
19879 - threeˇ
19880 - four
19881 + our
19882 five
19883 "#
19884 .unindent(),
19885 );
19886}
19887
19888#[gpui::test]
19889async fn test_edit_after_expanded_modification_hunk(
19890 executor: BackgroundExecutor,
19891 cx: &mut TestAppContext,
19892) {
19893 init_test(cx, |_| {});
19894
19895 let mut cx = EditorTestContext::new(cx).await;
19896
19897 let diff_base = r#"
19898 use some::mod1;
19899 use some::mod2;
19900
19901 const A: u32 = 42;
19902 const B: u32 = 42;
19903 const C: u32 = 42;
19904 const D: u32 = 42;
19905
19906
19907 fn main() {
19908 println!("hello");
19909
19910 println!("world");
19911 }"#
19912 .unindent();
19913
19914 cx.set_state(
19915 &r#"
19916 use some::mod1;
19917 use some::mod2;
19918
19919 const A: u32 = 42;
19920 const B: u32 = 42;
19921 const C: u32 = 43ˇ
19922 const D: u32 = 42;
19923
19924
19925 fn main() {
19926 println!("hello");
19927
19928 println!("world");
19929 }"#
19930 .unindent(),
19931 );
19932
19933 cx.set_head_text(&diff_base);
19934 executor.run_until_parked();
19935 cx.update_editor(|editor, window, cx| {
19936 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19937 });
19938 executor.run_until_parked();
19939
19940 cx.assert_state_with_diff(
19941 r#"
19942 use some::mod1;
19943 use some::mod2;
19944
19945 const A: u32 = 42;
19946 const B: u32 = 42;
19947 - const C: u32 = 42;
19948 + const C: u32 = 43ˇ
19949 const D: u32 = 42;
19950
19951
19952 fn main() {
19953 println!("hello");
19954
19955 println!("world");
19956 }"#
19957 .unindent(),
19958 );
19959
19960 cx.update_editor(|editor, window, cx| {
19961 editor.handle_input("\nnew_line\n", window, cx);
19962 });
19963 executor.run_until_parked();
19964
19965 cx.assert_state_with_diff(
19966 r#"
19967 use some::mod1;
19968 use some::mod2;
19969
19970 const A: u32 = 42;
19971 const B: u32 = 42;
19972 - const C: u32 = 42;
19973 + const C: u32 = 43
19974 + new_line
19975 + ˇ
19976 const D: u32 = 42;
19977
19978
19979 fn main() {
19980 println!("hello");
19981
19982 println!("world");
19983 }"#
19984 .unindent(),
19985 );
19986}
19987
19988#[gpui::test]
19989async fn test_stage_and_unstage_added_file_hunk(
19990 executor: BackgroundExecutor,
19991 cx: &mut TestAppContext,
19992) {
19993 init_test(cx, |_| {});
19994
19995 let mut cx = EditorTestContext::new(cx).await;
19996 cx.update_editor(|editor, _, cx| {
19997 editor.set_expand_all_diff_hunks(cx);
19998 });
19999
20000 let working_copy = r#"
20001 ˇfn main() {
20002 println!("hello, world!");
20003 }
20004 "#
20005 .unindent();
20006
20007 cx.set_state(&working_copy);
20008 executor.run_until_parked();
20009
20010 cx.assert_state_with_diff(
20011 r#"
20012 + ˇfn main() {
20013 + println!("hello, world!");
20014 + }
20015 "#
20016 .unindent(),
20017 );
20018 cx.assert_index_text(None);
20019
20020 cx.update_editor(|editor, window, cx| {
20021 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20022 });
20023 executor.run_until_parked();
20024 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
20025 cx.assert_state_with_diff(
20026 r#"
20027 + ˇfn main() {
20028 + println!("hello, world!");
20029 + }
20030 "#
20031 .unindent(),
20032 );
20033
20034 cx.update_editor(|editor, window, cx| {
20035 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20036 });
20037 executor.run_until_parked();
20038 cx.assert_index_text(None);
20039}
20040
20041async fn setup_indent_guides_editor(
20042 text: &str,
20043 cx: &mut TestAppContext,
20044) -> (BufferId, EditorTestContext) {
20045 init_test(cx, |_| {});
20046
20047 let mut cx = EditorTestContext::new(cx).await;
20048
20049 let buffer_id = cx.update_editor(|editor, window, cx| {
20050 editor.set_text(text, window, cx);
20051 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
20052
20053 buffer_ids[0]
20054 });
20055
20056 (buffer_id, cx)
20057}
20058
20059fn assert_indent_guides(
20060 range: Range<u32>,
20061 expected: Vec<IndentGuide>,
20062 active_indices: Option<Vec<usize>>,
20063 cx: &mut EditorTestContext,
20064) {
20065 let indent_guides = cx.update_editor(|editor, window, cx| {
20066 let snapshot = editor.snapshot(window, cx).display_snapshot;
20067 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
20068 editor,
20069 MultiBufferRow(range.start)..MultiBufferRow(range.end),
20070 true,
20071 &snapshot,
20072 cx,
20073 );
20074
20075 indent_guides.sort_by(|a, b| {
20076 a.depth.cmp(&b.depth).then(
20077 a.start_row
20078 .cmp(&b.start_row)
20079 .then(a.end_row.cmp(&b.end_row)),
20080 )
20081 });
20082 indent_guides
20083 });
20084
20085 if let Some(expected) = active_indices {
20086 let active_indices = cx.update_editor(|editor, window, cx| {
20087 let snapshot = editor.snapshot(window, cx).display_snapshot;
20088 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
20089 });
20090
20091 assert_eq!(
20092 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
20093 expected,
20094 "Active indent guide indices do not match"
20095 );
20096 }
20097
20098 assert_eq!(indent_guides, expected, "Indent guides do not match");
20099}
20100
20101fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
20102 IndentGuide {
20103 buffer_id,
20104 start_row: MultiBufferRow(start_row),
20105 end_row: MultiBufferRow(end_row),
20106 depth,
20107 tab_size: 4,
20108 settings: IndentGuideSettings {
20109 enabled: true,
20110 line_width: 1,
20111 active_line_width: 1,
20112 coloring: IndentGuideColoring::default(),
20113 background_coloring: IndentGuideBackgroundColoring::default(),
20114 },
20115 }
20116}
20117
20118#[gpui::test]
20119async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
20120 let (buffer_id, mut cx) = setup_indent_guides_editor(
20121 &"
20122 fn main() {
20123 let a = 1;
20124 }"
20125 .unindent(),
20126 cx,
20127 )
20128 .await;
20129
20130 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20131}
20132
20133#[gpui::test]
20134async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
20135 let (buffer_id, mut cx) = setup_indent_guides_editor(
20136 &"
20137 fn main() {
20138 let a = 1;
20139 let b = 2;
20140 }"
20141 .unindent(),
20142 cx,
20143 )
20144 .await;
20145
20146 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
20147}
20148
20149#[gpui::test]
20150async fn test_indent_guide_nested(cx: &mut TestAppContext) {
20151 let (buffer_id, mut cx) = setup_indent_guides_editor(
20152 &"
20153 fn main() {
20154 let a = 1;
20155 if a == 3 {
20156 let b = 2;
20157 } else {
20158 let c = 3;
20159 }
20160 }"
20161 .unindent(),
20162 cx,
20163 )
20164 .await;
20165
20166 assert_indent_guides(
20167 0..8,
20168 vec![
20169 indent_guide(buffer_id, 1, 6, 0),
20170 indent_guide(buffer_id, 3, 3, 1),
20171 indent_guide(buffer_id, 5, 5, 1),
20172 ],
20173 None,
20174 &mut cx,
20175 );
20176}
20177
20178#[gpui::test]
20179async fn test_indent_guide_tab(cx: &mut TestAppContext) {
20180 let (buffer_id, mut cx) = setup_indent_guides_editor(
20181 &"
20182 fn main() {
20183 let a = 1;
20184 let b = 2;
20185 let c = 3;
20186 }"
20187 .unindent(),
20188 cx,
20189 )
20190 .await;
20191
20192 assert_indent_guides(
20193 0..5,
20194 vec![
20195 indent_guide(buffer_id, 1, 3, 0),
20196 indent_guide(buffer_id, 2, 2, 1),
20197 ],
20198 None,
20199 &mut cx,
20200 );
20201}
20202
20203#[gpui::test]
20204async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
20205 let (buffer_id, mut cx) = setup_indent_guides_editor(
20206 &"
20207 fn main() {
20208 let a = 1;
20209
20210 let c = 3;
20211 }"
20212 .unindent(),
20213 cx,
20214 )
20215 .await;
20216
20217 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
20218}
20219
20220#[gpui::test]
20221async fn test_indent_guide_complex(cx: &mut TestAppContext) {
20222 let (buffer_id, mut cx) = setup_indent_guides_editor(
20223 &"
20224 fn main() {
20225 let a = 1;
20226
20227 let c = 3;
20228
20229 if a == 3 {
20230 let b = 2;
20231 } else {
20232 let c = 3;
20233 }
20234 }"
20235 .unindent(),
20236 cx,
20237 )
20238 .await;
20239
20240 assert_indent_guides(
20241 0..11,
20242 vec![
20243 indent_guide(buffer_id, 1, 9, 0),
20244 indent_guide(buffer_id, 6, 6, 1),
20245 indent_guide(buffer_id, 8, 8, 1),
20246 ],
20247 None,
20248 &mut cx,
20249 );
20250}
20251
20252#[gpui::test]
20253async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
20254 let (buffer_id, mut cx) = setup_indent_guides_editor(
20255 &"
20256 fn main() {
20257 let a = 1;
20258
20259 let c = 3;
20260
20261 if a == 3 {
20262 let b = 2;
20263 } else {
20264 let c = 3;
20265 }
20266 }"
20267 .unindent(),
20268 cx,
20269 )
20270 .await;
20271
20272 assert_indent_guides(
20273 1..11,
20274 vec![
20275 indent_guide(buffer_id, 1, 9, 0),
20276 indent_guide(buffer_id, 6, 6, 1),
20277 indent_guide(buffer_id, 8, 8, 1),
20278 ],
20279 None,
20280 &mut cx,
20281 );
20282}
20283
20284#[gpui::test]
20285async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
20286 let (buffer_id, mut cx) = setup_indent_guides_editor(
20287 &"
20288 fn main() {
20289 let a = 1;
20290
20291 let c = 3;
20292
20293 if a == 3 {
20294 let b = 2;
20295 } else {
20296 let c = 3;
20297 }
20298 }"
20299 .unindent(),
20300 cx,
20301 )
20302 .await;
20303
20304 assert_indent_guides(
20305 1..10,
20306 vec![
20307 indent_guide(buffer_id, 1, 9, 0),
20308 indent_guide(buffer_id, 6, 6, 1),
20309 indent_guide(buffer_id, 8, 8, 1),
20310 ],
20311 None,
20312 &mut cx,
20313 );
20314}
20315
20316#[gpui::test]
20317async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
20318 let (buffer_id, mut cx) = setup_indent_guides_editor(
20319 &"
20320 fn main() {
20321 if a {
20322 b(
20323 c,
20324 d,
20325 )
20326 } else {
20327 e(
20328 f
20329 )
20330 }
20331 }"
20332 .unindent(),
20333 cx,
20334 )
20335 .await;
20336
20337 assert_indent_guides(
20338 0..11,
20339 vec![
20340 indent_guide(buffer_id, 1, 10, 0),
20341 indent_guide(buffer_id, 2, 5, 1),
20342 indent_guide(buffer_id, 7, 9, 1),
20343 indent_guide(buffer_id, 3, 4, 2),
20344 indent_guide(buffer_id, 8, 8, 2),
20345 ],
20346 None,
20347 &mut cx,
20348 );
20349
20350 cx.update_editor(|editor, window, cx| {
20351 editor.fold_at(MultiBufferRow(2), window, cx);
20352 assert_eq!(
20353 editor.display_text(cx),
20354 "
20355 fn main() {
20356 if a {
20357 b(⋯
20358 )
20359 } else {
20360 e(
20361 f
20362 )
20363 }
20364 }"
20365 .unindent()
20366 );
20367 });
20368
20369 assert_indent_guides(
20370 0..11,
20371 vec![
20372 indent_guide(buffer_id, 1, 10, 0),
20373 indent_guide(buffer_id, 2, 5, 1),
20374 indent_guide(buffer_id, 7, 9, 1),
20375 indent_guide(buffer_id, 8, 8, 2),
20376 ],
20377 None,
20378 &mut cx,
20379 );
20380}
20381
20382#[gpui::test]
20383async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
20384 let (buffer_id, mut cx) = setup_indent_guides_editor(
20385 &"
20386 block1
20387 block2
20388 block3
20389 block4
20390 block2
20391 block1
20392 block1"
20393 .unindent(),
20394 cx,
20395 )
20396 .await;
20397
20398 assert_indent_guides(
20399 1..10,
20400 vec![
20401 indent_guide(buffer_id, 1, 4, 0),
20402 indent_guide(buffer_id, 2, 3, 1),
20403 indent_guide(buffer_id, 3, 3, 2),
20404 ],
20405 None,
20406 &mut cx,
20407 );
20408}
20409
20410#[gpui::test]
20411async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
20412 let (buffer_id, mut cx) = setup_indent_guides_editor(
20413 &"
20414 block1
20415 block2
20416 block3
20417
20418 block1
20419 block1"
20420 .unindent(),
20421 cx,
20422 )
20423 .await;
20424
20425 assert_indent_guides(
20426 0..6,
20427 vec![
20428 indent_guide(buffer_id, 1, 2, 0),
20429 indent_guide(buffer_id, 2, 2, 1),
20430 ],
20431 None,
20432 &mut cx,
20433 );
20434}
20435
20436#[gpui::test]
20437async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
20438 let (buffer_id, mut cx) = setup_indent_guides_editor(
20439 &"
20440 function component() {
20441 \treturn (
20442 \t\t\t
20443 \t\t<div>
20444 \t\t\t<abc></abc>
20445 \t\t</div>
20446 \t)
20447 }"
20448 .unindent(),
20449 cx,
20450 )
20451 .await;
20452
20453 assert_indent_guides(
20454 0..8,
20455 vec![
20456 indent_guide(buffer_id, 1, 6, 0),
20457 indent_guide(buffer_id, 2, 5, 1),
20458 indent_guide(buffer_id, 4, 4, 2),
20459 ],
20460 None,
20461 &mut cx,
20462 );
20463}
20464
20465#[gpui::test]
20466async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
20467 let (buffer_id, mut cx) = setup_indent_guides_editor(
20468 &"
20469 function component() {
20470 \treturn (
20471 \t
20472 \t\t<div>
20473 \t\t\t<abc></abc>
20474 \t\t</div>
20475 \t)
20476 }"
20477 .unindent(),
20478 cx,
20479 )
20480 .await;
20481
20482 assert_indent_guides(
20483 0..8,
20484 vec![
20485 indent_guide(buffer_id, 1, 6, 0),
20486 indent_guide(buffer_id, 2, 5, 1),
20487 indent_guide(buffer_id, 4, 4, 2),
20488 ],
20489 None,
20490 &mut cx,
20491 );
20492}
20493
20494#[gpui::test]
20495async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
20496 let (buffer_id, mut cx) = setup_indent_guides_editor(
20497 &"
20498 block1
20499
20500
20501
20502 block2
20503 "
20504 .unindent(),
20505 cx,
20506 )
20507 .await;
20508
20509 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20510}
20511
20512#[gpui::test]
20513async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
20514 let (buffer_id, mut cx) = setup_indent_guides_editor(
20515 &"
20516 def a:
20517 \tb = 3
20518 \tif True:
20519 \t\tc = 4
20520 \t\td = 5
20521 \tprint(b)
20522 "
20523 .unindent(),
20524 cx,
20525 )
20526 .await;
20527
20528 assert_indent_guides(
20529 0..6,
20530 vec![
20531 indent_guide(buffer_id, 1, 5, 0),
20532 indent_guide(buffer_id, 3, 4, 1),
20533 ],
20534 None,
20535 &mut cx,
20536 );
20537}
20538
20539#[gpui::test]
20540async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
20541 let (buffer_id, mut cx) = setup_indent_guides_editor(
20542 &"
20543 fn main() {
20544 let a = 1;
20545 }"
20546 .unindent(),
20547 cx,
20548 )
20549 .await;
20550
20551 cx.update_editor(|editor, window, cx| {
20552 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20553 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20554 });
20555 });
20556
20557 assert_indent_guides(
20558 0..3,
20559 vec![indent_guide(buffer_id, 1, 1, 0)],
20560 Some(vec![0]),
20561 &mut cx,
20562 );
20563}
20564
20565#[gpui::test]
20566async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
20567 let (buffer_id, mut cx) = setup_indent_guides_editor(
20568 &"
20569 fn main() {
20570 if 1 == 2 {
20571 let a = 1;
20572 }
20573 }"
20574 .unindent(),
20575 cx,
20576 )
20577 .await;
20578
20579 cx.update_editor(|editor, window, cx| {
20580 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20581 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20582 });
20583 });
20584
20585 assert_indent_guides(
20586 0..4,
20587 vec![
20588 indent_guide(buffer_id, 1, 3, 0),
20589 indent_guide(buffer_id, 2, 2, 1),
20590 ],
20591 Some(vec![1]),
20592 &mut cx,
20593 );
20594
20595 cx.update_editor(|editor, window, cx| {
20596 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20597 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20598 });
20599 });
20600
20601 assert_indent_guides(
20602 0..4,
20603 vec![
20604 indent_guide(buffer_id, 1, 3, 0),
20605 indent_guide(buffer_id, 2, 2, 1),
20606 ],
20607 Some(vec![1]),
20608 &mut cx,
20609 );
20610
20611 cx.update_editor(|editor, window, cx| {
20612 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20613 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
20614 });
20615 });
20616
20617 assert_indent_guides(
20618 0..4,
20619 vec![
20620 indent_guide(buffer_id, 1, 3, 0),
20621 indent_guide(buffer_id, 2, 2, 1),
20622 ],
20623 Some(vec![0]),
20624 &mut cx,
20625 );
20626}
20627
20628#[gpui::test]
20629async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
20630 let (buffer_id, mut cx) = setup_indent_guides_editor(
20631 &"
20632 fn main() {
20633 let a = 1;
20634
20635 let b = 2;
20636 }"
20637 .unindent(),
20638 cx,
20639 )
20640 .await;
20641
20642 cx.update_editor(|editor, window, cx| {
20643 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20644 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20645 });
20646 });
20647
20648 assert_indent_guides(
20649 0..5,
20650 vec![indent_guide(buffer_id, 1, 3, 0)],
20651 Some(vec![0]),
20652 &mut cx,
20653 );
20654}
20655
20656#[gpui::test]
20657async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
20658 let (buffer_id, mut cx) = setup_indent_guides_editor(
20659 &"
20660 def m:
20661 a = 1
20662 pass"
20663 .unindent(),
20664 cx,
20665 )
20666 .await;
20667
20668 cx.update_editor(|editor, window, cx| {
20669 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20670 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20671 });
20672 });
20673
20674 assert_indent_guides(
20675 0..3,
20676 vec![indent_guide(buffer_id, 1, 2, 0)],
20677 Some(vec![0]),
20678 &mut cx,
20679 );
20680}
20681
20682#[gpui::test]
20683async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
20684 init_test(cx, |_| {});
20685 let mut cx = EditorTestContext::new(cx).await;
20686 let text = indoc! {
20687 "
20688 impl A {
20689 fn b() {
20690 0;
20691 3;
20692 5;
20693 6;
20694 7;
20695 }
20696 }
20697 "
20698 };
20699 let base_text = indoc! {
20700 "
20701 impl A {
20702 fn b() {
20703 0;
20704 1;
20705 2;
20706 3;
20707 4;
20708 }
20709 fn c() {
20710 5;
20711 6;
20712 7;
20713 }
20714 }
20715 "
20716 };
20717
20718 cx.update_editor(|editor, window, cx| {
20719 editor.set_text(text, window, cx);
20720
20721 editor.buffer().update(cx, |multibuffer, cx| {
20722 let buffer = multibuffer.as_singleton().unwrap();
20723 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
20724
20725 multibuffer.set_all_diff_hunks_expanded(cx);
20726 multibuffer.add_diff(diff, cx);
20727
20728 buffer.read(cx).remote_id()
20729 })
20730 });
20731 cx.run_until_parked();
20732
20733 cx.assert_state_with_diff(
20734 indoc! { "
20735 impl A {
20736 fn b() {
20737 0;
20738 - 1;
20739 - 2;
20740 3;
20741 - 4;
20742 - }
20743 - fn c() {
20744 5;
20745 6;
20746 7;
20747 }
20748 }
20749 ˇ"
20750 }
20751 .to_string(),
20752 );
20753
20754 let mut actual_guides = cx.update_editor(|editor, window, cx| {
20755 editor
20756 .snapshot(window, cx)
20757 .buffer_snapshot()
20758 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
20759 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
20760 .collect::<Vec<_>>()
20761 });
20762 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
20763 assert_eq!(
20764 actual_guides,
20765 vec![
20766 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
20767 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
20768 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
20769 ]
20770 );
20771}
20772
20773#[gpui::test]
20774async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20775 init_test(cx, |_| {});
20776 let mut cx = EditorTestContext::new(cx).await;
20777
20778 let diff_base = r#"
20779 a
20780 b
20781 c
20782 "#
20783 .unindent();
20784
20785 cx.set_state(
20786 &r#"
20787 ˇA
20788 b
20789 C
20790 "#
20791 .unindent(),
20792 );
20793 cx.set_head_text(&diff_base);
20794 cx.update_editor(|editor, window, cx| {
20795 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20796 });
20797 executor.run_until_parked();
20798
20799 let both_hunks_expanded = r#"
20800 - a
20801 + ˇA
20802 b
20803 - c
20804 + C
20805 "#
20806 .unindent();
20807
20808 cx.assert_state_with_diff(both_hunks_expanded.clone());
20809
20810 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20811 let snapshot = editor.snapshot(window, cx);
20812 let hunks = editor
20813 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
20814 .collect::<Vec<_>>();
20815 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20816 let buffer_id = hunks[0].buffer_id;
20817 hunks
20818 .into_iter()
20819 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20820 .collect::<Vec<_>>()
20821 });
20822 assert_eq!(hunk_ranges.len(), 2);
20823
20824 cx.update_editor(|editor, _, cx| {
20825 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20826 });
20827 executor.run_until_parked();
20828
20829 let second_hunk_expanded = r#"
20830 ˇA
20831 b
20832 - c
20833 + C
20834 "#
20835 .unindent();
20836
20837 cx.assert_state_with_diff(second_hunk_expanded);
20838
20839 cx.update_editor(|editor, _, cx| {
20840 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20841 });
20842 executor.run_until_parked();
20843
20844 cx.assert_state_with_diff(both_hunks_expanded.clone());
20845
20846 cx.update_editor(|editor, _, cx| {
20847 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20848 });
20849 executor.run_until_parked();
20850
20851 let first_hunk_expanded = r#"
20852 - a
20853 + ˇA
20854 b
20855 C
20856 "#
20857 .unindent();
20858
20859 cx.assert_state_with_diff(first_hunk_expanded);
20860
20861 cx.update_editor(|editor, _, cx| {
20862 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20863 });
20864 executor.run_until_parked();
20865
20866 cx.assert_state_with_diff(both_hunks_expanded);
20867
20868 cx.set_state(
20869 &r#"
20870 ˇA
20871 b
20872 "#
20873 .unindent(),
20874 );
20875 cx.run_until_parked();
20876
20877 // TODO this cursor position seems bad
20878 cx.assert_state_with_diff(
20879 r#"
20880 - ˇa
20881 + A
20882 b
20883 "#
20884 .unindent(),
20885 );
20886
20887 cx.update_editor(|editor, window, cx| {
20888 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20889 });
20890
20891 cx.assert_state_with_diff(
20892 r#"
20893 - ˇa
20894 + A
20895 b
20896 - c
20897 "#
20898 .unindent(),
20899 );
20900
20901 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20902 let snapshot = editor.snapshot(window, cx);
20903 let hunks = editor
20904 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
20905 .collect::<Vec<_>>();
20906 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20907 let buffer_id = hunks[0].buffer_id;
20908 hunks
20909 .into_iter()
20910 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20911 .collect::<Vec<_>>()
20912 });
20913 assert_eq!(hunk_ranges.len(), 2);
20914
20915 cx.update_editor(|editor, _, cx| {
20916 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20917 });
20918 executor.run_until_parked();
20919
20920 cx.assert_state_with_diff(
20921 r#"
20922 - ˇa
20923 + A
20924 b
20925 "#
20926 .unindent(),
20927 );
20928}
20929
20930#[gpui::test]
20931async fn test_toggle_deletion_hunk_at_start_of_file(
20932 executor: BackgroundExecutor,
20933 cx: &mut TestAppContext,
20934) {
20935 init_test(cx, |_| {});
20936 let mut cx = EditorTestContext::new(cx).await;
20937
20938 let diff_base = r#"
20939 a
20940 b
20941 c
20942 "#
20943 .unindent();
20944
20945 cx.set_state(
20946 &r#"
20947 ˇb
20948 c
20949 "#
20950 .unindent(),
20951 );
20952 cx.set_head_text(&diff_base);
20953 cx.update_editor(|editor, window, cx| {
20954 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20955 });
20956 executor.run_until_parked();
20957
20958 let hunk_expanded = r#"
20959 - a
20960 ˇb
20961 c
20962 "#
20963 .unindent();
20964
20965 cx.assert_state_with_diff(hunk_expanded.clone());
20966
20967 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20968 let snapshot = editor.snapshot(window, cx);
20969 let hunks = editor
20970 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
20971 .collect::<Vec<_>>();
20972 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20973 let buffer_id = hunks[0].buffer_id;
20974 hunks
20975 .into_iter()
20976 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20977 .collect::<Vec<_>>()
20978 });
20979 assert_eq!(hunk_ranges.len(), 1);
20980
20981 cx.update_editor(|editor, _, cx| {
20982 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20983 });
20984 executor.run_until_parked();
20985
20986 let hunk_collapsed = r#"
20987 ˇb
20988 c
20989 "#
20990 .unindent();
20991
20992 cx.assert_state_with_diff(hunk_collapsed);
20993
20994 cx.update_editor(|editor, _, cx| {
20995 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20996 });
20997 executor.run_until_parked();
20998
20999 cx.assert_state_with_diff(hunk_expanded);
21000}
21001
21002#[gpui::test]
21003async fn test_display_diff_hunks(cx: &mut TestAppContext) {
21004 init_test(cx, |_| {});
21005
21006 let fs = FakeFs::new(cx.executor());
21007 fs.insert_tree(
21008 path!("/test"),
21009 json!({
21010 ".git": {},
21011 "file-1": "ONE\n",
21012 "file-2": "TWO\n",
21013 "file-3": "THREE\n",
21014 }),
21015 )
21016 .await;
21017
21018 fs.set_head_for_repo(
21019 path!("/test/.git").as_ref(),
21020 &[
21021 ("file-1", "one\n".into()),
21022 ("file-2", "two\n".into()),
21023 ("file-3", "three\n".into()),
21024 ],
21025 "deadbeef",
21026 );
21027
21028 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
21029 let mut buffers = vec![];
21030 for i in 1..=3 {
21031 let buffer = project
21032 .update(cx, |project, cx| {
21033 let path = format!(path!("/test/file-{}"), i);
21034 project.open_local_buffer(path, cx)
21035 })
21036 .await
21037 .unwrap();
21038 buffers.push(buffer);
21039 }
21040
21041 let multibuffer = cx.new(|cx| {
21042 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
21043 multibuffer.set_all_diff_hunks_expanded(cx);
21044 for buffer in &buffers {
21045 let snapshot = buffer.read(cx).snapshot();
21046 multibuffer.set_excerpts_for_path(
21047 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
21048 buffer.clone(),
21049 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
21050 2,
21051 cx,
21052 );
21053 }
21054 multibuffer
21055 });
21056
21057 let editor = cx.add_window(|window, cx| {
21058 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
21059 });
21060 cx.run_until_parked();
21061
21062 let snapshot = editor
21063 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21064 .unwrap();
21065 let hunks = snapshot
21066 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
21067 .map(|hunk| match hunk {
21068 DisplayDiffHunk::Unfolded {
21069 display_row_range, ..
21070 } => display_row_range,
21071 DisplayDiffHunk::Folded { .. } => unreachable!(),
21072 })
21073 .collect::<Vec<_>>();
21074 assert_eq!(
21075 hunks,
21076 [
21077 DisplayRow(2)..DisplayRow(4),
21078 DisplayRow(7)..DisplayRow(9),
21079 DisplayRow(12)..DisplayRow(14),
21080 ]
21081 );
21082}
21083
21084#[gpui::test]
21085async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
21086 init_test(cx, |_| {});
21087
21088 let mut cx = EditorTestContext::new(cx).await;
21089 cx.set_head_text(indoc! { "
21090 one
21091 two
21092 three
21093 four
21094 five
21095 "
21096 });
21097 cx.set_index_text(indoc! { "
21098 one
21099 two
21100 three
21101 four
21102 five
21103 "
21104 });
21105 cx.set_state(indoc! {"
21106 one
21107 TWO
21108 ˇTHREE
21109 FOUR
21110 five
21111 "});
21112 cx.run_until_parked();
21113 cx.update_editor(|editor, window, cx| {
21114 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21115 });
21116 cx.run_until_parked();
21117 cx.assert_index_text(Some(indoc! {"
21118 one
21119 TWO
21120 THREE
21121 FOUR
21122 five
21123 "}));
21124 cx.set_state(indoc! { "
21125 one
21126 TWO
21127 ˇTHREE-HUNDRED
21128 FOUR
21129 five
21130 "});
21131 cx.run_until_parked();
21132 cx.update_editor(|editor, window, cx| {
21133 let snapshot = editor.snapshot(window, cx);
21134 let hunks = editor
21135 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21136 .collect::<Vec<_>>();
21137 assert_eq!(hunks.len(), 1);
21138 assert_eq!(
21139 hunks[0].status(),
21140 DiffHunkStatus {
21141 kind: DiffHunkStatusKind::Modified,
21142 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
21143 }
21144 );
21145
21146 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21147 });
21148 cx.run_until_parked();
21149 cx.assert_index_text(Some(indoc! {"
21150 one
21151 TWO
21152 THREE-HUNDRED
21153 FOUR
21154 five
21155 "}));
21156}
21157
21158#[gpui::test]
21159fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
21160 init_test(cx, |_| {});
21161
21162 let editor = cx.add_window(|window, cx| {
21163 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
21164 build_editor(buffer, window, cx)
21165 });
21166
21167 let render_args = Arc::new(Mutex::new(None));
21168 let snapshot = editor
21169 .update(cx, |editor, window, cx| {
21170 let snapshot = editor.buffer().read(cx).snapshot(cx);
21171 let range =
21172 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
21173
21174 struct RenderArgs {
21175 row: MultiBufferRow,
21176 folded: bool,
21177 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
21178 }
21179
21180 let crease = Crease::inline(
21181 range,
21182 FoldPlaceholder::test(),
21183 {
21184 let toggle_callback = render_args.clone();
21185 move |row, folded, callback, _window, _cx| {
21186 *toggle_callback.lock() = Some(RenderArgs {
21187 row,
21188 folded,
21189 callback,
21190 });
21191 div()
21192 }
21193 },
21194 |_row, _folded, _window, _cx| div(),
21195 );
21196
21197 editor.insert_creases(Some(crease), cx);
21198 let snapshot = editor.snapshot(window, cx);
21199 let _div =
21200 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
21201 snapshot
21202 })
21203 .unwrap();
21204
21205 let render_args = render_args.lock().take().unwrap();
21206 assert_eq!(render_args.row, MultiBufferRow(1));
21207 assert!(!render_args.folded);
21208 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21209
21210 cx.update_window(*editor, |_, window, cx| {
21211 (render_args.callback)(true, window, cx)
21212 })
21213 .unwrap();
21214 let snapshot = editor
21215 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21216 .unwrap();
21217 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
21218
21219 cx.update_window(*editor, |_, window, cx| {
21220 (render_args.callback)(false, window, cx)
21221 })
21222 .unwrap();
21223 let snapshot = editor
21224 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21225 .unwrap();
21226 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21227}
21228
21229#[gpui::test]
21230async fn test_input_text(cx: &mut TestAppContext) {
21231 init_test(cx, |_| {});
21232 let mut cx = EditorTestContext::new(cx).await;
21233
21234 cx.set_state(
21235 &r#"ˇone
21236 two
21237
21238 three
21239 fourˇ
21240 five
21241
21242 siˇx"#
21243 .unindent(),
21244 );
21245
21246 cx.dispatch_action(HandleInput(String::new()));
21247 cx.assert_editor_state(
21248 &r#"ˇone
21249 two
21250
21251 three
21252 fourˇ
21253 five
21254
21255 siˇx"#
21256 .unindent(),
21257 );
21258
21259 cx.dispatch_action(HandleInput("AAAA".to_string()));
21260 cx.assert_editor_state(
21261 &r#"AAAAˇone
21262 two
21263
21264 three
21265 fourAAAAˇ
21266 five
21267
21268 siAAAAˇx"#
21269 .unindent(),
21270 );
21271}
21272
21273#[gpui::test]
21274async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
21275 init_test(cx, |_| {});
21276
21277 let mut cx = EditorTestContext::new(cx).await;
21278 cx.set_state(
21279 r#"let foo = 1;
21280let foo = 2;
21281let foo = 3;
21282let fooˇ = 4;
21283let foo = 5;
21284let foo = 6;
21285let foo = 7;
21286let foo = 8;
21287let foo = 9;
21288let foo = 10;
21289let foo = 11;
21290let foo = 12;
21291let foo = 13;
21292let foo = 14;
21293let foo = 15;"#,
21294 );
21295
21296 cx.update_editor(|e, window, cx| {
21297 assert_eq!(
21298 e.next_scroll_position,
21299 NextScrollCursorCenterTopBottom::Center,
21300 "Default next scroll direction is center",
21301 );
21302
21303 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21304 assert_eq!(
21305 e.next_scroll_position,
21306 NextScrollCursorCenterTopBottom::Top,
21307 "After center, next scroll direction should be top",
21308 );
21309
21310 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21311 assert_eq!(
21312 e.next_scroll_position,
21313 NextScrollCursorCenterTopBottom::Bottom,
21314 "After top, next scroll direction should be bottom",
21315 );
21316
21317 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21318 assert_eq!(
21319 e.next_scroll_position,
21320 NextScrollCursorCenterTopBottom::Center,
21321 "After bottom, scrolling should start over",
21322 );
21323
21324 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21325 assert_eq!(
21326 e.next_scroll_position,
21327 NextScrollCursorCenterTopBottom::Top,
21328 "Scrolling continues if retriggered fast enough"
21329 );
21330 });
21331
21332 cx.executor()
21333 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
21334 cx.executor().run_until_parked();
21335 cx.update_editor(|e, _, _| {
21336 assert_eq!(
21337 e.next_scroll_position,
21338 NextScrollCursorCenterTopBottom::Center,
21339 "If scrolling is not triggered fast enough, it should reset"
21340 );
21341 });
21342}
21343
21344#[gpui::test]
21345async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
21346 init_test(cx, |_| {});
21347 let mut cx = EditorLspTestContext::new_rust(
21348 lsp::ServerCapabilities {
21349 definition_provider: Some(lsp::OneOf::Left(true)),
21350 references_provider: Some(lsp::OneOf::Left(true)),
21351 ..lsp::ServerCapabilities::default()
21352 },
21353 cx,
21354 )
21355 .await;
21356
21357 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
21358 let go_to_definition = cx
21359 .lsp
21360 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21361 move |params, _| async move {
21362 if empty_go_to_definition {
21363 Ok(None)
21364 } else {
21365 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
21366 uri: params.text_document_position_params.text_document.uri,
21367 range: lsp::Range::new(
21368 lsp::Position::new(4, 3),
21369 lsp::Position::new(4, 6),
21370 ),
21371 })))
21372 }
21373 },
21374 );
21375 let references = cx
21376 .lsp
21377 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21378 Ok(Some(vec![lsp::Location {
21379 uri: params.text_document_position.text_document.uri,
21380 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
21381 }]))
21382 });
21383 (go_to_definition, references)
21384 };
21385
21386 cx.set_state(
21387 &r#"fn one() {
21388 let mut a = ˇtwo();
21389 }
21390
21391 fn two() {}"#
21392 .unindent(),
21393 );
21394 set_up_lsp_handlers(false, &mut cx);
21395 let navigated = cx
21396 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21397 .await
21398 .expect("Failed to navigate to definition");
21399 assert_eq!(
21400 navigated,
21401 Navigated::Yes,
21402 "Should have navigated to definition from the GetDefinition response"
21403 );
21404 cx.assert_editor_state(
21405 &r#"fn one() {
21406 let mut a = two();
21407 }
21408
21409 fn «twoˇ»() {}"#
21410 .unindent(),
21411 );
21412
21413 let editors = cx.update_workspace(|workspace, _, cx| {
21414 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21415 });
21416 cx.update_editor(|_, _, test_editor_cx| {
21417 assert_eq!(
21418 editors.len(),
21419 1,
21420 "Initially, only one, test, editor should be open in the workspace"
21421 );
21422 assert_eq!(
21423 test_editor_cx.entity(),
21424 editors.last().expect("Asserted len is 1").clone()
21425 );
21426 });
21427
21428 set_up_lsp_handlers(true, &mut cx);
21429 let navigated = cx
21430 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21431 .await
21432 .expect("Failed to navigate to lookup references");
21433 assert_eq!(
21434 navigated,
21435 Navigated::Yes,
21436 "Should have navigated to references as a fallback after empty GoToDefinition response"
21437 );
21438 // We should not change the selections in the existing file,
21439 // if opening another milti buffer with the references
21440 cx.assert_editor_state(
21441 &r#"fn one() {
21442 let mut a = two();
21443 }
21444
21445 fn «twoˇ»() {}"#
21446 .unindent(),
21447 );
21448 let editors = cx.update_workspace(|workspace, _, cx| {
21449 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21450 });
21451 cx.update_editor(|_, _, test_editor_cx| {
21452 assert_eq!(
21453 editors.len(),
21454 2,
21455 "After falling back to references search, we open a new editor with the results"
21456 );
21457 let references_fallback_text = editors
21458 .into_iter()
21459 .find(|new_editor| *new_editor != test_editor_cx.entity())
21460 .expect("Should have one non-test editor now")
21461 .read(test_editor_cx)
21462 .text(test_editor_cx);
21463 assert_eq!(
21464 references_fallback_text, "fn one() {\n let mut a = two();\n}",
21465 "Should use the range from the references response and not the GoToDefinition one"
21466 );
21467 });
21468}
21469
21470#[gpui::test]
21471async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
21472 init_test(cx, |_| {});
21473 cx.update(|cx| {
21474 let mut editor_settings = EditorSettings::get_global(cx).clone();
21475 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
21476 EditorSettings::override_global(editor_settings, cx);
21477 });
21478 let mut cx = EditorLspTestContext::new_rust(
21479 lsp::ServerCapabilities {
21480 definition_provider: Some(lsp::OneOf::Left(true)),
21481 references_provider: Some(lsp::OneOf::Left(true)),
21482 ..lsp::ServerCapabilities::default()
21483 },
21484 cx,
21485 )
21486 .await;
21487 let original_state = r#"fn one() {
21488 let mut a = ˇtwo();
21489 }
21490
21491 fn two() {}"#
21492 .unindent();
21493 cx.set_state(&original_state);
21494
21495 let mut go_to_definition = cx
21496 .lsp
21497 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21498 move |_, _| async move { Ok(None) },
21499 );
21500 let _references = cx
21501 .lsp
21502 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
21503 panic!("Should not call for references with no go to definition fallback")
21504 });
21505
21506 let navigated = cx
21507 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21508 .await
21509 .expect("Failed to navigate to lookup references");
21510 go_to_definition
21511 .next()
21512 .await
21513 .expect("Should have called the go_to_definition handler");
21514
21515 assert_eq!(
21516 navigated,
21517 Navigated::No,
21518 "Should have navigated to references as a fallback after empty GoToDefinition response"
21519 );
21520 cx.assert_editor_state(&original_state);
21521 let editors = cx.update_workspace(|workspace, _, cx| {
21522 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21523 });
21524 cx.update_editor(|_, _, _| {
21525 assert_eq!(
21526 editors.len(),
21527 1,
21528 "After unsuccessful fallback, no other editor should have been opened"
21529 );
21530 });
21531}
21532
21533#[gpui::test]
21534async fn test_find_all_references_editor_reuse(cx: &mut TestAppContext) {
21535 init_test(cx, |_| {});
21536 let mut cx = EditorLspTestContext::new_rust(
21537 lsp::ServerCapabilities {
21538 references_provider: Some(lsp::OneOf::Left(true)),
21539 ..lsp::ServerCapabilities::default()
21540 },
21541 cx,
21542 )
21543 .await;
21544
21545 cx.set_state(
21546 &r#"
21547 fn one() {
21548 let mut a = two();
21549 }
21550
21551 fn ˇtwo() {}"#
21552 .unindent(),
21553 );
21554 cx.lsp
21555 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21556 Ok(Some(vec![
21557 lsp::Location {
21558 uri: params.text_document_position.text_document.uri.clone(),
21559 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21560 },
21561 lsp::Location {
21562 uri: params.text_document_position.text_document.uri,
21563 range: lsp::Range::new(lsp::Position::new(4, 4), lsp::Position::new(4, 7)),
21564 },
21565 ]))
21566 });
21567 let navigated = cx
21568 .update_editor(|editor, window, cx| {
21569 editor.find_all_references(&FindAllReferences, window, cx)
21570 })
21571 .unwrap()
21572 .await
21573 .expect("Failed to navigate to references");
21574 assert_eq!(
21575 navigated,
21576 Navigated::Yes,
21577 "Should have navigated to references from the FindAllReferences response"
21578 );
21579 cx.assert_editor_state(
21580 &r#"fn one() {
21581 let mut a = two();
21582 }
21583
21584 fn ˇtwo() {}"#
21585 .unindent(),
21586 );
21587
21588 let editors = cx.update_workspace(|workspace, _, cx| {
21589 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21590 });
21591 cx.update_editor(|_, _, _| {
21592 assert_eq!(editors.len(), 2, "We should have opened a new multibuffer");
21593 });
21594
21595 cx.set_state(
21596 &r#"fn one() {
21597 let mut a = ˇtwo();
21598 }
21599
21600 fn two() {}"#
21601 .unindent(),
21602 );
21603 let navigated = cx
21604 .update_editor(|editor, window, cx| {
21605 editor.find_all_references(&FindAllReferences, window, cx)
21606 })
21607 .unwrap()
21608 .await
21609 .expect("Failed to navigate to references");
21610 assert_eq!(
21611 navigated,
21612 Navigated::Yes,
21613 "Should have navigated to references from the FindAllReferences response"
21614 );
21615 cx.assert_editor_state(
21616 &r#"fn one() {
21617 let mut a = ˇtwo();
21618 }
21619
21620 fn two() {}"#
21621 .unindent(),
21622 );
21623 let editors = cx.update_workspace(|workspace, _, cx| {
21624 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21625 });
21626 cx.update_editor(|_, _, _| {
21627 assert_eq!(
21628 editors.len(),
21629 2,
21630 "should have re-used the previous multibuffer"
21631 );
21632 });
21633
21634 cx.set_state(
21635 &r#"fn one() {
21636 let mut a = ˇtwo();
21637 }
21638 fn three() {}
21639 fn two() {}"#
21640 .unindent(),
21641 );
21642 cx.lsp
21643 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21644 Ok(Some(vec![
21645 lsp::Location {
21646 uri: params.text_document_position.text_document.uri.clone(),
21647 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21648 },
21649 lsp::Location {
21650 uri: params.text_document_position.text_document.uri,
21651 range: lsp::Range::new(lsp::Position::new(5, 4), lsp::Position::new(5, 7)),
21652 },
21653 ]))
21654 });
21655 let navigated = cx
21656 .update_editor(|editor, window, cx| {
21657 editor.find_all_references(&FindAllReferences, window, cx)
21658 })
21659 .unwrap()
21660 .await
21661 .expect("Failed to navigate to references");
21662 assert_eq!(
21663 navigated,
21664 Navigated::Yes,
21665 "Should have navigated to references from the FindAllReferences response"
21666 );
21667 cx.assert_editor_state(
21668 &r#"fn one() {
21669 let mut a = ˇtwo();
21670 }
21671 fn three() {}
21672 fn two() {}"#
21673 .unindent(),
21674 );
21675 let editors = cx.update_workspace(|workspace, _, cx| {
21676 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21677 });
21678 cx.update_editor(|_, _, _| {
21679 assert_eq!(
21680 editors.len(),
21681 3,
21682 "should have used a new multibuffer as offsets changed"
21683 );
21684 });
21685}
21686#[gpui::test]
21687async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
21688 init_test(cx, |_| {});
21689
21690 let language = Arc::new(Language::new(
21691 LanguageConfig::default(),
21692 Some(tree_sitter_rust::LANGUAGE.into()),
21693 ));
21694
21695 let text = r#"
21696 #[cfg(test)]
21697 mod tests() {
21698 #[test]
21699 fn runnable_1() {
21700 let a = 1;
21701 }
21702
21703 #[test]
21704 fn runnable_2() {
21705 let a = 1;
21706 let b = 2;
21707 }
21708 }
21709 "#
21710 .unindent();
21711
21712 let fs = FakeFs::new(cx.executor());
21713 fs.insert_file("/file.rs", Default::default()).await;
21714
21715 let project = Project::test(fs, ["/a".as_ref()], cx).await;
21716 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21717 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21718 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
21719 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
21720
21721 let editor = cx.new_window_entity(|window, cx| {
21722 Editor::new(
21723 EditorMode::full(),
21724 multi_buffer,
21725 Some(project.clone()),
21726 window,
21727 cx,
21728 )
21729 });
21730
21731 editor.update_in(cx, |editor, window, cx| {
21732 let snapshot = editor.buffer().read(cx).snapshot(cx);
21733 editor.tasks.insert(
21734 (buffer.read(cx).remote_id(), 3),
21735 RunnableTasks {
21736 templates: vec![],
21737 offset: snapshot.anchor_before(43),
21738 column: 0,
21739 extra_variables: HashMap::default(),
21740 context_range: BufferOffset(43)..BufferOffset(85),
21741 },
21742 );
21743 editor.tasks.insert(
21744 (buffer.read(cx).remote_id(), 8),
21745 RunnableTasks {
21746 templates: vec![],
21747 offset: snapshot.anchor_before(86),
21748 column: 0,
21749 extra_variables: HashMap::default(),
21750 context_range: BufferOffset(86)..BufferOffset(191),
21751 },
21752 );
21753
21754 // Test finding task when cursor is inside function body
21755 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21756 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
21757 });
21758 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
21759 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
21760
21761 // Test finding task when cursor is on function name
21762 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21763 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
21764 });
21765 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
21766 assert_eq!(row, 8, "Should find task when cursor is on function name");
21767 });
21768}
21769
21770#[gpui::test]
21771async fn test_folding_buffers(cx: &mut TestAppContext) {
21772 init_test(cx, |_| {});
21773
21774 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
21775 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
21776 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
21777
21778 let fs = FakeFs::new(cx.executor());
21779 fs.insert_tree(
21780 path!("/a"),
21781 json!({
21782 "first.rs": sample_text_1,
21783 "second.rs": sample_text_2,
21784 "third.rs": sample_text_3,
21785 }),
21786 )
21787 .await;
21788 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21789 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21790 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21791 let worktree = project.update(cx, |project, cx| {
21792 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
21793 assert_eq!(worktrees.len(), 1);
21794 worktrees.pop().unwrap()
21795 });
21796 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
21797
21798 let buffer_1 = project
21799 .update(cx, |project, cx| {
21800 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
21801 })
21802 .await
21803 .unwrap();
21804 let buffer_2 = project
21805 .update(cx, |project, cx| {
21806 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
21807 })
21808 .await
21809 .unwrap();
21810 let buffer_3 = project
21811 .update(cx, |project, cx| {
21812 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
21813 })
21814 .await
21815 .unwrap();
21816
21817 let multi_buffer = cx.new(|cx| {
21818 let mut multi_buffer = MultiBuffer::new(ReadWrite);
21819 multi_buffer.push_excerpts(
21820 buffer_1.clone(),
21821 [
21822 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21823 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21824 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21825 ],
21826 cx,
21827 );
21828 multi_buffer.push_excerpts(
21829 buffer_2.clone(),
21830 [
21831 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21832 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21833 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21834 ],
21835 cx,
21836 );
21837 multi_buffer.push_excerpts(
21838 buffer_3.clone(),
21839 [
21840 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21841 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21842 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21843 ],
21844 cx,
21845 );
21846 multi_buffer
21847 });
21848 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
21849 Editor::new(
21850 EditorMode::full(),
21851 multi_buffer.clone(),
21852 Some(project.clone()),
21853 window,
21854 cx,
21855 )
21856 });
21857
21858 assert_eq!(
21859 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21860 "\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",
21861 );
21862
21863 multi_buffer_editor.update(cx, |editor, cx| {
21864 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
21865 });
21866 assert_eq!(
21867 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21868 "\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",
21869 "After folding the first buffer, its text should not be displayed"
21870 );
21871
21872 multi_buffer_editor.update(cx, |editor, cx| {
21873 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
21874 });
21875 assert_eq!(
21876 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21877 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
21878 "After folding the second buffer, its text should not be displayed"
21879 );
21880
21881 multi_buffer_editor.update(cx, |editor, cx| {
21882 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
21883 });
21884 assert_eq!(
21885 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21886 "\n\n\n\n\n",
21887 "After folding the third buffer, its text should not be displayed"
21888 );
21889
21890 // Emulate selection inside the fold logic, that should work
21891 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21892 editor
21893 .snapshot(window, cx)
21894 .next_line_boundary(Point::new(0, 4));
21895 });
21896
21897 multi_buffer_editor.update(cx, |editor, cx| {
21898 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
21899 });
21900 assert_eq!(
21901 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21902 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
21903 "After unfolding the second buffer, its text should be displayed"
21904 );
21905
21906 // Typing inside of buffer 1 causes that buffer to be unfolded.
21907 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21908 assert_eq!(
21909 multi_buffer
21910 .read(cx)
21911 .snapshot(cx)
21912 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
21913 .collect::<String>(),
21914 "bbbb"
21915 );
21916 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
21917 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
21918 });
21919 editor.handle_input("B", window, cx);
21920 });
21921
21922 assert_eq!(
21923 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21924 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
21925 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
21926 );
21927
21928 multi_buffer_editor.update(cx, |editor, cx| {
21929 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
21930 });
21931 assert_eq!(
21932 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21933 "\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",
21934 "After unfolding the all buffers, all original text should be displayed"
21935 );
21936}
21937
21938#[gpui::test]
21939async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
21940 init_test(cx, |_| {});
21941
21942 let sample_text_1 = "1111\n2222\n3333".to_string();
21943 let sample_text_2 = "4444\n5555\n6666".to_string();
21944 let sample_text_3 = "7777\n8888\n9999".to_string();
21945
21946 let fs = FakeFs::new(cx.executor());
21947 fs.insert_tree(
21948 path!("/a"),
21949 json!({
21950 "first.rs": sample_text_1,
21951 "second.rs": sample_text_2,
21952 "third.rs": sample_text_3,
21953 }),
21954 )
21955 .await;
21956 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21957 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21958 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21959 let worktree = project.update(cx, |project, cx| {
21960 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
21961 assert_eq!(worktrees.len(), 1);
21962 worktrees.pop().unwrap()
21963 });
21964 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
21965
21966 let buffer_1 = project
21967 .update(cx, |project, cx| {
21968 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
21969 })
21970 .await
21971 .unwrap();
21972 let buffer_2 = project
21973 .update(cx, |project, cx| {
21974 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
21975 })
21976 .await
21977 .unwrap();
21978 let buffer_3 = project
21979 .update(cx, |project, cx| {
21980 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
21981 })
21982 .await
21983 .unwrap();
21984
21985 let multi_buffer = cx.new(|cx| {
21986 let mut multi_buffer = MultiBuffer::new(ReadWrite);
21987 multi_buffer.push_excerpts(
21988 buffer_1.clone(),
21989 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21990 cx,
21991 );
21992 multi_buffer.push_excerpts(
21993 buffer_2.clone(),
21994 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21995 cx,
21996 );
21997 multi_buffer.push_excerpts(
21998 buffer_3.clone(),
21999 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22000 cx,
22001 );
22002 multi_buffer
22003 });
22004
22005 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22006 Editor::new(
22007 EditorMode::full(),
22008 multi_buffer,
22009 Some(project.clone()),
22010 window,
22011 cx,
22012 )
22013 });
22014
22015 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
22016 assert_eq!(
22017 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22018 full_text,
22019 );
22020
22021 multi_buffer_editor.update(cx, |editor, cx| {
22022 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
22023 });
22024 assert_eq!(
22025 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22026 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
22027 "After folding the first buffer, its text should not be displayed"
22028 );
22029
22030 multi_buffer_editor.update(cx, |editor, cx| {
22031 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
22032 });
22033
22034 assert_eq!(
22035 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22036 "\n\n\n\n\n\n7777\n8888\n9999",
22037 "After folding the second buffer, its text should not be displayed"
22038 );
22039
22040 multi_buffer_editor.update(cx, |editor, cx| {
22041 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
22042 });
22043 assert_eq!(
22044 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22045 "\n\n\n\n\n",
22046 "After folding the third buffer, its text should not be displayed"
22047 );
22048
22049 multi_buffer_editor.update(cx, |editor, cx| {
22050 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
22051 });
22052 assert_eq!(
22053 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22054 "\n\n\n\n4444\n5555\n6666\n\n",
22055 "After unfolding the second buffer, its text should be displayed"
22056 );
22057
22058 multi_buffer_editor.update(cx, |editor, cx| {
22059 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
22060 });
22061 assert_eq!(
22062 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22063 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
22064 "After unfolding the first buffer, its text should be displayed"
22065 );
22066
22067 multi_buffer_editor.update(cx, |editor, cx| {
22068 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
22069 });
22070 assert_eq!(
22071 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22072 full_text,
22073 "After unfolding all buffers, all original text should be displayed"
22074 );
22075}
22076
22077#[gpui::test]
22078async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
22079 init_test(cx, |_| {});
22080
22081 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
22082
22083 let fs = FakeFs::new(cx.executor());
22084 fs.insert_tree(
22085 path!("/a"),
22086 json!({
22087 "main.rs": sample_text,
22088 }),
22089 )
22090 .await;
22091 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22092 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22093 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22094 let worktree = project.update(cx, |project, cx| {
22095 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22096 assert_eq!(worktrees.len(), 1);
22097 worktrees.pop().unwrap()
22098 });
22099 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22100
22101 let buffer_1 = project
22102 .update(cx, |project, cx| {
22103 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22104 })
22105 .await
22106 .unwrap();
22107
22108 let multi_buffer = cx.new(|cx| {
22109 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22110 multi_buffer.push_excerpts(
22111 buffer_1.clone(),
22112 [ExcerptRange::new(
22113 Point::new(0, 0)
22114 ..Point::new(
22115 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
22116 0,
22117 ),
22118 )],
22119 cx,
22120 );
22121 multi_buffer
22122 });
22123 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22124 Editor::new(
22125 EditorMode::full(),
22126 multi_buffer,
22127 Some(project.clone()),
22128 window,
22129 cx,
22130 )
22131 });
22132
22133 let selection_range = Point::new(1, 0)..Point::new(2, 0);
22134 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22135 enum TestHighlight {}
22136 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
22137 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
22138 editor.highlight_text::<TestHighlight>(
22139 vec![highlight_range.clone()],
22140 HighlightStyle::color(Hsla::green()),
22141 cx,
22142 );
22143 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22144 s.select_ranges(Some(highlight_range))
22145 });
22146 });
22147
22148 let full_text = format!("\n\n{sample_text}");
22149 assert_eq!(
22150 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22151 full_text,
22152 );
22153}
22154
22155#[gpui::test]
22156async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
22157 init_test(cx, |_| {});
22158 cx.update(|cx| {
22159 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
22160 "keymaps/default-linux.json",
22161 cx,
22162 )
22163 .unwrap();
22164 cx.bind_keys(default_key_bindings);
22165 });
22166
22167 let (editor, cx) = cx.add_window_view(|window, cx| {
22168 let multi_buffer = MultiBuffer::build_multi(
22169 [
22170 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
22171 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
22172 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
22173 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
22174 ],
22175 cx,
22176 );
22177 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
22178
22179 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
22180 // fold all but the second buffer, so that we test navigating between two
22181 // adjacent folded buffers, as well as folded buffers at the start and
22182 // end the multibuffer
22183 editor.fold_buffer(buffer_ids[0], cx);
22184 editor.fold_buffer(buffer_ids[2], cx);
22185 editor.fold_buffer(buffer_ids[3], cx);
22186
22187 editor
22188 });
22189 cx.simulate_resize(size(px(1000.), px(1000.)));
22190
22191 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
22192 cx.assert_excerpts_with_selections(indoc! {"
22193 [EXCERPT]
22194 ˇ[FOLDED]
22195 [EXCERPT]
22196 a1
22197 b1
22198 [EXCERPT]
22199 [FOLDED]
22200 [EXCERPT]
22201 [FOLDED]
22202 "
22203 });
22204 cx.simulate_keystroke("down");
22205 cx.assert_excerpts_with_selections(indoc! {"
22206 [EXCERPT]
22207 [FOLDED]
22208 [EXCERPT]
22209 ˇa1
22210 b1
22211 [EXCERPT]
22212 [FOLDED]
22213 [EXCERPT]
22214 [FOLDED]
22215 "
22216 });
22217 cx.simulate_keystroke("down");
22218 cx.assert_excerpts_with_selections(indoc! {"
22219 [EXCERPT]
22220 [FOLDED]
22221 [EXCERPT]
22222 a1
22223 ˇb1
22224 [EXCERPT]
22225 [FOLDED]
22226 [EXCERPT]
22227 [FOLDED]
22228 "
22229 });
22230 cx.simulate_keystroke("down");
22231 cx.assert_excerpts_with_selections(indoc! {"
22232 [EXCERPT]
22233 [FOLDED]
22234 [EXCERPT]
22235 a1
22236 b1
22237 ˇ[EXCERPT]
22238 [FOLDED]
22239 [EXCERPT]
22240 [FOLDED]
22241 "
22242 });
22243 cx.simulate_keystroke("down");
22244 cx.assert_excerpts_with_selections(indoc! {"
22245 [EXCERPT]
22246 [FOLDED]
22247 [EXCERPT]
22248 a1
22249 b1
22250 [EXCERPT]
22251 ˇ[FOLDED]
22252 [EXCERPT]
22253 [FOLDED]
22254 "
22255 });
22256 for _ in 0..5 {
22257 cx.simulate_keystroke("down");
22258 cx.assert_excerpts_with_selections(indoc! {"
22259 [EXCERPT]
22260 [FOLDED]
22261 [EXCERPT]
22262 a1
22263 b1
22264 [EXCERPT]
22265 [FOLDED]
22266 [EXCERPT]
22267 ˇ[FOLDED]
22268 "
22269 });
22270 }
22271
22272 cx.simulate_keystroke("up");
22273 cx.assert_excerpts_with_selections(indoc! {"
22274 [EXCERPT]
22275 [FOLDED]
22276 [EXCERPT]
22277 a1
22278 b1
22279 [EXCERPT]
22280 ˇ[FOLDED]
22281 [EXCERPT]
22282 [FOLDED]
22283 "
22284 });
22285 cx.simulate_keystroke("up");
22286 cx.assert_excerpts_with_selections(indoc! {"
22287 [EXCERPT]
22288 [FOLDED]
22289 [EXCERPT]
22290 a1
22291 b1
22292 ˇ[EXCERPT]
22293 [FOLDED]
22294 [EXCERPT]
22295 [FOLDED]
22296 "
22297 });
22298 cx.simulate_keystroke("up");
22299 cx.assert_excerpts_with_selections(indoc! {"
22300 [EXCERPT]
22301 [FOLDED]
22302 [EXCERPT]
22303 a1
22304 ˇb1
22305 [EXCERPT]
22306 [FOLDED]
22307 [EXCERPT]
22308 [FOLDED]
22309 "
22310 });
22311 cx.simulate_keystroke("up");
22312 cx.assert_excerpts_with_selections(indoc! {"
22313 [EXCERPT]
22314 [FOLDED]
22315 [EXCERPT]
22316 ˇa1
22317 b1
22318 [EXCERPT]
22319 [FOLDED]
22320 [EXCERPT]
22321 [FOLDED]
22322 "
22323 });
22324 for _ in 0..5 {
22325 cx.simulate_keystroke("up");
22326 cx.assert_excerpts_with_selections(indoc! {"
22327 [EXCERPT]
22328 ˇ[FOLDED]
22329 [EXCERPT]
22330 a1
22331 b1
22332 [EXCERPT]
22333 [FOLDED]
22334 [EXCERPT]
22335 [FOLDED]
22336 "
22337 });
22338 }
22339}
22340
22341#[gpui::test]
22342async fn test_edit_prediction_text(cx: &mut TestAppContext) {
22343 init_test(cx, |_| {});
22344
22345 // Simple insertion
22346 assert_highlighted_edits(
22347 "Hello, world!",
22348 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
22349 true,
22350 cx,
22351 |highlighted_edits, cx| {
22352 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
22353 assert_eq!(highlighted_edits.highlights.len(), 1);
22354 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
22355 assert_eq!(
22356 highlighted_edits.highlights[0].1.background_color,
22357 Some(cx.theme().status().created_background)
22358 );
22359 },
22360 )
22361 .await;
22362
22363 // Replacement
22364 assert_highlighted_edits(
22365 "This is a test.",
22366 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
22367 false,
22368 cx,
22369 |highlighted_edits, cx| {
22370 assert_eq!(highlighted_edits.text, "That is a test.");
22371 assert_eq!(highlighted_edits.highlights.len(), 1);
22372 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
22373 assert_eq!(
22374 highlighted_edits.highlights[0].1.background_color,
22375 Some(cx.theme().status().created_background)
22376 );
22377 },
22378 )
22379 .await;
22380
22381 // Multiple edits
22382 assert_highlighted_edits(
22383 "Hello, world!",
22384 vec![
22385 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
22386 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
22387 ],
22388 false,
22389 cx,
22390 |highlighted_edits, cx| {
22391 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
22392 assert_eq!(highlighted_edits.highlights.len(), 2);
22393 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
22394 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
22395 assert_eq!(
22396 highlighted_edits.highlights[0].1.background_color,
22397 Some(cx.theme().status().created_background)
22398 );
22399 assert_eq!(
22400 highlighted_edits.highlights[1].1.background_color,
22401 Some(cx.theme().status().created_background)
22402 );
22403 },
22404 )
22405 .await;
22406
22407 // Multiple lines with edits
22408 assert_highlighted_edits(
22409 "First line\nSecond line\nThird line\nFourth line",
22410 vec![
22411 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
22412 (
22413 Point::new(2, 0)..Point::new(2, 10),
22414 "New third line".to_string(),
22415 ),
22416 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
22417 ],
22418 false,
22419 cx,
22420 |highlighted_edits, cx| {
22421 assert_eq!(
22422 highlighted_edits.text,
22423 "Second modified\nNew third line\nFourth updated line"
22424 );
22425 assert_eq!(highlighted_edits.highlights.len(), 3);
22426 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
22427 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
22428 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
22429 for highlight in &highlighted_edits.highlights {
22430 assert_eq!(
22431 highlight.1.background_color,
22432 Some(cx.theme().status().created_background)
22433 );
22434 }
22435 },
22436 )
22437 .await;
22438}
22439
22440#[gpui::test]
22441async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
22442 init_test(cx, |_| {});
22443
22444 // Deletion
22445 assert_highlighted_edits(
22446 "Hello, world!",
22447 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
22448 true,
22449 cx,
22450 |highlighted_edits, cx| {
22451 assert_eq!(highlighted_edits.text, "Hello, world!");
22452 assert_eq!(highlighted_edits.highlights.len(), 1);
22453 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
22454 assert_eq!(
22455 highlighted_edits.highlights[0].1.background_color,
22456 Some(cx.theme().status().deleted_background)
22457 );
22458 },
22459 )
22460 .await;
22461
22462 // Insertion
22463 assert_highlighted_edits(
22464 "Hello, world!",
22465 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
22466 true,
22467 cx,
22468 |highlighted_edits, cx| {
22469 assert_eq!(highlighted_edits.highlights.len(), 1);
22470 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
22471 assert_eq!(
22472 highlighted_edits.highlights[0].1.background_color,
22473 Some(cx.theme().status().created_background)
22474 );
22475 },
22476 )
22477 .await;
22478}
22479
22480async fn assert_highlighted_edits(
22481 text: &str,
22482 edits: Vec<(Range<Point>, String)>,
22483 include_deletions: bool,
22484 cx: &mut TestAppContext,
22485 assertion_fn: impl Fn(HighlightedText, &App),
22486) {
22487 let window = cx.add_window(|window, cx| {
22488 let buffer = MultiBuffer::build_simple(text, cx);
22489 Editor::new(EditorMode::full(), buffer, None, window, cx)
22490 });
22491 let cx = &mut VisualTestContext::from_window(*window, cx);
22492
22493 let (buffer, snapshot) = window
22494 .update(cx, |editor, _window, cx| {
22495 (
22496 editor.buffer().clone(),
22497 editor.buffer().read(cx).snapshot(cx),
22498 )
22499 })
22500 .unwrap();
22501
22502 let edits = edits
22503 .into_iter()
22504 .map(|(range, edit)| {
22505 (
22506 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
22507 edit,
22508 )
22509 })
22510 .collect::<Vec<_>>();
22511
22512 let text_anchor_edits = edits
22513 .clone()
22514 .into_iter()
22515 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
22516 .collect::<Vec<_>>();
22517
22518 let edit_preview = window
22519 .update(cx, |_, _window, cx| {
22520 buffer
22521 .read(cx)
22522 .as_singleton()
22523 .unwrap()
22524 .read(cx)
22525 .preview_edits(text_anchor_edits.into(), cx)
22526 })
22527 .unwrap()
22528 .await;
22529
22530 cx.update(|_window, cx| {
22531 let highlighted_edits = edit_prediction_edit_text(
22532 snapshot.as_singleton().unwrap().2,
22533 &edits,
22534 &edit_preview,
22535 include_deletions,
22536 cx,
22537 );
22538 assertion_fn(highlighted_edits, cx)
22539 });
22540}
22541
22542#[track_caller]
22543fn assert_breakpoint(
22544 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
22545 path: &Arc<Path>,
22546 expected: Vec<(u32, Breakpoint)>,
22547) {
22548 if expected.is_empty() {
22549 assert!(!breakpoints.contains_key(path), "{}", path.display());
22550 } else {
22551 let mut breakpoint = breakpoints
22552 .get(path)
22553 .unwrap()
22554 .iter()
22555 .map(|breakpoint| {
22556 (
22557 breakpoint.row,
22558 Breakpoint {
22559 message: breakpoint.message.clone(),
22560 state: breakpoint.state,
22561 condition: breakpoint.condition.clone(),
22562 hit_condition: breakpoint.hit_condition.clone(),
22563 },
22564 )
22565 })
22566 .collect::<Vec<_>>();
22567
22568 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
22569
22570 assert_eq!(expected, breakpoint);
22571 }
22572}
22573
22574fn add_log_breakpoint_at_cursor(
22575 editor: &mut Editor,
22576 log_message: &str,
22577 window: &mut Window,
22578 cx: &mut Context<Editor>,
22579) {
22580 let (anchor, bp) = editor
22581 .breakpoints_at_cursors(window, cx)
22582 .first()
22583 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
22584 .unwrap_or_else(|| {
22585 let cursor_position: Point = editor.selections.newest(cx).head();
22586
22587 let breakpoint_position = editor
22588 .snapshot(window, cx)
22589 .display_snapshot
22590 .buffer_snapshot()
22591 .anchor_before(Point::new(cursor_position.row, 0));
22592
22593 (breakpoint_position, Breakpoint::new_log(log_message))
22594 });
22595
22596 editor.edit_breakpoint_at_anchor(
22597 anchor,
22598 bp,
22599 BreakpointEditAction::EditLogMessage(log_message.into()),
22600 cx,
22601 );
22602}
22603
22604#[gpui::test]
22605async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
22606 init_test(cx, |_| {});
22607
22608 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22609 let fs = FakeFs::new(cx.executor());
22610 fs.insert_tree(
22611 path!("/a"),
22612 json!({
22613 "main.rs": sample_text,
22614 }),
22615 )
22616 .await;
22617 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22618 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22619 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22620
22621 let fs = FakeFs::new(cx.executor());
22622 fs.insert_tree(
22623 path!("/a"),
22624 json!({
22625 "main.rs": sample_text,
22626 }),
22627 )
22628 .await;
22629 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22630 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22631 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22632 let worktree_id = workspace
22633 .update(cx, |workspace, _window, cx| {
22634 workspace.project().update(cx, |project, cx| {
22635 project.worktrees(cx).next().unwrap().read(cx).id()
22636 })
22637 })
22638 .unwrap();
22639
22640 let buffer = project
22641 .update(cx, |project, cx| {
22642 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22643 })
22644 .await
22645 .unwrap();
22646
22647 let (editor, cx) = cx.add_window_view(|window, cx| {
22648 Editor::new(
22649 EditorMode::full(),
22650 MultiBuffer::build_from_buffer(buffer, cx),
22651 Some(project.clone()),
22652 window,
22653 cx,
22654 )
22655 });
22656
22657 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22658 let abs_path = project.read_with(cx, |project, cx| {
22659 project
22660 .absolute_path(&project_path, cx)
22661 .map(Arc::from)
22662 .unwrap()
22663 });
22664
22665 // assert we can add breakpoint on the first line
22666 editor.update_in(cx, |editor, window, cx| {
22667 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22668 editor.move_to_end(&MoveToEnd, window, cx);
22669 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22670 });
22671
22672 let breakpoints = editor.update(cx, |editor, cx| {
22673 editor
22674 .breakpoint_store()
22675 .as_ref()
22676 .unwrap()
22677 .read(cx)
22678 .all_source_breakpoints(cx)
22679 });
22680
22681 assert_eq!(1, breakpoints.len());
22682 assert_breakpoint(
22683 &breakpoints,
22684 &abs_path,
22685 vec![
22686 (0, Breakpoint::new_standard()),
22687 (3, Breakpoint::new_standard()),
22688 ],
22689 );
22690
22691 editor.update_in(cx, |editor, window, cx| {
22692 editor.move_to_beginning(&MoveToBeginning, window, cx);
22693 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22694 });
22695
22696 let breakpoints = editor.update(cx, |editor, cx| {
22697 editor
22698 .breakpoint_store()
22699 .as_ref()
22700 .unwrap()
22701 .read(cx)
22702 .all_source_breakpoints(cx)
22703 });
22704
22705 assert_eq!(1, breakpoints.len());
22706 assert_breakpoint(
22707 &breakpoints,
22708 &abs_path,
22709 vec![(3, Breakpoint::new_standard())],
22710 );
22711
22712 editor.update_in(cx, |editor, window, cx| {
22713 editor.move_to_end(&MoveToEnd, window, cx);
22714 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22715 });
22716
22717 let breakpoints = editor.update(cx, |editor, cx| {
22718 editor
22719 .breakpoint_store()
22720 .as_ref()
22721 .unwrap()
22722 .read(cx)
22723 .all_source_breakpoints(cx)
22724 });
22725
22726 assert_eq!(0, breakpoints.len());
22727 assert_breakpoint(&breakpoints, &abs_path, vec![]);
22728}
22729
22730#[gpui::test]
22731async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
22732 init_test(cx, |_| {});
22733
22734 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22735
22736 let fs = FakeFs::new(cx.executor());
22737 fs.insert_tree(
22738 path!("/a"),
22739 json!({
22740 "main.rs": sample_text,
22741 }),
22742 )
22743 .await;
22744 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22745 let (workspace, cx) =
22746 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22747
22748 let worktree_id = workspace.update(cx, |workspace, cx| {
22749 workspace.project().update(cx, |project, cx| {
22750 project.worktrees(cx).next().unwrap().read(cx).id()
22751 })
22752 });
22753
22754 let buffer = project
22755 .update(cx, |project, cx| {
22756 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22757 })
22758 .await
22759 .unwrap();
22760
22761 let (editor, cx) = cx.add_window_view(|window, cx| {
22762 Editor::new(
22763 EditorMode::full(),
22764 MultiBuffer::build_from_buffer(buffer, cx),
22765 Some(project.clone()),
22766 window,
22767 cx,
22768 )
22769 });
22770
22771 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22772 let abs_path = project.read_with(cx, |project, cx| {
22773 project
22774 .absolute_path(&project_path, cx)
22775 .map(Arc::from)
22776 .unwrap()
22777 });
22778
22779 editor.update_in(cx, |editor, window, cx| {
22780 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
22781 });
22782
22783 let breakpoints = editor.update(cx, |editor, cx| {
22784 editor
22785 .breakpoint_store()
22786 .as_ref()
22787 .unwrap()
22788 .read(cx)
22789 .all_source_breakpoints(cx)
22790 });
22791
22792 assert_breakpoint(
22793 &breakpoints,
22794 &abs_path,
22795 vec![(0, Breakpoint::new_log("hello world"))],
22796 );
22797
22798 // Removing a log message from a log breakpoint should remove it
22799 editor.update_in(cx, |editor, window, cx| {
22800 add_log_breakpoint_at_cursor(editor, "", window, cx);
22801 });
22802
22803 let breakpoints = editor.update(cx, |editor, cx| {
22804 editor
22805 .breakpoint_store()
22806 .as_ref()
22807 .unwrap()
22808 .read(cx)
22809 .all_source_breakpoints(cx)
22810 });
22811
22812 assert_breakpoint(&breakpoints, &abs_path, vec![]);
22813
22814 editor.update_in(cx, |editor, window, cx| {
22815 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22816 editor.move_to_end(&MoveToEnd, window, cx);
22817 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22818 // Not adding a log message to a standard breakpoint shouldn't remove it
22819 add_log_breakpoint_at_cursor(editor, "", window, cx);
22820 });
22821
22822 let breakpoints = editor.update(cx, |editor, cx| {
22823 editor
22824 .breakpoint_store()
22825 .as_ref()
22826 .unwrap()
22827 .read(cx)
22828 .all_source_breakpoints(cx)
22829 });
22830
22831 assert_breakpoint(
22832 &breakpoints,
22833 &abs_path,
22834 vec![
22835 (0, Breakpoint::new_standard()),
22836 (3, Breakpoint::new_standard()),
22837 ],
22838 );
22839
22840 editor.update_in(cx, |editor, window, cx| {
22841 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
22842 });
22843
22844 let breakpoints = editor.update(cx, |editor, cx| {
22845 editor
22846 .breakpoint_store()
22847 .as_ref()
22848 .unwrap()
22849 .read(cx)
22850 .all_source_breakpoints(cx)
22851 });
22852
22853 assert_breakpoint(
22854 &breakpoints,
22855 &abs_path,
22856 vec![
22857 (0, Breakpoint::new_standard()),
22858 (3, Breakpoint::new_log("hello world")),
22859 ],
22860 );
22861
22862 editor.update_in(cx, |editor, window, cx| {
22863 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
22864 });
22865
22866 let breakpoints = editor.update(cx, |editor, cx| {
22867 editor
22868 .breakpoint_store()
22869 .as_ref()
22870 .unwrap()
22871 .read(cx)
22872 .all_source_breakpoints(cx)
22873 });
22874
22875 assert_breakpoint(
22876 &breakpoints,
22877 &abs_path,
22878 vec![
22879 (0, Breakpoint::new_standard()),
22880 (3, Breakpoint::new_log("hello Earth!!")),
22881 ],
22882 );
22883}
22884
22885/// This also tests that Editor::breakpoint_at_cursor_head is working properly
22886/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
22887/// or when breakpoints were placed out of order. This tests for a regression too
22888#[gpui::test]
22889async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
22890 init_test(cx, |_| {});
22891
22892 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22893 let fs = FakeFs::new(cx.executor());
22894 fs.insert_tree(
22895 path!("/a"),
22896 json!({
22897 "main.rs": sample_text,
22898 }),
22899 )
22900 .await;
22901 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22902 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22903 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22904
22905 let fs = FakeFs::new(cx.executor());
22906 fs.insert_tree(
22907 path!("/a"),
22908 json!({
22909 "main.rs": sample_text,
22910 }),
22911 )
22912 .await;
22913 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22914 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22915 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22916 let worktree_id = workspace
22917 .update(cx, |workspace, _window, cx| {
22918 workspace.project().update(cx, |project, cx| {
22919 project.worktrees(cx).next().unwrap().read(cx).id()
22920 })
22921 })
22922 .unwrap();
22923
22924 let buffer = project
22925 .update(cx, |project, cx| {
22926 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22927 })
22928 .await
22929 .unwrap();
22930
22931 let (editor, cx) = cx.add_window_view(|window, cx| {
22932 Editor::new(
22933 EditorMode::full(),
22934 MultiBuffer::build_from_buffer(buffer, cx),
22935 Some(project.clone()),
22936 window,
22937 cx,
22938 )
22939 });
22940
22941 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22942 let abs_path = project.read_with(cx, |project, cx| {
22943 project
22944 .absolute_path(&project_path, cx)
22945 .map(Arc::from)
22946 .unwrap()
22947 });
22948
22949 // assert we can add breakpoint on the first line
22950 editor.update_in(cx, |editor, window, cx| {
22951 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22952 editor.move_to_end(&MoveToEnd, window, cx);
22953 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22954 editor.move_up(&MoveUp, window, cx);
22955 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22956 });
22957
22958 let breakpoints = editor.update(cx, |editor, cx| {
22959 editor
22960 .breakpoint_store()
22961 .as_ref()
22962 .unwrap()
22963 .read(cx)
22964 .all_source_breakpoints(cx)
22965 });
22966
22967 assert_eq!(1, breakpoints.len());
22968 assert_breakpoint(
22969 &breakpoints,
22970 &abs_path,
22971 vec![
22972 (0, Breakpoint::new_standard()),
22973 (2, Breakpoint::new_standard()),
22974 (3, Breakpoint::new_standard()),
22975 ],
22976 );
22977
22978 editor.update_in(cx, |editor, window, cx| {
22979 editor.move_to_beginning(&MoveToBeginning, window, cx);
22980 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22981 editor.move_to_end(&MoveToEnd, window, cx);
22982 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22983 // Disabling a breakpoint that doesn't exist should do nothing
22984 editor.move_up(&MoveUp, window, cx);
22985 editor.move_up(&MoveUp, window, cx);
22986 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22987 });
22988
22989 let breakpoints = editor.update(cx, |editor, cx| {
22990 editor
22991 .breakpoint_store()
22992 .as_ref()
22993 .unwrap()
22994 .read(cx)
22995 .all_source_breakpoints(cx)
22996 });
22997
22998 let disable_breakpoint = {
22999 let mut bp = Breakpoint::new_standard();
23000 bp.state = BreakpointState::Disabled;
23001 bp
23002 };
23003
23004 assert_eq!(1, breakpoints.len());
23005 assert_breakpoint(
23006 &breakpoints,
23007 &abs_path,
23008 vec![
23009 (0, disable_breakpoint.clone()),
23010 (2, Breakpoint::new_standard()),
23011 (3, disable_breakpoint.clone()),
23012 ],
23013 );
23014
23015 editor.update_in(cx, |editor, window, cx| {
23016 editor.move_to_beginning(&MoveToBeginning, window, cx);
23017 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
23018 editor.move_to_end(&MoveToEnd, window, cx);
23019 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
23020 editor.move_up(&MoveUp, window, cx);
23021 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23022 });
23023
23024 let breakpoints = editor.update(cx, |editor, cx| {
23025 editor
23026 .breakpoint_store()
23027 .as_ref()
23028 .unwrap()
23029 .read(cx)
23030 .all_source_breakpoints(cx)
23031 });
23032
23033 assert_eq!(1, breakpoints.len());
23034 assert_breakpoint(
23035 &breakpoints,
23036 &abs_path,
23037 vec![
23038 (0, Breakpoint::new_standard()),
23039 (2, disable_breakpoint),
23040 (3, Breakpoint::new_standard()),
23041 ],
23042 );
23043}
23044
23045#[gpui::test]
23046async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
23047 init_test(cx, |_| {});
23048 let capabilities = lsp::ServerCapabilities {
23049 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
23050 prepare_provider: Some(true),
23051 work_done_progress_options: Default::default(),
23052 })),
23053 ..Default::default()
23054 };
23055 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23056
23057 cx.set_state(indoc! {"
23058 struct Fˇoo {}
23059 "});
23060
23061 cx.update_editor(|editor, _, cx| {
23062 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23063 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23064 editor.highlight_background::<DocumentHighlightRead>(
23065 &[highlight_range],
23066 |theme| theme.colors().editor_document_highlight_read_background,
23067 cx,
23068 );
23069 });
23070
23071 let mut prepare_rename_handler = cx
23072 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
23073 move |_, _, _| async move {
23074 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
23075 start: lsp::Position {
23076 line: 0,
23077 character: 7,
23078 },
23079 end: lsp::Position {
23080 line: 0,
23081 character: 10,
23082 },
23083 })))
23084 },
23085 );
23086 let prepare_rename_task = cx
23087 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23088 .expect("Prepare rename was not started");
23089 prepare_rename_handler.next().await.unwrap();
23090 prepare_rename_task.await.expect("Prepare rename failed");
23091
23092 let mut rename_handler =
23093 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23094 let edit = lsp::TextEdit {
23095 range: lsp::Range {
23096 start: lsp::Position {
23097 line: 0,
23098 character: 7,
23099 },
23100 end: lsp::Position {
23101 line: 0,
23102 character: 10,
23103 },
23104 },
23105 new_text: "FooRenamed".to_string(),
23106 };
23107 Ok(Some(lsp::WorkspaceEdit::new(
23108 // Specify the same edit twice
23109 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
23110 )))
23111 });
23112 let rename_task = cx
23113 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23114 .expect("Confirm rename was not started");
23115 rename_handler.next().await.unwrap();
23116 rename_task.await.expect("Confirm rename failed");
23117 cx.run_until_parked();
23118
23119 // Despite two edits, only one is actually applied as those are identical
23120 cx.assert_editor_state(indoc! {"
23121 struct FooRenamedˇ {}
23122 "});
23123}
23124
23125#[gpui::test]
23126async fn test_rename_without_prepare(cx: &mut TestAppContext) {
23127 init_test(cx, |_| {});
23128 // These capabilities indicate that the server does not support prepare rename.
23129 let capabilities = lsp::ServerCapabilities {
23130 rename_provider: Some(lsp::OneOf::Left(true)),
23131 ..Default::default()
23132 };
23133 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23134
23135 cx.set_state(indoc! {"
23136 struct Fˇoo {}
23137 "});
23138
23139 cx.update_editor(|editor, _window, cx| {
23140 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23141 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23142 editor.highlight_background::<DocumentHighlightRead>(
23143 &[highlight_range],
23144 |theme| theme.colors().editor_document_highlight_read_background,
23145 cx,
23146 );
23147 });
23148
23149 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23150 .expect("Prepare rename was not started")
23151 .await
23152 .expect("Prepare rename failed");
23153
23154 let mut rename_handler =
23155 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23156 let edit = lsp::TextEdit {
23157 range: lsp::Range {
23158 start: lsp::Position {
23159 line: 0,
23160 character: 7,
23161 },
23162 end: lsp::Position {
23163 line: 0,
23164 character: 10,
23165 },
23166 },
23167 new_text: "FooRenamed".to_string(),
23168 };
23169 Ok(Some(lsp::WorkspaceEdit::new(
23170 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
23171 )))
23172 });
23173 let rename_task = cx
23174 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23175 .expect("Confirm rename was not started");
23176 rename_handler.next().await.unwrap();
23177 rename_task.await.expect("Confirm rename failed");
23178 cx.run_until_parked();
23179
23180 // Correct range is renamed, as `surrounding_word` is used to find it.
23181 cx.assert_editor_state(indoc! {"
23182 struct FooRenamedˇ {}
23183 "});
23184}
23185
23186#[gpui::test]
23187async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
23188 init_test(cx, |_| {});
23189 let mut cx = EditorTestContext::new(cx).await;
23190
23191 let language = Arc::new(
23192 Language::new(
23193 LanguageConfig::default(),
23194 Some(tree_sitter_html::LANGUAGE.into()),
23195 )
23196 .with_brackets_query(
23197 r#"
23198 ("<" @open "/>" @close)
23199 ("</" @open ">" @close)
23200 ("<" @open ">" @close)
23201 ("\"" @open "\"" @close)
23202 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
23203 "#,
23204 )
23205 .unwrap(),
23206 );
23207 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23208
23209 cx.set_state(indoc! {"
23210 <span>ˇ</span>
23211 "});
23212 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23213 cx.assert_editor_state(indoc! {"
23214 <span>
23215 ˇ
23216 </span>
23217 "});
23218
23219 cx.set_state(indoc! {"
23220 <span><span></span>ˇ</span>
23221 "});
23222 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23223 cx.assert_editor_state(indoc! {"
23224 <span><span></span>
23225 ˇ</span>
23226 "});
23227
23228 cx.set_state(indoc! {"
23229 <span>ˇ
23230 </span>
23231 "});
23232 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23233 cx.assert_editor_state(indoc! {"
23234 <span>
23235 ˇ
23236 </span>
23237 "});
23238}
23239
23240#[gpui::test(iterations = 10)]
23241async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
23242 init_test(cx, |_| {});
23243
23244 let fs = FakeFs::new(cx.executor());
23245 fs.insert_tree(
23246 path!("/dir"),
23247 json!({
23248 "a.ts": "a",
23249 }),
23250 )
23251 .await;
23252
23253 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
23254 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23255 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23256
23257 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23258 language_registry.add(Arc::new(Language::new(
23259 LanguageConfig {
23260 name: "TypeScript".into(),
23261 matcher: LanguageMatcher {
23262 path_suffixes: vec!["ts".to_string()],
23263 ..Default::default()
23264 },
23265 ..Default::default()
23266 },
23267 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
23268 )));
23269 let mut fake_language_servers = language_registry.register_fake_lsp(
23270 "TypeScript",
23271 FakeLspAdapter {
23272 capabilities: lsp::ServerCapabilities {
23273 code_lens_provider: Some(lsp::CodeLensOptions {
23274 resolve_provider: Some(true),
23275 }),
23276 execute_command_provider: Some(lsp::ExecuteCommandOptions {
23277 commands: vec!["_the/command".to_string()],
23278 ..lsp::ExecuteCommandOptions::default()
23279 }),
23280 ..lsp::ServerCapabilities::default()
23281 },
23282 ..FakeLspAdapter::default()
23283 },
23284 );
23285
23286 let editor = workspace
23287 .update(cx, |workspace, window, cx| {
23288 workspace.open_abs_path(
23289 PathBuf::from(path!("/dir/a.ts")),
23290 OpenOptions::default(),
23291 window,
23292 cx,
23293 )
23294 })
23295 .unwrap()
23296 .await
23297 .unwrap()
23298 .downcast::<Editor>()
23299 .unwrap();
23300 cx.executor().run_until_parked();
23301
23302 let fake_server = fake_language_servers.next().await.unwrap();
23303
23304 let buffer = editor.update(cx, |editor, cx| {
23305 editor
23306 .buffer()
23307 .read(cx)
23308 .as_singleton()
23309 .expect("have opened a single file by path")
23310 });
23311
23312 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
23313 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
23314 drop(buffer_snapshot);
23315 let actions = cx
23316 .update_window(*workspace, |_, window, cx| {
23317 project.code_actions(&buffer, anchor..anchor, window, cx)
23318 })
23319 .unwrap();
23320
23321 fake_server
23322 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23323 Ok(Some(vec![
23324 lsp::CodeLens {
23325 range: lsp::Range::default(),
23326 command: Some(lsp::Command {
23327 title: "Code lens command".to_owned(),
23328 command: "_the/command".to_owned(),
23329 arguments: None,
23330 }),
23331 data: None,
23332 },
23333 lsp::CodeLens {
23334 range: lsp::Range::default(),
23335 command: Some(lsp::Command {
23336 title: "Command not in capabilities".to_owned(),
23337 command: "not in capabilities".to_owned(),
23338 arguments: None,
23339 }),
23340 data: None,
23341 },
23342 lsp::CodeLens {
23343 range: lsp::Range {
23344 start: lsp::Position {
23345 line: 1,
23346 character: 1,
23347 },
23348 end: lsp::Position {
23349 line: 1,
23350 character: 1,
23351 },
23352 },
23353 command: Some(lsp::Command {
23354 title: "Command not in range".to_owned(),
23355 command: "_the/command".to_owned(),
23356 arguments: None,
23357 }),
23358 data: None,
23359 },
23360 ]))
23361 })
23362 .next()
23363 .await;
23364
23365 let actions = actions.await.unwrap();
23366 assert_eq!(
23367 actions.len(),
23368 1,
23369 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
23370 );
23371 let action = actions[0].clone();
23372 let apply = project.update(cx, |project, cx| {
23373 project.apply_code_action(buffer.clone(), action, true, cx)
23374 });
23375
23376 // Resolving the code action does not populate its edits. In absence of
23377 // edits, we must execute the given command.
23378 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
23379 |mut lens, _| async move {
23380 let lens_command = lens.command.as_mut().expect("should have a command");
23381 assert_eq!(lens_command.title, "Code lens command");
23382 lens_command.arguments = Some(vec![json!("the-argument")]);
23383 Ok(lens)
23384 },
23385 );
23386
23387 // While executing the command, the language server sends the editor
23388 // a `workspaceEdit` request.
23389 fake_server
23390 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
23391 let fake = fake_server.clone();
23392 move |params, _| {
23393 assert_eq!(params.command, "_the/command");
23394 let fake = fake.clone();
23395 async move {
23396 fake.server
23397 .request::<lsp::request::ApplyWorkspaceEdit>(
23398 lsp::ApplyWorkspaceEditParams {
23399 label: None,
23400 edit: lsp::WorkspaceEdit {
23401 changes: Some(
23402 [(
23403 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
23404 vec![lsp::TextEdit {
23405 range: lsp::Range::new(
23406 lsp::Position::new(0, 0),
23407 lsp::Position::new(0, 0),
23408 ),
23409 new_text: "X".into(),
23410 }],
23411 )]
23412 .into_iter()
23413 .collect(),
23414 ),
23415 ..lsp::WorkspaceEdit::default()
23416 },
23417 },
23418 )
23419 .await
23420 .into_response()
23421 .unwrap();
23422 Ok(Some(json!(null)))
23423 }
23424 }
23425 })
23426 .next()
23427 .await;
23428
23429 // Applying the code lens command returns a project transaction containing the edits
23430 // sent by the language server in its `workspaceEdit` request.
23431 let transaction = apply.await.unwrap();
23432 assert!(transaction.0.contains_key(&buffer));
23433 buffer.update(cx, |buffer, cx| {
23434 assert_eq!(buffer.text(), "Xa");
23435 buffer.undo(cx);
23436 assert_eq!(buffer.text(), "a");
23437 });
23438
23439 let actions_after_edits = cx
23440 .update_window(*workspace, |_, window, cx| {
23441 project.code_actions(&buffer, anchor..anchor, window, cx)
23442 })
23443 .unwrap()
23444 .await
23445 .unwrap();
23446 assert_eq!(
23447 actions, actions_after_edits,
23448 "For the same selection, same code lens actions should be returned"
23449 );
23450
23451 let _responses =
23452 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23453 panic!("No more code lens requests are expected");
23454 });
23455 editor.update_in(cx, |editor, window, cx| {
23456 editor.select_all(&SelectAll, window, cx);
23457 });
23458 cx.executor().run_until_parked();
23459 let new_actions = cx
23460 .update_window(*workspace, |_, window, cx| {
23461 project.code_actions(&buffer, anchor..anchor, window, cx)
23462 })
23463 .unwrap()
23464 .await
23465 .unwrap();
23466 assert_eq!(
23467 actions, new_actions,
23468 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
23469 );
23470}
23471
23472#[gpui::test]
23473async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
23474 init_test(cx, |_| {});
23475
23476 let fs = FakeFs::new(cx.executor());
23477 let main_text = r#"fn main() {
23478println!("1");
23479println!("2");
23480println!("3");
23481println!("4");
23482println!("5");
23483}"#;
23484 let lib_text = "mod foo {}";
23485 fs.insert_tree(
23486 path!("/a"),
23487 json!({
23488 "lib.rs": lib_text,
23489 "main.rs": main_text,
23490 }),
23491 )
23492 .await;
23493
23494 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23495 let (workspace, cx) =
23496 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23497 let worktree_id = workspace.update(cx, |workspace, cx| {
23498 workspace.project().update(cx, |project, cx| {
23499 project.worktrees(cx).next().unwrap().read(cx).id()
23500 })
23501 });
23502
23503 let expected_ranges = vec![
23504 Point::new(0, 0)..Point::new(0, 0),
23505 Point::new(1, 0)..Point::new(1, 1),
23506 Point::new(2, 0)..Point::new(2, 2),
23507 Point::new(3, 0)..Point::new(3, 3),
23508 ];
23509
23510 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23511 let editor_1 = workspace
23512 .update_in(cx, |workspace, window, cx| {
23513 workspace.open_path(
23514 (worktree_id, rel_path("main.rs")),
23515 Some(pane_1.downgrade()),
23516 true,
23517 window,
23518 cx,
23519 )
23520 })
23521 .unwrap()
23522 .await
23523 .downcast::<Editor>()
23524 .unwrap();
23525 pane_1.update(cx, |pane, cx| {
23526 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23527 open_editor.update(cx, |editor, cx| {
23528 assert_eq!(
23529 editor.display_text(cx),
23530 main_text,
23531 "Original main.rs text on initial open",
23532 );
23533 assert_eq!(
23534 editor
23535 .selections
23536 .all::<Point>(cx)
23537 .into_iter()
23538 .map(|s| s.range())
23539 .collect::<Vec<_>>(),
23540 vec![Point::zero()..Point::zero()],
23541 "Default selections on initial open",
23542 );
23543 })
23544 });
23545 editor_1.update_in(cx, |editor, window, cx| {
23546 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23547 s.select_ranges(expected_ranges.clone());
23548 });
23549 });
23550
23551 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
23552 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
23553 });
23554 let editor_2 = workspace
23555 .update_in(cx, |workspace, window, cx| {
23556 workspace.open_path(
23557 (worktree_id, rel_path("main.rs")),
23558 Some(pane_2.downgrade()),
23559 true,
23560 window,
23561 cx,
23562 )
23563 })
23564 .unwrap()
23565 .await
23566 .downcast::<Editor>()
23567 .unwrap();
23568 pane_2.update(cx, |pane, cx| {
23569 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23570 open_editor.update(cx, |editor, cx| {
23571 assert_eq!(
23572 editor.display_text(cx),
23573 main_text,
23574 "Original main.rs text on initial open in another panel",
23575 );
23576 assert_eq!(
23577 editor
23578 .selections
23579 .all::<Point>(cx)
23580 .into_iter()
23581 .map(|s| s.range())
23582 .collect::<Vec<_>>(),
23583 vec![Point::zero()..Point::zero()],
23584 "Default selections on initial open in another panel",
23585 );
23586 })
23587 });
23588
23589 editor_2.update_in(cx, |editor, window, cx| {
23590 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
23591 });
23592
23593 let _other_editor_1 = workspace
23594 .update_in(cx, |workspace, window, cx| {
23595 workspace.open_path(
23596 (worktree_id, rel_path("lib.rs")),
23597 Some(pane_1.downgrade()),
23598 true,
23599 window,
23600 cx,
23601 )
23602 })
23603 .unwrap()
23604 .await
23605 .downcast::<Editor>()
23606 .unwrap();
23607 pane_1
23608 .update_in(cx, |pane, window, cx| {
23609 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23610 })
23611 .await
23612 .unwrap();
23613 drop(editor_1);
23614 pane_1.update(cx, |pane, cx| {
23615 pane.active_item()
23616 .unwrap()
23617 .downcast::<Editor>()
23618 .unwrap()
23619 .update(cx, |editor, cx| {
23620 assert_eq!(
23621 editor.display_text(cx),
23622 lib_text,
23623 "Other file should be open and active",
23624 );
23625 });
23626 assert_eq!(pane.items().count(), 1, "No other editors should be open");
23627 });
23628
23629 let _other_editor_2 = workspace
23630 .update_in(cx, |workspace, window, cx| {
23631 workspace.open_path(
23632 (worktree_id, rel_path("lib.rs")),
23633 Some(pane_2.downgrade()),
23634 true,
23635 window,
23636 cx,
23637 )
23638 })
23639 .unwrap()
23640 .await
23641 .downcast::<Editor>()
23642 .unwrap();
23643 pane_2
23644 .update_in(cx, |pane, window, cx| {
23645 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23646 })
23647 .await
23648 .unwrap();
23649 drop(editor_2);
23650 pane_2.update(cx, |pane, cx| {
23651 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23652 open_editor.update(cx, |editor, cx| {
23653 assert_eq!(
23654 editor.display_text(cx),
23655 lib_text,
23656 "Other file should be open and active in another panel too",
23657 );
23658 });
23659 assert_eq!(
23660 pane.items().count(),
23661 1,
23662 "No other editors should be open in another pane",
23663 );
23664 });
23665
23666 let _editor_1_reopened = workspace
23667 .update_in(cx, |workspace, window, cx| {
23668 workspace.open_path(
23669 (worktree_id, rel_path("main.rs")),
23670 Some(pane_1.downgrade()),
23671 true,
23672 window,
23673 cx,
23674 )
23675 })
23676 .unwrap()
23677 .await
23678 .downcast::<Editor>()
23679 .unwrap();
23680 let _editor_2_reopened = workspace
23681 .update_in(cx, |workspace, window, cx| {
23682 workspace.open_path(
23683 (worktree_id, rel_path("main.rs")),
23684 Some(pane_2.downgrade()),
23685 true,
23686 window,
23687 cx,
23688 )
23689 })
23690 .unwrap()
23691 .await
23692 .downcast::<Editor>()
23693 .unwrap();
23694 pane_1.update(cx, |pane, cx| {
23695 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23696 open_editor.update(cx, |editor, cx| {
23697 assert_eq!(
23698 editor.display_text(cx),
23699 main_text,
23700 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
23701 );
23702 assert_eq!(
23703 editor
23704 .selections
23705 .all::<Point>(cx)
23706 .into_iter()
23707 .map(|s| s.range())
23708 .collect::<Vec<_>>(),
23709 expected_ranges,
23710 "Previous editor in the 1st panel had selections and should get them restored on reopen",
23711 );
23712 })
23713 });
23714 pane_2.update(cx, |pane, cx| {
23715 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23716 open_editor.update(cx, |editor, cx| {
23717 assert_eq!(
23718 editor.display_text(cx),
23719 r#"fn main() {
23720⋯rintln!("1");
23721⋯intln!("2");
23722⋯ntln!("3");
23723println!("4");
23724println!("5");
23725}"#,
23726 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
23727 );
23728 assert_eq!(
23729 editor
23730 .selections
23731 .all::<Point>(cx)
23732 .into_iter()
23733 .map(|s| s.range())
23734 .collect::<Vec<_>>(),
23735 vec![Point::zero()..Point::zero()],
23736 "Previous editor in the 2nd pane had no selections changed hence should restore none",
23737 );
23738 })
23739 });
23740}
23741
23742#[gpui::test]
23743async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
23744 init_test(cx, |_| {});
23745
23746 let fs = FakeFs::new(cx.executor());
23747 let main_text = r#"fn main() {
23748println!("1");
23749println!("2");
23750println!("3");
23751println!("4");
23752println!("5");
23753}"#;
23754 let lib_text = "mod foo {}";
23755 fs.insert_tree(
23756 path!("/a"),
23757 json!({
23758 "lib.rs": lib_text,
23759 "main.rs": main_text,
23760 }),
23761 )
23762 .await;
23763
23764 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23765 let (workspace, cx) =
23766 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23767 let worktree_id = workspace.update(cx, |workspace, cx| {
23768 workspace.project().update(cx, |project, cx| {
23769 project.worktrees(cx).next().unwrap().read(cx).id()
23770 })
23771 });
23772
23773 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23774 let editor = workspace
23775 .update_in(cx, |workspace, window, cx| {
23776 workspace.open_path(
23777 (worktree_id, rel_path("main.rs")),
23778 Some(pane.downgrade()),
23779 true,
23780 window,
23781 cx,
23782 )
23783 })
23784 .unwrap()
23785 .await
23786 .downcast::<Editor>()
23787 .unwrap();
23788 pane.update(cx, |pane, cx| {
23789 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23790 open_editor.update(cx, |editor, cx| {
23791 assert_eq!(
23792 editor.display_text(cx),
23793 main_text,
23794 "Original main.rs text on initial open",
23795 );
23796 })
23797 });
23798 editor.update_in(cx, |editor, window, cx| {
23799 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
23800 });
23801
23802 cx.update_global(|store: &mut SettingsStore, cx| {
23803 store.update_user_settings(cx, |s| {
23804 s.workspace.restore_on_file_reopen = Some(false);
23805 });
23806 });
23807 editor.update_in(cx, |editor, window, cx| {
23808 editor.fold_ranges(
23809 vec![
23810 Point::new(1, 0)..Point::new(1, 1),
23811 Point::new(2, 0)..Point::new(2, 2),
23812 Point::new(3, 0)..Point::new(3, 3),
23813 ],
23814 false,
23815 window,
23816 cx,
23817 );
23818 });
23819 pane.update_in(cx, |pane, window, cx| {
23820 pane.close_all_items(&CloseAllItems::default(), window, cx)
23821 })
23822 .await
23823 .unwrap();
23824 pane.update(cx, |pane, _| {
23825 assert!(pane.active_item().is_none());
23826 });
23827 cx.update_global(|store: &mut SettingsStore, cx| {
23828 store.update_user_settings(cx, |s| {
23829 s.workspace.restore_on_file_reopen = Some(true);
23830 });
23831 });
23832
23833 let _editor_reopened = workspace
23834 .update_in(cx, |workspace, window, cx| {
23835 workspace.open_path(
23836 (worktree_id, rel_path("main.rs")),
23837 Some(pane.downgrade()),
23838 true,
23839 window,
23840 cx,
23841 )
23842 })
23843 .unwrap()
23844 .await
23845 .downcast::<Editor>()
23846 .unwrap();
23847 pane.update(cx, |pane, cx| {
23848 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23849 open_editor.update(cx, |editor, cx| {
23850 assert_eq!(
23851 editor.display_text(cx),
23852 main_text,
23853 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
23854 );
23855 })
23856 });
23857}
23858
23859#[gpui::test]
23860async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
23861 struct EmptyModalView {
23862 focus_handle: gpui::FocusHandle,
23863 }
23864 impl EventEmitter<DismissEvent> for EmptyModalView {}
23865 impl Render for EmptyModalView {
23866 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
23867 div()
23868 }
23869 }
23870 impl Focusable for EmptyModalView {
23871 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
23872 self.focus_handle.clone()
23873 }
23874 }
23875 impl workspace::ModalView for EmptyModalView {}
23876 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
23877 EmptyModalView {
23878 focus_handle: cx.focus_handle(),
23879 }
23880 }
23881
23882 init_test(cx, |_| {});
23883
23884 let fs = FakeFs::new(cx.executor());
23885 let project = Project::test(fs, [], cx).await;
23886 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23887 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
23888 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23889 let editor = cx.new_window_entity(|window, cx| {
23890 Editor::new(
23891 EditorMode::full(),
23892 buffer,
23893 Some(project.clone()),
23894 window,
23895 cx,
23896 )
23897 });
23898 workspace
23899 .update(cx, |workspace, window, cx| {
23900 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
23901 })
23902 .unwrap();
23903 editor.update_in(cx, |editor, window, cx| {
23904 editor.open_context_menu(&OpenContextMenu, window, cx);
23905 assert!(editor.mouse_context_menu.is_some());
23906 });
23907 workspace
23908 .update(cx, |workspace, window, cx| {
23909 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
23910 })
23911 .unwrap();
23912 cx.read(|cx| {
23913 assert!(editor.read(cx).mouse_context_menu.is_none());
23914 });
23915}
23916
23917fn set_linked_edit_ranges(
23918 opening: (Point, Point),
23919 closing: (Point, Point),
23920 editor: &mut Editor,
23921 cx: &mut Context<Editor>,
23922) {
23923 let Some((buffer, _)) = editor
23924 .buffer
23925 .read(cx)
23926 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
23927 else {
23928 panic!("Failed to get buffer for selection position");
23929 };
23930 let buffer = buffer.read(cx);
23931 let buffer_id = buffer.remote_id();
23932 let opening_range = buffer.anchor_before(opening.0)..buffer.anchor_after(opening.1);
23933 let closing_range = buffer.anchor_before(closing.0)..buffer.anchor_after(closing.1);
23934 let mut linked_ranges = HashMap::default();
23935 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
23936 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
23937}
23938
23939#[gpui::test]
23940async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
23941 init_test(cx, |_| {});
23942
23943 let fs = FakeFs::new(cx.executor());
23944 fs.insert_file(path!("/file.html"), Default::default())
23945 .await;
23946
23947 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
23948
23949 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23950 let html_language = Arc::new(Language::new(
23951 LanguageConfig {
23952 name: "HTML".into(),
23953 matcher: LanguageMatcher {
23954 path_suffixes: vec!["html".to_string()],
23955 ..LanguageMatcher::default()
23956 },
23957 brackets: BracketPairConfig {
23958 pairs: vec![BracketPair {
23959 start: "<".into(),
23960 end: ">".into(),
23961 close: true,
23962 ..Default::default()
23963 }],
23964 ..Default::default()
23965 },
23966 ..Default::default()
23967 },
23968 Some(tree_sitter_html::LANGUAGE.into()),
23969 ));
23970 language_registry.add(html_language);
23971 let mut fake_servers = language_registry.register_fake_lsp(
23972 "HTML",
23973 FakeLspAdapter {
23974 capabilities: lsp::ServerCapabilities {
23975 completion_provider: Some(lsp::CompletionOptions {
23976 resolve_provider: Some(true),
23977 ..Default::default()
23978 }),
23979 ..Default::default()
23980 },
23981 ..Default::default()
23982 },
23983 );
23984
23985 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23986 let cx = &mut VisualTestContext::from_window(*workspace, cx);
23987
23988 let worktree_id = workspace
23989 .update(cx, |workspace, _window, cx| {
23990 workspace.project().update(cx, |project, cx| {
23991 project.worktrees(cx).next().unwrap().read(cx).id()
23992 })
23993 })
23994 .unwrap();
23995 project
23996 .update(cx, |project, cx| {
23997 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
23998 })
23999 .await
24000 .unwrap();
24001 let editor = workspace
24002 .update(cx, |workspace, window, cx| {
24003 workspace.open_path((worktree_id, rel_path("file.html")), None, true, window, cx)
24004 })
24005 .unwrap()
24006 .await
24007 .unwrap()
24008 .downcast::<Editor>()
24009 .unwrap();
24010
24011 let fake_server = fake_servers.next().await.unwrap();
24012 editor.update_in(cx, |editor, window, cx| {
24013 editor.set_text("<ad></ad>", window, cx);
24014 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
24015 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
24016 });
24017 set_linked_edit_ranges(
24018 (Point::new(0, 1), Point::new(0, 3)),
24019 (Point::new(0, 6), Point::new(0, 8)),
24020 editor,
24021 cx,
24022 );
24023 });
24024 let mut completion_handle =
24025 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
24026 Ok(Some(lsp::CompletionResponse::Array(vec![
24027 lsp::CompletionItem {
24028 label: "head".to_string(),
24029 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
24030 lsp::InsertReplaceEdit {
24031 new_text: "head".to_string(),
24032 insert: lsp::Range::new(
24033 lsp::Position::new(0, 1),
24034 lsp::Position::new(0, 3),
24035 ),
24036 replace: lsp::Range::new(
24037 lsp::Position::new(0, 1),
24038 lsp::Position::new(0, 3),
24039 ),
24040 },
24041 )),
24042 ..Default::default()
24043 },
24044 ])))
24045 });
24046 editor.update_in(cx, |editor, window, cx| {
24047 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
24048 });
24049 cx.run_until_parked();
24050 completion_handle.next().await.unwrap();
24051 editor.update(cx, |editor, _| {
24052 assert!(
24053 editor.context_menu_visible(),
24054 "Completion menu should be visible"
24055 );
24056 });
24057 editor.update_in(cx, |editor, window, cx| {
24058 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
24059 });
24060 cx.executor().run_until_parked();
24061 editor.update(cx, |editor, cx| {
24062 assert_eq!(editor.text(cx), "<head></head>");
24063 });
24064}
24065
24066#[gpui::test]
24067async fn test_linked_edits_on_typing_punctuation(cx: &mut TestAppContext) {
24068 init_test(cx, |_| {});
24069
24070 let mut cx = EditorTestContext::new(cx).await;
24071 let language = Arc::new(Language::new(
24072 LanguageConfig {
24073 name: "TSX".into(),
24074 matcher: LanguageMatcher {
24075 path_suffixes: vec!["tsx".to_string()],
24076 ..LanguageMatcher::default()
24077 },
24078 brackets: BracketPairConfig {
24079 pairs: vec![BracketPair {
24080 start: "<".into(),
24081 end: ">".into(),
24082 close: true,
24083 ..Default::default()
24084 }],
24085 ..Default::default()
24086 },
24087 linked_edit_characters: HashSet::from_iter(['.']),
24088 ..Default::default()
24089 },
24090 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
24091 ));
24092 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24093
24094 // Test typing > does not extend linked pair
24095 cx.set_state("<divˇ<div></div>");
24096 cx.update_editor(|editor, _, cx| {
24097 set_linked_edit_ranges(
24098 (Point::new(0, 1), Point::new(0, 4)),
24099 (Point::new(0, 11), Point::new(0, 14)),
24100 editor,
24101 cx,
24102 );
24103 });
24104 cx.update_editor(|editor, window, cx| {
24105 editor.handle_input(">", window, cx);
24106 });
24107 cx.assert_editor_state("<div>ˇ<div></div>");
24108
24109 // Test typing . do extend linked pair
24110 cx.set_state("<Animatedˇ></Animated>");
24111 cx.update_editor(|editor, _, cx| {
24112 set_linked_edit_ranges(
24113 (Point::new(0, 1), Point::new(0, 9)),
24114 (Point::new(0, 12), Point::new(0, 20)),
24115 editor,
24116 cx,
24117 );
24118 });
24119 cx.update_editor(|editor, window, cx| {
24120 editor.handle_input(".", window, cx);
24121 });
24122 cx.assert_editor_state("<Animated.ˇ></Animated.>");
24123 cx.update_editor(|editor, _, cx| {
24124 set_linked_edit_ranges(
24125 (Point::new(0, 1), Point::new(0, 10)),
24126 (Point::new(0, 13), Point::new(0, 21)),
24127 editor,
24128 cx,
24129 );
24130 });
24131 cx.update_editor(|editor, window, cx| {
24132 editor.handle_input("V", window, cx);
24133 });
24134 cx.assert_editor_state("<Animated.Vˇ></Animated.V>");
24135}
24136
24137#[gpui::test]
24138async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
24139 init_test(cx, |_| {});
24140
24141 let fs = FakeFs::new(cx.executor());
24142 fs.insert_tree(
24143 path!("/root"),
24144 json!({
24145 "a": {
24146 "main.rs": "fn main() {}",
24147 },
24148 "foo": {
24149 "bar": {
24150 "external_file.rs": "pub mod external {}",
24151 }
24152 }
24153 }),
24154 )
24155 .await;
24156
24157 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
24158 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24159 language_registry.add(rust_lang());
24160 let _fake_servers = language_registry.register_fake_lsp(
24161 "Rust",
24162 FakeLspAdapter {
24163 ..FakeLspAdapter::default()
24164 },
24165 );
24166 let (workspace, cx) =
24167 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24168 let worktree_id = workspace.update(cx, |workspace, cx| {
24169 workspace.project().update(cx, |project, cx| {
24170 project.worktrees(cx).next().unwrap().read(cx).id()
24171 })
24172 });
24173
24174 let assert_language_servers_count =
24175 |expected: usize, context: &str, cx: &mut VisualTestContext| {
24176 project.update(cx, |project, cx| {
24177 let current = project
24178 .lsp_store()
24179 .read(cx)
24180 .as_local()
24181 .unwrap()
24182 .language_servers
24183 .len();
24184 assert_eq!(expected, current, "{context}");
24185 });
24186 };
24187
24188 assert_language_servers_count(
24189 0,
24190 "No servers should be running before any file is open",
24191 cx,
24192 );
24193 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24194 let main_editor = workspace
24195 .update_in(cx, |workspace, window, cx| {
24196 workspace.open_path(
24197 (worktree_id, rel_path("main.rs")),
24198 Some(pane.downgrade()),
24199 true,
24200 window,
24201 cx,
24202 )
24203 })
24204 .unwrap()
24205 .await
24206 .downcast::<Editor>()
24207 .unwrap();
24208 pane.update(cx, |pane, cx| {
24209 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24210 open_editor.update(cx, |editor, cx| {
24211 assert_eq!(
24212 editor.display_text(cx),
24213 "fn main() {}",
24214 "Original main.rs text on initial open",
24215 );
24216 });
24217 assert_eq!(open_editor, main_editor);
24218 });
24219 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
24220
24221 let external_editor = workspace
24222 .update_in(cx, |workspace, window, cx| {
24223 workspace.open_abs_path(
24224 PathBuf::from("/root/foo/bar/external_file.rs"),
24225 OpenOptions::default(),
24226 window,
24227 cx,
24228 )
24229 })
24230 .await
24231 .expect("opening external file")
24232 .downcast::<Editor>()
24233 .expect("downcasted external file's open element to editor");
24234 pane.update(cx, |pane, cx| {
24235 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24236 open_editor.update(cx, |editor, cx| {
24237 assert_eq!(
24238 editor.display_text(cx),
24239 "pub mod external {}",
24240 "External file is open now",
24241 );
24242 });
24243 assert_eq!(open_editor, external_editor);
24244 });
24245 assert_language_servers_count(
24246 1,
24247 "Second, external, *.rs file should join the existing server",
24248 cx,
24249 );
24250
24251 pane.update_in(cx, |pane, window, cx| {
24252 pane.close_active_item(&CloseActiveItem::default(), window, cx)
24253 })
24254 .await
24255 .unwrap();
24256 pane.update_in(cx, |pane, window, cx| {
24257 pane.navigate_backward(&Default::default(), window, cx);
24258 });
24259 cx.run_until_parked();
24260 pane.update(cx, |pane, cx| {
24261 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24262 open_editor.update(cx, |editor, cx| {
24263 assert_eq!(
24264 editor.display_text(cx),
24265 "pub mod external {}",
24266 "External file is open now",
24267 );
24268 });
24269 });
24270 assert_language_servers_count(
24271 1,
24272 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
24273 cx,
24274 );
24275
24276 cx.update(|_, cx| {
24277 workspace::reload(cx);
24278 });
24279 assert_language_servers_count(
24280 1,
24281 "After reloading the worktree with local and external files opened, only one project should be started",
24282 cx,
24283 );
24284}
24285
24286#[gpui::test]
24287async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
24288 init_test(cx, |_| {});
24289
24290 let mut cx = EditorTestContext::new(cx).await;
24291 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24292 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24293
24294 // test cursor move to start of each line on tab
24295 // for `if`, `elif`, `else`, `while`, `with` and `for`
24296 cx.set_state(indoc! {"
24297 def main():
24298 ˇ for item in items:
24299 ˇ while item.active:
24300 ˇ if item.value > 10:
24301 ˇ continue
24302 ˇ elif item.value < 0:
24303 ˇ break
24304 ˇ else:
24305 ˇ with item.context() as ctx:
24306 ˇ yield count
24307 ˇ else:
24308 ˇ log('while else')
24309 ˇ else:
24310 ˇ log('for else')
24311 "});
24312 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24313 cx.assert_editor_state(indoc! {"
24314 def main():
24315 ˇfor item in items:
24316 ˇwhile item.active:
24317 ˇif item.value > 10:
24318 ˇcontinue
24319 ˇelif item.value < 0:
24320 ˇbreak
24321 ˇelse:
24322 ˇwith item.context() as ctx:
24323 ˇyield count
24324 ˇelse:
24325 ˇlog('while else')
24326 ˇelse:
24327 ˇlog('for else')
24328 "});
24329 // test relative indent is preserved when tab
24330 // for `if`, `elif`, `else`, `while`, `with` and `for`
24331 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24332 cx.assert_editor_state(indoc! {"
24333 def main():
24334 ˇfor item in items:
24335 ˇwhile item.active:
24336 ˇif item.value > 10:
24337 ˇcontinue
24338 ˇelif item.value < 0:
24339 ˇbreak
24340 ˇelse:
24341 ˇwith item.context() as ctx:
24342 ˇyield count
24343 ˇelse:
24344 ˇlog('while else')
24345 ˇelse:
24346 ˇlog('for else')
24347 "});
24348
24349 // test cursor move to start of each line on tab
24350 // for `try`, `except`, `else`, `finally`, `match` and `def`
24351 cx.set_state(indoc! {"
24352 def main():
24353 ˇ try:
24354 ˇ fetch()
24355 ˇ except ValueError:
24356 ˇ handle_error()
24357 ˇ else:
24358 ˇ match value:
24359 ˇ case _:
24360 ˇ finally:
24361 ˇ def status():
24362 ˇ return 0
24363 "});
24364 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24365 cx.assert_editor_state(indoc! {"
24366 def main():
24367 ˇtry:
24368 ˇfetch()
24369 ˇexcept ValueError:
24370 ˇhandle_error()
24371 ˇelse:
24372 ˇmatch value:
24373 ˇcase _:
24374 ˇfinally:
24375 ˇdef status():
24376 ˇreturn 0
24377 "});
24378 // test relative indent is preserved when tab
24379 // for `try`, `except`, `else`, `finally`, `match` and `def`
24380 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24381 cx.assert_editor_state(indoc! {"
24382 def main():
24383 ˇtry:
24384 ˇfetch()
24385 ˇexcept ValueError:
24386 ˇhandle_error()
24387 ˇelse:
24388 ˇmatch value:
24389 ˇcase _:
24390 ˇfinally:
24391 ˇdef status():
24392 ˇreturn 0
24393 "});
24394}
24395
24396#[gpui::test]
24397async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
24398 init_test(cx, |_| {});
24399
24400 let mut cx = EditorTestContext::new(cx).await;
24401 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24402 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24403
24404 // test `else` auto outdents when typed inside `if` block
24405 cx.set_state(indoc! {"
24406 def main():
24407 if i == 2:
24408 return
24409 ˇ
24410 "});
24411 cx.update_editor(|editor, window, cx| {
24412 editor.handle_input("else:", window, cx);
24413 });
24414 cx.assert_editor_state(indoc! {"
24415 def main():
24416 if i == 2:
24417 return
24418 else:ˇ
24419 "});
24420
24421 // test `except` auto outdents when typed inside `try` block
24422 cx.set_state(indoc! {"
24423 def main():
24424 try:
24425 i = 2
24426 ˇ
24427 "});
24428 cx.update_editor(|editor, window, cx| {
24429 editor.handle_input("except:", window, cx);
24430 });
24431 cx.assert_editor_state(indoc! {"
24432 def main():
24433 try:
24434 i = 2
24435 except:ˇ
24436 "});
24437
24438 // test `else` auto outdents when typed inside `except` block
24439 cx.set_state(indoc! {"
24440 def main():
24441 try:
24442 i = 2
24443 except:
24444 j = 2
24445 ˇ
24446 "});
24447 cx.update_editor(|editor, window, cx| {
24448 editor.handle_input("else:", window, cx);
24449 });
24450 cx.assert_editor_state(indoc! {"
24451 def main():
24452 try:
24453 i = 2
24454 except:
24455 j = 2
24456 else:ˇ
24457 "});
24458
24459 // test `finally` auto outdents when typed inside `else` block
24460 cx.set_state(indoc! {"
24461 def main():
24462 try:
24463 i = 2
24464 except:
24465 j = 2
24466 else:
24467 k = 2
24468 ˇ
24469 "});
24470 cx.update_editor(|editor, window, cx| {
24471 editor.handle_input("finally:", window, cx);
24472 });
24473 cx.assert_editor_state(indoc! {"
24474 def main():
24475 try:
24476 i = 2
24477 except:
24478 j = 2
24479 else:
24480 k = 2
24481 finally:ˇ
24482 "});
24483
24484 // test `else` does not outdents when typed inside `except` block right after for block
24485 cx.set_state(indoc! {"
24486 def main():
24487 try:
24488 i = 2
24489 except:
24490 for i in range(n):
24491 pass
24492 ˇ
24493 "});
24494 cx.update_editor(|editor, window, cx| {
24495 editor.handle_input("else:", window, cx);
24496 });
24497 cx.assert_editor_state(indoc! {"
24498 def main():
24499 try:
24500 i = 2
24501 except:
24502 for i in range(n):
24503 pass
24504 else:ˇ
24505 "});
24506
24507 // test `finally` auto outdents when typed inside `else` block right after for block
24508 cx.set_state(indoc! {"
24509 def main():
24510 try:
24511 i = 2
24512 except:
24513 j = 2
24514 else:
24515 for i in range(n):
24516 pass
24517 ˇ
24518 "});
24519 cx.update_editor(|editor, window, cx| {
24520 editor.handle_input("finally:", window, cx);
24521 });
24522 cx.assert_editor_state(indoc! {"
24523 def main():
24524 try:
24525 i = 2
24526 except:
24527 j = 2
24528 else:
24529 for i in range(n):
24530 pass
24531 finally:ˇ
24532 "});
24533
24534 // test `except` outdents to inner "try" block
24535 cx.set_state(indoc! {"
24536 def main():
24537 try:
24538 i = 2
24539 if i == 2:
24540 try:
24541 i = 3
24542 ˇ
24543 "});
24544 cx.update_editor(|editor, window, cx| {
24545 editor.handle_input("except:", window, cx);
24546 });
24547 cx.assert_editor_state(indoc! {"
24548 def main():
24549 try:
24550 i = 2
24551 if i == 2:
24552 try:
24553 i = 3
24554 except:ˇ
24555 "});
24556
24557 // test `except` outdents to outer "try" block
24558 cx.set_state(indoc! {"
24559 def main():
24560 try:
24561 i = 2
24562 if i == 2:
24563 try:
24564 i = 3
24565 ˇ
24566 "});
24567 cx.update_editor(|editor, window, cx| {
24568 editor.handle_input("except:", window, cx);
24569 });
24570 cx.assert_editor_state(indoc! {"
24571 def main():
24572 try:
24573 i = 2
24574 if i == 2:
24575 try:
24576 i = 3
24577 except:ˇ
24578 "});
24579
24580 // test `else` stays at correct indent when typed after `for` block
24581 cx.set_state(indoc! {"
24582 def main():
24583 for i in range(10):
24584 if i == 3:
24585 break
24586 ˇ
24587 "});
24588 cx.update_editor(|editor, window, cx| {
24589 editor.handle_input("else:", window, cx);
24590 });
24591 cx.assert_editor_state(indoc! {"
24592 def main():
24593 for i in range(10):
24594 if i == 3:
24595 break
24596 else:ˇ
24597 "});
24598
24599 // test does not outdent on typing after line with square brackets
24600 cx.set_state(indoc! {"
24601 def f() -> list[str]:
24602 ˇ
24603 "});
24604 cx.update_editor(|editor, window, cx| {
24605 editor.handle_input("a", window, cx);
24606 });
24607 cx.assert_editor_state(indoc! {"
24608 def f() -> list[str]:
24609 aˇ
24610 "});
24611
24612 // test does not outdent on typing : after case keyword
24613 cx.set_state(indoc! {"
24614 match 1:
24615 caseˇ
24616 "});
24617 cx.update_editor(|editor, window, cx| {
24618 editor.handle_input(":", window, cx);
24619 });
24620 cx.assert_editor_state(indoc! {"
24621 match 1:
24622 case:ˇ
24623 "});
24624}
24625
24626#[gpui::test]
24627async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
24628 init_test(cx, |_| {});
24629 update_test_language_settings(cx, |settings| {
24630 settings.defaults.extend_comment_on_newline = Some(false);
24631 });
24632 let mut cx = EditorTestContext::new(cx).await;
24633 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24634 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24635
24636 // test correct indent after newline on comment
24637 cx.set_state(indoc! {"
24638 # COMMENT:ˇ
24639 "});
24640 cx.update_editor(|editor, window, cx| {
24641 editor.newline(&Newline, window, cx);
24642 });
24643 cx.assert_editor_state(indoc! {"
24644 # COMMENT:
24645 ˇ
24646 "});
24647
24648 // test correct indent after newline in brackets
24649 cx.set_state(indoc! {"
24650 {ˇ}
24651 "});
24652 cx.update_editor(|editor, window, cx| {
24653 editor.newline(&Newline, window, cx);
24654 });
24655 cx.run_until_parked();
24656 cx.assert_editor_state(indoc! {"
24657 {
24658 ˇ
24659 }
24660 "});
24661
24662 cx.set_state(indoc! {"
24663 (ˇ)
24664 "});
24665 cx.update_editor(|editor, window, cx| {
24666 editor.newline(&Newline, window, cx);
24667 });
24668 cx.run_until_parked();
24669 cx.assert_editor_state(indoc! {"
24670 (
24671 ˇ
24672 )
24673 "});
24674
24675 // do not indent after empty lists or dictionaries
24676 cx.set_state(indoc! {"
24677 a = []ˇ
24678 "});
24679 cx.update_editor(|editor, window, cx| {
24680 editor.newline(&Newline, window, cx);
24681 });
24682 cx.run_until_parked();
24683 cx.assert_editor_state(indoc! {"
24684 a = []
24685 ˇ
24686 "});
24687}
24688
24689#[gpui::test]
24690async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
24691 init_test(cx, |_| {});
24692
24693 let mut cx = EditorTestContext::new(cx).await;
24694 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24695 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24696
24697 // test cursor move to start of each line on tab
24698 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
24699 cx.set_state(indoc! {"
24700 function main() {
24701 ˇ for item in $items; do
24702 ˇ while [ -n \"$item\" ]; do
24703 ˇ if [ \"$value\" -gt 10 ]; then
24704 ˇ continue
24705 ˇ elif [ \"$value\" -lt 0 ]; then
24706 ˇ break
24707 ˇ else
24708 ˇ echo \"$item\"
24709 ˇ fi
24710 ˇ done
24711 ˇ done
24712 ˇ}
24713 "});
24714 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24715 cx.assert_editor_state(indoc! {"
24716 function main() {
24717 ˇfor item in $items; do
24718 ˇwhile [ -n \"$item\" ]; do
24719 ˇif [ \"$value\" -gt 10 ]; then
24720 ˇcontinue
24721 ˇelif [ \"$value\" -lt 0 ]; then
24722 ˇbreak
24723 ˇelse
24724 ˇecho \"$item\"
24725 ˇfi
24726 ˇdone
24727 ˇdone
24728 ˇ}
24729 "});
24730 // test relative indent is preserved when tab
24731 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24732 cx.assert_editor_state(indoc! {"
24733 function main() {
24734 ˇfor item in $items; do
24735 ˇwhile [ -n \"$item\" ]; do
24736 ˇif [ \"$value\" -gt 10 ]; then
24737 ˇcontinue
24738 ˇelif [ \"$value\" -lt 0 ]; then
24739 ˇbreak
24740 ˇelse
24741 ˇecho \"$item\"
24742 ˇfi
24743 ˇdone
24744 ˇdone
24745 ˇ}
24746 "});
24747
24748 // test cursor move to start of each line on tab
24749 // for `case` statement with patterns
24750 cx.set_state(indoc! {"
24751 function handle() {
24752 ˇ case \"$1\" in
24753 ˇ start)
24754 ˇ echo \"a\"
24755 ˇ ;;
24756 ˇ stop)
24757 ˇ echo \"b\"
24758 ˇ ;;
24759 ˇ *)
24760 ˇ echo \"c\"
24761 ˇ ;;
24762 ˇ esac
24763 ˇ}
24764 "});
24765 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24766 cx.assert_editor_state(indoc! {"
24767 function handle() {
24768 ˇcase \"$1\" in
24769 ˇstart)
24770 ˇecho \"a\"
24771 ˇ;;
24772 ˇstop)
24773 ˇecho \"b\"
24774 ˇ;;
24775 ˇ*)
24776 ˇecho \"c\"
24777 ˇ;;
24778 ˇesac
24779 ˇ}
24780 "});
24781}
24782
24783#[gpui::test]
24784async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
24785 init_test(cx, |_| {});
24786
24787 let mut cx = EditorTestContext::new(cx).await;
24788 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24789 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24790
24791 // test indents on comment insert
24792 cx.set_state(indoc! {"
24793 function main() {
24794 ˇ for item in $items; do
24795 ˇ while [ -n \"$item\" ]; do
24796 ˇ if [ \"$value\" -gt 10 ]; then
24797 ˇ continue
24798 ˇ elif [ \"$value\" -lt 0 ]; then
24799 ˇ break
24800 ˇ else
24801 ˇ echo \"$item\"
24802 ˇ fi
24803 ˇ done
24804 ˇ done
24805 ˇ}
24806 "});
24807 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
24808 cx.assert_editor_state(indoc! {"
24809 function main() {
24810 #ˇ for item in $items; do
24811 #ˇ while [ -n \"$item\" ]; do
24812 #ˇ if [ \"$value\" -gt 10 ]; then
24813 #ˇ continue
24814 #ˇ elif [ \"$value\" -lt 0 ]; then
24815 #ˇ break
24816 #ˇ else
24817 #ˇ echo \"$item\"
24818 #ˇ fi
24819 #ˇ done
24820 #ˇ done
24821 #ˇ}
24822 "});
24823}
24824
24825#[gpui::test]
24826async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
24827 init_test(cx, |_| {});
24828
24829 let mut cx = EditorTestContext::new(cx).await;
24830 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24831 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24832
24833 // test `else` auto outdents when typed inside `if` block
24834 cx.set_state(indoc! {"
24835 if [ \"$1\" = \"test\" ]; then
24836 echo \"foo bar\"
24837 ˇ
24838 "});
24839 cx.update_editor(|editor, window, cx| {
24840 editor.handle_input("else", window, cx);
24841 });
24842 cx.assert_editor_state(indoc! {"
24843 if [ \"$1\" = \"test\" ]; then
24844 echo \"foo bar\"
24845 elseˇ
24846 "});
24847
24848 // test `elif` auto outdents when typed inside `if` block
24849 cx.set_state(indoc! {"
24850 if [ \"$1\" = \"test\" ]; then
24851 echo \"foo bar\"
24852 ˇ
24853 "});
24854 cx.update_editor(|editor, window, cx| {
24855 editor.handle_input("elif", window, cx);
24856 });
24857 cx.assert_editor_state(indoc! {"
24858 if [ \"$1\" = \"test\" ]; then
24859 echo \"foo bar\"
24860 elifˇ
24861 "});
24862
24863 // test `fi` auto outdents when typed inside `else` block
24864 cx.set_state(indoc! {"
24865 if [ \"$1\" = \"test\" ]; then
24866 echo \"foo bar\"
24867 else
24868 echo \"bar baz\"
24869 ˇ
24870 "});
24871 cx.update_editor(|editor, window, cx| {
24872 editor.handle_input("fi", window, cx);
24873 });
24874 cx.assert_editor_state(indoc! {"
24875 if [ \"$1\" = \"test\" ]; then
24876 echo \"foo bar\"
24877 else
24878 echo \"bar baz\"
24879 fiˇ
24880 "});
24881
24882 // test `done` auto outdents when typed inside `while` block
24883 cx.set_state(indoc! {"
24884 while read line; do
24885 echo \"$line\"
24886 ˇ
24887 "});
24888 cx.update_editor(|editor, window, cx| {
24889 editor.handle_input("done", window, cx);
24890 });
24891 cx.assert_editor_state(indoc! {"
24892 while read line; do
24893 echo \"$line\"
24894 doneˇ
24895 "});
24896
24897 // test `done` auto outdents when typed inside `for` block
24898 cx.set_state(indoc! {"
24899 for file in *.txt; do
24900 cat \"$file\"
24901 ˇ
24902 "});
24903 cx.update_editor(|editor, window, cx| {
24904 editor.handle_input("done", window, cx);
24905 });
24906 cx.assert_editor_state(indoc! {"
24907 for file in *.txt; do
24908 cat \"$file\"
24909 doneˇ
24910 "});
24911
24912 // test `esac` auto outdents when typed inside `case` block
24913 cx.set_state(indoc! {"
24914 case \"$1\" in
24915 start)
24916 echo \"foo bar\"
24917 ;;
24918 stop)
24919 echo \"bar baz\"
24920 ;;
24921 ˇ
24922 "});
24923 cx.update_editor(|editor, window, cx| {
24924 editor.handle_input("esac", window, cx);
24925 });
24926 cx.assert_editor_state(indoc! {"
24927 case \"$1\" in
24928 start)
24929 echo \"foo bar\"
24930 ;;
24931 stop)
24932 echo \"bar baz\"
24933 ;;
24934 esacˇ
24935 "});
24936
24937 // test `*)` auto outdents when typed inside `case` block
24938 cx.set_state(indoc! {"
24939 case \"$1\" in
24940 start)
24941 echo \"foo bar\"
24942 ;;
24943 ˇ
24944 "});
24945 cx.update_editor(|editor, window, cx| {
24946 editor.handle_input("*)", window, cx);
24947 });
24948 cx.assert_editor_state(indoc! {"
24949 case \"$1\" in
24950 start)
24951 echo \"foo bar\"
24952 ;;
24953 *)ˇ
24954 "});
24955
24956 // test `fi` outdents to correct level with nested if blocks
24957 cx.set_state(indoc! {"
24958 if [ \"$1\" = \"test\" ]; then
24959 echo \"outer if\"
24960 if [ \"$2\" = \"debug\" ]; then
24961 echo \"inner if\"
24962 ˇ
24963 "});
24964 cx.update_editor(|editor, window, cx| {
24965 editor.handle_input("fi", window, cx);
24966 });
24967 cx.assert_editor_state(indoc! {"
24968 if [ \"$1\" = \"test\" ]; then
24969 echo \"outer if\"
24970 if [ \"$2\" = \"debug\" ]; then
24971 echo \"inner if\"
24972 fiˇ
24973 "});
24974}
24975
24976#[gpui::test]
24977async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
24978 init_test(cx, |_| {});
24979 update_test_language_settings(cx, |settings| {
24980 settings.defaults.extend_comment_on_newline = Some(false);
24981 });
24982 let mut cx = EditorTestContext::new(cx).await;
24983 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24984 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24985
24986 // test correct indent after newline on comment
24987 cx.set_state(indoc! {"
24988 # COMMENT:ˇ
24989 "});
24990 cx.update_editor(|editor, window, cx| {
24991 editor.newline(&Newline, window, cx);
24992 });
24993 cx.assert_editor_state(indoc! {"
24994 # COMMENT:
24995 ˇ
24996 "});
24997
24998 // test correct indent after newline after `then`
24999 cx.set_state(indoc! {"
25000
25001 if [ \"$1\" = \"test\" ]; thenˇ
25002 "});
25003 cx.update_editor(|editor, window, cx| {
25004 editor.newline(&Newline, window, cx);
25005 });
25006 cx.run_until_parked();
25007 cx.assert_editor_state(indoc! {"
25008
25009 if [ \"$1\" = \"test\" ]; then
25010 ˇ
25011 "});
25012
25013 // test correct indent after newline after `else`
25014 cx.set_state(indoc! {"
25015 if [ \"$1\" = \"test\" ]; then
25016 elseˇ
25017 "});
25018 cx.update_editor(|editor, window, cx| {
25019 editor.newline(&Newline, window, cx);
25020 });
25021 cx.run_until_parked();
25022 cx.assert_editor_state(indoc! {"
25023 if [ \"$1\" = \"test\" ]; then
25024 else
25025 ˇ
25026 "});
25027
25028 // test correct indent after newline after `elif`
25029 cx.set_state(indoc! {"
25030 if [ \"$1\" = \"test\" ]; then
25031 elifˇ
25032 "});
25033 cx.update_editor(|editor, window, cx| {
25034 editor.newline(&Newline, window, cx);
25035 });
25036 cx.run_until_parked();
25037 cx.assert_editor_state(indoc! {"
25038 if [ \"$1\" = \"test\" ]; then
25039 elif
25040 ˇ
25041 "});
25042
25043 // test correct indent after newline after `do`
25044 cx.set_state(indoc! {"
25045 for file in *.txt; doˇ
25046 "});
25047 cx.update_editor(|editor, window, cx| {
25048 editor.newline(&Newline, window, cx);
25049 });
25050 cx.run_until_parked();
25051 cx.assert_editor_state(indoc! {"
25052 for file in *.txt; do
25053 ˇ
25054 "});
25055
25056 // test correct indent after newline after case pattern
25057 cx.set_state(indoc! {"
25058 case \"$1\" in
25059 start)ˇ
25060 "});
25061 cx.update_editor(|editor, window, cx| {
25062 editor.newline(&Newline, window, cx);
25063 });
25064 cx.run_until_parked();
25065 cx.assert_editor_state(indoc! {"
25066 case \"$1\" in
25067 start)
25068 ˇ
25069 "});
25070
25071 // test correct indent after newline after case pattern
25072 cx.set_state(indoc! {"
25073 case \"$1\" in
25074 start)
25075 ;;
25076 *)ˇ
25077 "});
25078 cx.update_editor(|editor, window, cx| {
25079 editor.newline(&Newline, window, cx);
25080 });
25081 cx.run_until_parked();
25082 cx.assert_editor_state(indoc! {"
25083 case \"$1\" in
25084 start)
25085 ;;
25086 *)
25087 ˇ
25088 "});
25089
25090 // test correct indent after newline after function opening brace
25091 cx.set_state(indoc! {"
25092 function test() {ˇ}
25093 "});
25094 cx.update_editor(|editor, window, cx| {
25095 editor.newline(&Newline, window, cx);
25096 });
25097 cx.run_until_parked();
25098 cx.assert_editor_state(indoc! {"
25099 function test() {
25100 ˇ
25101 }
25102 "});
25103
25104 // test no extra indent after semicolon on same line
25105 cx.set_state(indoc! {"
25106 echo \"test\";ˇ
25107 "});
25108 cx.update_editor(|editor, window, cx| {
25109 editor.newline(&Newline, window, cx);
25110 });
25111 cx.run_until_parked();
25112 cx.assert_editor_state(indoc! {"
25113 echo \"test\";
25114 ˇ
25115 "});
25116}
25117
25118fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
25119 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
25120 point..point
25121}
25122
25123#[track_caller]
25124fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
25125 let (text, ranges) = marked_text_ranges(marked_text, true);
25126 assert_eq!(editor.text(cx), text);
25127 assert_eq!(
25128 editor.selections.ranges(cx),
25129 ranges,
25130 "Assert selections are {}",
25131 marked_text
25132 );
25133}
25134
25135pub fn handle_signature_help_request(
25136 cx: &mut EditorLspTestContext,
25137 mocked_response: lsp::SignatureHelp,
25138) -> impl Future<Output = ()> + use<> {
25139 let mut request =
25140 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
25141 let mocked_response = mocked_response.clone();
25142 async move { Ok(Some(mocked_response)) }
25143 });
25144
25145 async move {
25146 request.next().await;
25147 }
25148}
25149
25150#[track_caller]
25151pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
25152 cx.update_editor(|editor, _, _| {
25153 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
25154 let entries = menu.entries.borrow();
25155 let entries = entries
25156 .iter()
25157 .map(|entry| entry.string.as_str())
25158 .collect::<Vec<_>>();
25159 assert_eq!(entries, expected);
25160 } else {
25161 panic!("Expected completions menu");
25162 }
25163 });
25164}
25165
25166/// Handle completion request passing a marked string specifying where the completion
25167/// should be triggered from using '|' character, what range should be replaced, and what completions
25168/// should be returned using '<' and '>' to delimit the range.
25169///
25170/// Also see `handle_completion_request_with_insert_and_replace`.
25171#[track_caller]
25172pub fn handle_completion_request(
25173 marked_string: &str,
25174 completions: Vec<&'static str>,
25175 is_incomplete: bool,
25176 counter: Arc<AtomicUsize>,
25177 cx: &mut EditorLspTestContext,
25178) -> impl Future<Output = ()> {
25179 let complete_from_marker: TextRangeMarker = '|'.into();
25180 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25181 let (_, mut marked_ranges) = marked_text_ranges_by(
25182 marked_string,
25183 vec![complete_from_marker.clone(), replace_range_marker.clone()],
25184 );
25185
25186 let complete_from_position =
25187 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25188 let replace_range =
25189 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25190
25191 let mut request =
25192 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25193 let completions = completions.clone();
25194 counter.fetch_add(1, atomic::Ordering::Release);
25195 async move {
25196 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25197 assert_eq!(
25198 params.text_document_position.position,
25199 complete_from_position
25200 );
25201 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
25202 is_incomplete,
25203 item_defaults: None,
25204 items: completions
25205 .iter()
25206 .map(|completion_text| lsp::CompletionItem {
25207 label: completion_text.to_string(),
25208 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
25209 range: replace_range,
25210 new_text: completion_text.to_string(),
25211 })),
25212 ..Default::default()
25213 })
25214 .collect(),
25215 })))
25216 }
25217 });
25218
25219 async move {
25220 request.next().await;
25221 }
25222}
25223
25224/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
25225/// given instead, which also contains an `insert` range.
25226///
25227/// This function uses markers to define ranges:
25228/// - `|` marks the cursor position
25229/// - `<>` marks the replace range
25230/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
25231pub fn handle_completion_request_with_insert_and_replace(
25232 cx: &mut EditorLspTestContext,
25233 marked_string: &str,
25234 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
25235 counter: Arc<AtomicUsize>,
25236) -> impl Future<Output = ()> {
25237 let complete_from_marker: TextRangeMarker = '|'.into();
25238 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25239 let insert_range_marker: TextRangeMarker = ('{', '}').into();
25240
25241 let (_, mut marked_ranges) = marked_text_ranges_by(
25242 marked_string,
25243 vec![
25244 complete_from_marker.clone(),
25245 replace_range_marker.clone(),
25246 insert_range_marker.clone(),
25247 ],
25248 );
25249
25250 let complete_from_position =
25251 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25252 let replace_range =
25253 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25254
25255 let insert_range = match marked_ranges.remove(&insert_range_marker) {
25256 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
25257 _ => lsp::Range {
25258 start: replace_range.start,
25259 end: complete_from_position,
25260 },
25261 };
25262
25263 let mut request =
25264 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25265 let completions = completions.clone();
25266 counter.fetch_add(1, atomic::Ordering::Release);
25267 async move {
25268 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25269 assert_eq!(
25270 params.text_document_position.position, complete_from_position,
25271 "marker `|` position doesn't match",
25272 );
25273 Ok(Some(lsp::CompletionResponse::Array(
25274 completions
25275 .iter()
25276 .map(|(label, new_text)| lsp::CompletionItem {
25277 label: label.to_string(),
25278 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
25279 lsp::InsertReplaceEdit {
25280 insert: insert_range,
25281 replace: replace_range,
25282 new_text: new_text.to_string(),
25283 },
25284 )),
25285 ..Default::default()
25286 })
25287 .collect(),
25288 )))
25289 }
25290 });
25291
25292 async move {
25293 request.next().await;
25294 }
25295}
25296
25297fn handle_resolve_completion_request(
25298 cx: &mut EditorLspTestContext,
25299 edits: Option<Vec<(&'static str, &'static str)>>,
25300) -> impl Future<Output = ()> {
25301 let edits = edits.map(|edits| {
25302 edits
25303 .iter()
25304 .map(|(marked_string, new_text)| {
25305 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
25306 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
25307 lsp::TextEdit::new(replace_range, new_text.to_string())
25308 })
25309 .collect::<Vec<_>>()
25310 });
25311
25312 let mut request =
25313 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
25314 let edits = edits.clone();
25315 async move {
25316 Ok(lsp::CompletionItem {
25317 additional_text_edits: edits,
25318 ..Default::default()
25319 })
25320 }
25321 });
25322
25323 async move {
25324 request.next().await;
25325 }
25326}
25327
25328pub(crate) fn update_test_language_settings(
25329 cx: &mut TestAppContext,
25330 f: impl Fn(&mut AllLanguageSettingsContent),
25331) {
25332 cx.update(|cx| {
25333 SettingsStore::update_global(cx, |store, cx| {
25334 store.update_user_settings(cx, |settings| f(&mut settings.project.all_languages));
25335 });
25336 });
25337}
25338
25339pub(crate) fn update_test_project_settings(
25340 cx: &mut TestAppContext,
25341 f: impl Fn(&mut ProjectSettingsContent),
25342) {
25343 cx.update(|cx| {
25344 SettingsStore::update_global(cx, |store, cx| {
25345 store.update_user_settings(cx, |settings| f(&mut settings.project));
25346 });
25347 });
25348}
25349
25350pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
25351 cx.update(|cx| {
25352 assets::Assets.load_test_fonts(cx);
25353 let store = SettingsStore::test(cx);
25354 cx.set_global(store);
25355 theme::init(theme::LoadThemes::JustBase, cx);
25356 release_channel::init(SemanticVersion::default(), cx);
25357 client::init_settings(cx);
25358 language::init(cx);
25359 Project::init_settings(cx);
25360 workspace::init_settings(cx);
25361 crate::init(cx);
25362 });
25363 zlog::init_test();
25364 update_test_language_settings(cx, f);
25365}
25366
25367#[track_caller]
25368fn assert_hunk_revert(
25369 not_reverted_text_with_selections: &str,
25370 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
25371 expected_reverted_text_with_selections: &str,
25372 base_text: &str,
25373 cx: &mut EditorLspTestContext,
25374) {
25375 cx.set_state(not_reverted_text_with_selections);
25376 cx.set_head_text(base_text);
25377 cx.executor().run_until_parked();
25378
25379 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
25380 let snapshot = editor.snapshot(window, cx);
25381 let reverted_hunk_statuses = snapshot
25382 .buffer_snapshot()
25383 .diff_hunks_in_range(0..snapshot.buffer_snapshot().len())
25384 .map(|hunk| hunk.status().kind)
25385 .collect::<Vec<_>>();
25386
25387 editor.git_restore(&Default::default(), window, cx);
25388 reverted_hunk_statuses
25389 });
25390 cx.executor().run_until_parked();
25391 cx.assert_editor_state(expected_reverted_text_with_selections);
25392 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
25393}
25394
25395#[gpui::test(iterations = 10)]
25396async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
25397 init_test(cx, |_| {});
25398
25399 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
25400 let counter = diagnostic_requests.clone();
25401
25402 let fs = FakeFs::new(cx.executor());
25403 fs.insert_tree(
25404 path!("/a"),
25405 json!({
25406 "first.rs": "fn main() { let a = 5; }",
25407 "second.rs": "// Test file",
25408 }),
25409 )
25410 .await;
25411
25412 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25413 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25414 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25415
25416 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25417 language_registry.add(rust_lang());
25418 let mut fake_servers = language_registry.register_fake_lsp(
25419 "Rust",
25420 FakeLspAdapter {
25421 capabilities: lsp::ServerCapabilities {
25422 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
25423 lsp::DiagnosticOptions {
25424 identifier: None,
25425 inter_file_dependencies: true,
25426 workspace_diagnostics: true,
25427 work_done_progress_options: Default::default(),
25428 },
25429 )),
25430 ..Default::default()
25431 },
25432 ..Default::default()
25433 },
25434 );
25435
25436 let editor = workspace
25437 .update(cx, |workspace, window, cx| {
25438 workspace.open_abs_path(
25439 PathBuf::from(path!("/a/first.rs")),
25440 OpenOptions::default(),
25441 window,
25442 cx,
25443 )
25444 })
25445 .unwrap()
25446 .await
25447 .unwrap()
25448 .downcast::<Editor>()
25449 .unwrap();
25450 let fake_server = fake_servers.next().await.unwrap();
25451 let server_id = fake_server.server.server_id();
25452 let mut first_request = fake_server
25453 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
25454 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
25455 let result_id = Some(new_result_id.to_string());
25456 assert_eq!(
25457 params.text_document.uri,
25458 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25459 );
25460 async move {
25461 Ok(lsp::DocumentDiagnosticReportResult::Report(
25462 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
25463 related_documents: None,
25464 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
25465 items: Vec::new(),
25466 result_id,
25467 },
25468 }),
25469 ))
25470 }
25471 });
25472
25473 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
25474 project.update(cx, |project, cx| {
25475 let buffer_id = editor
25476 .read(cx)
25477 .buffer()
25478 .read(cx)
25479 .as_singleton()
25480 .expect("created a singleton buffer")
25481 .read(cx)
25482 .remote_id();
25483 let buffer_result_id = project
25484 .lsp_store()
25485 .read(cx)
25486 .result_id(server_id, buffer_id, cx);
25487 assert_eq!(expected, buffer_result_id);
25488 });
25489 };
25490
25491 ensure_result_id(None, cx);
25492 cx.executor().advance_clock(Duration::from_millis(60));
25493 cx.executor().run_until_parked();
25494 assert_eq!(
25495 diagnostic_requests.load(atomic::Ordering::Acquire),
25496 1,
25497 "Opening file should trigger diagnostic request"
25498 );
25499 first_request
25500 .next()
25501 .await
25502 .expect("should have sent the first diagnostics pull request");
25503 ensure_result_id(Some("1".to_string()), cx);
25504
25505 // Editing should trigger diagnostics
25506 editor.update_in(cx, |editor, window, cx| {
25507 editor.handle_input("2", window, cx)
25508 });
25509 cx.executor().advance_clock(Duration::from_millis(60));
25510 cx.executor().run_until_parked();
25511 assert_eq!(
25512 diagnostic_requests.load(atomic::Ordering::Acquire),
25513 2,
25514 "Editing should trigger diagnostic request"
25515 );
25516 ensure_result_id(Some("2".to_string()), cx);
25517
25518 // Moving cursor should not trigger diagnostic request
25519 editor.update_in(cx, |editor, window, cx| {
25520 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25521 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
25522 });
25523 });
25524 cx.executor().advance_clock(Duration::from_millis(60));
25525 cx.executor().run_until_parked();
25526 assert_eq!(
25527 diagnostic_requests.load(atomic::Ordering::Acquire),
25528 2,
25529 "Cursor movement should not trigger diagnostic request"
25530 );
25531 ensure_result_id(Some("2".to_string()), cx);
25532 // Multiple rapid edits should be debounced
25533 for _ in 0..5 {
25534 editor.update_in(cx, |editor, window, cx| {
25535 editor.handle_input("x", window, cx)
25536 });
25537 }
25538 cx.executor().advance_clock(Duration::from_millis(60));
25539 cx.executor().run_until_parked();
25540
25541 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
25542 assert!(
25543 final_requests <= 4,
25544 "Multiple rapid edits should be debounced (got {final_requests} requests)",
25545 );
25546 ensure_result_id(Some(final_requests.to_string()), cx);
25547}
25548
25549#[gpui::test]
25550async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
25551 // Regression test for issue #11671
25552 // Previously, adding a cursor after moving multiple cursors would reset
25553 // the cursor count instead of adding to the existing cursors.
25554 init_test(cx, |_| {});
25555 let mut cx = EditorTestContext::new(cx).await;
25556
25557 // Create a simple buffer with cursor at start
25558 cx.set_state(indoc! {"
25559 ˇaaaa
25560 bbbb
25561 cccc
25562 dddd
25563 eeee
25564 ffff
25565 gggg
25566 hhhh"});
25567
25568 // Add 2 cursors below (so we have 3 total)
25569 cx.update_editor(|editor, window, cx| {
25570 editor.add_selection_below(&Default::default(), window, cx);
25571 editor.add_selection_below(&Default::default(), window, cx);
25572 });
25573
25574 // Verify we have 3 cursors
25575 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
25576 assert_eq!(
25577 initial_count, 3,
25578 "Should have 3 cursors after adding 2 below"
25579 );
25580
25581 // Move down one line
25582 cx.update_editor(|editor, window, cx| {
25583 editor.move_down(&MoveDown, window, cx);
25584 });
25585
25586 // Add another cursor below
25587 cx.update_editor(|editor, window, cx| {
25588 editor.add_selection_below(&Default::default(), window, cx);
25589 });
25590
25591 // Should now have 4 cursors (3 original + 1 new)
25592 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
25593 assert_eq!(
25594 final_count, 4,
25595 "Should have 4 cursors after moving and adding another"
25596 );
25597}
25598
25599#[gpui::test(iterations = 10)]
25600async fn test_document_colors(cx: &mut TestAppContext) {
25601 let expected_color = Rgba {
25602 r: 0.33,
25603 g: 0.33,
25604 b: 0.33,
25605 a: 0.33,
25606 };
25607
25608 init_test(cx, |_| {});
25609
25610 let fs = FakeFs::new(cx.executor());
25611 fs.insert_tree(
25612 path!("/a"),
25613 json!({
25614 "first.rs": "fn main() { let a = 5; }",
25615 }),
25616 )
25617 .await;
25618
25619 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25620 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25621 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25622
25623 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25624 language_registry.add(rust_lang());
25625 let mut fake_servers = language_registry.register_fake_lsp(
25626 "Rust",
25627 FakeLspAdapter {
25628 capabilities: lsp::ServerCapabilities {
25629 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
25630 ..lsp::ServerCapabilities::default()
25631 },
25632 name: "rust-analyzer",
25633 ..FakeLspAdapter::default()
25634 },
25635 );
25636 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
25637 "Rust",
25638 FakeLspAdapter {
25639 capabilities: lsp::ServerCapabilities {
25640 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
25641 ..lsp::ServerCapabilities::default()
25642 },
25643 name: "not-rust-analyzer",
25644 ..FakeLspAdapter::default()
25645 },
25646 );
25647
25648 let editor = workspace
25649 .update(cx, |workspace, window, cx| {
25650 workspace.open_abs_path(
25651 PathBuf::from(path!("/a/first.rs")),
25652 OpenOptions::default(),
25653 window,
25654 cx,
25655 )
25656 })
25657 .unwrap()
25658 .await
25659 .unwrap()
25660 .downcast::<Editor>()
25661 .unwrap();
25662 let fake_language_server = fake_servers.next().await.unwrap();
25663 let fake_language_server_without_capabilities =
25664 fake_servers_without_capabilities.next().await.unwrap();
25665 let requests_made = Arc::new(AtomicUsize::new(0));
25666 let closure_requests_made = Arc::clone(&requests_made);
25667 let mut color_request_handle = fake_language_server
25668 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
25669 let requests_made = Arc::clone(&closure_requests_made);
25670 async move {
25671 assert_eq!(
25672 params.text_document.uri,
25673 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25674 );
25675 requests_made.fetch_add(1, atomic::Ordering::Release);
25676 Ok(vec![
25677 lsp::ColorInformation {
25678 range: lsp::Range {
25679 start: lsp::Position {
25680 line: 0,
25681 character: 0,
25682 },
25683 end: lsp::Position {
25684 line: 0,
25685 character: 1,
25686 },
25687 },
25688 color: lsp::Color {
25689 red: 0.33,
25690 green: 0.33,
25691 blue: 0.33,
25692 alpha: 0.33,
25693 },
25694 },
25695 lsp::ColorInformation {
25696 range: lsp::Range {
25697 start: lsp::Position {
25698 line: 0,
25699 character: 0,
25700 },
25701 end: lsp::Position {
25702 line: 0,
25703 character: 1,
25704 },
25705 },
25706 color: lsp::Color {
25707 red: 0.33,
25708 green: 0.33,
25709 blue: 0.33,
25710 alpha: 0.33,
25711 },
25712 },
25713 ])
25714 }
25715 });
25716
25717 let _handle = fake_language_server_without_capabilities
25718 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
25719 panic!("Should not be called");
25720 });
25721 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
25722 color_request_handle.next().await.unwrap();
25723 cx.run_until_parked();
25724 assert_eq!(
25725 1,
25726 requests_made.load(atomic::Ordering::Acquire),
25727 "Should query for colors once per editor open"
25728 );
25729 editor.update_in(cx, |editor, _, cx| {
25730 assert_eq!(
25731 vec![expected_color],
25732 extract_color_inlays(editor, cx),
25733 "Should have an initial inlay"
25734 );
25735 });
25736
25737 // opening another file in a split should not influence the LSP query counter
25738 workspace
25739 .update(cx, |workspace, window, cx| {
25740 assert_eq!(
25741 workspace.panes().len(),
25742 1,
25743 "Should have one pane with one editor"
25744 );
25745 workspace.move_item_to_pane_in_direction(
25746 &MoveItemToPaneInDirection {
25747 direction: SplitDirection::Right,
25748 focus: false,
25749 clone: true,
25750 },
25751 window,
25752 cx,
25753 );
25754 })
25755 .unwrap();
25756 cx.run_until_parked();
25757 workspace
25758 .update(cx, |workspace, _, cx| {
25759 let panes = workspace.panes();
25760 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
25761 for pane in panes {
25762 let editor = pane
25763 .read(cx)
25764 .active_item()
25765 .and_then(|item| item.downcast::<Editor>())
25766 .expect("Should have opened an editor in each split");
25767 let editor_file = editor
25768 .read(cx)
25769 .buffer()
25770 .read(cx)
25771 .as_singleton()
25772 .expect("test deals with singleton buffers")
25773 .read(cx)
25774 .file()
25775 .expect("test buffese should have a file")
25776 .path();
25777 assert_eq!(
25778 editor_file.as_ref(),
25779 rel_path("first.rs"),
25780 "Both editors should be opened for the same file"
25781 )
25782 }
25783 })
25784 .unwrap();
25785
25786 cx.executor().advance_clock(Duration::from_millis(500));
25787 let save = editor.update_in(cx, |editor, window, cx| {
25788 editor.move_to_end(&MoveToEnd, window, cx);
25789 editor.handle_input("dirty", window, cx);
25790 editor.save(
25791 SaveOptions {
25792 format: true,
25793 autosave: true,
25794 },
25795 project.clone(),
25796 window,
25797 cx,
25798 )
25799 });
25800 save.await.unwrap();
25801
25802 color_request_handle.next().await.unwrap();
25803 cx.run_until_parked();
25804 assert_eq!(
25805 2,
25806 requests_made.load(atomic::Ordering::Acquire),
25807 "Should query for colors once per save (deduplicated) and once per formatting after save"
25808 );
25809
25810 drop(editor);
25811 let close = workspace
25812 .update(cx, |workspace, window, cx| {
25813 workspace.active_pane().update(cx, |pane, cx| {
25814 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25815 })
25816 })
25817 .unwrap();
25818 close.await.unwrap();
25819 let close = workspace
25820 .update(cx, |workspace, window, cx| {
25821 workspace.active_pane().update(cx, |pane, cx| {
25822 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25823 })
25824 })
25825 .unwrap();
25826 close.await.unwrap();
25827 assert_eq!(
25828 2,
25829 requests_made.load(atomic::Ordering::Acquire),
25830 "After saving and closing all editors, no extra requests should be made"
25831 );
25832 workspace
25833 .update(cx, |workspace, _, cx| {
25834 assert!(
25835 workspace.active_item(cx).is_none(),
25836 "Should close all editors"
25837 )
25838 })
25839 .unwrap();
25840
25841 workspace
25842 .update(cx, |workspace, window, cx| {
25843 workspace.active_pane().update(cx, |pane, cx| {
25844 pane.navigate_backward(&workspace::GoBack, window, cx);
25845 })
25846 })
25847 .unwrap();
25848 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
25849 cx.run_until_parked();
25850 let editor = workspace
25851 .update(cx, |workspace, _, cx| {
25852 workspace
25853 .active_item(cx)
25854 .expect("Should have reopened the editor again after navigating back")
25855 .downcast::<Editor>()
25856 .expect("Should be an editor")
25857 })
25858 .unwrap();
25859
25860 assert_eq!(
25861 2,
25862 requests_made.load(atomic::Ordering::Acquire),
25863 "Cache should be reused on buffer close and reopen"
25864 );
25865 editor.update(cx, |editor, cx| {
25866 assert_eq!(
25867 vec![expected_color],
25868 extract_color_inlays(editor, cx),
25869 "Should have an initial inlay"
25870 );
25871 });
25872
25873 drop(color_request_handle);
25874 let closure_requests_made = Arc::clone(&requests_made);
25875 let mut empty_color_request_handle = fake_language_server
25876 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
25877 let requests_made = Arc::clone(&closure_requests_made);
25878 async move {
25879 assert_eq!(
25880 params.text_document.uri,
25881 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25882 );
25883 requests_made.fetch_add(1, atomic::Ordering::Release);
25884 Ok(Vec::new())
25885 }
25886 });
25887 let save = editor.update_in(cx, |editor, window, cx| {
25888 editor.move_to_end(&MoveToEnd, window, cx);
25889 editor.handle_input("dirty_again", window, cx);
25890 editor.save(
25891 SaveOptions {
25892 format: false,
25893 autosave: true,
25894 },
25895 project.clone(),
25896 window,
25897 cx,
25898 )
25899 });
25900 save.await.unwrap();
25901
25902 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
25903 empty_color_request_handle.next().await.unwrap();
25904 cx.run_until_parked();
25905 assert_eq!(
25906 3,
25907 requests_made.load(atomic::Ordering::Acquire),
25908 "Should query for colors once per save only, as formatting was not requested"
25909 );
25910 editor.update(cx, |editor, cx| {
25911 assert_eq!(
25912 Vec::<Rgba>::new(),
25913 extract_color_inlays(editor, cx),
25914 "Should clear all colors when the server returns an empty response"
25915 );
25916 });
25917}
25918
25919#[gpui::test]
25920async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
25921 init_test(cx, |_| {});
25922 let (editor, cx) = cx.add_window_view(Editor::single_line);
25923 editor.update_in(cx, |editor, window, cx| {
25924 editor.set_text("oops\n\nwow\n", window, cx)
25925 });
25926 cx.run_until_parked();
25927 editor.update(cx, |editor, cx| {
25928 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
25929 });
25930 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
25931 cx.run_until_parked();
25932 editor.update(cx, |editor, cx| {
25933 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
25934 });
25935}
25936
25937#[gpui::test]
25938async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
25939 init_test(cx, |_| {});
25940
25941 cx.update(|cx| {
25942 register_project_item::<Editor>(cx);
25943 });
25944
25945 let fs = FakeFs::new(cx.executor());
25946 fs.insert_tree("/root1", json!({})).await;
25947 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
25948 .await;
25949
25950 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
25951 let (workspace, cx) =
25952 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
25953
25954 let worktree_id = project.update(cx, |project, cx| {
25955 project.worktrees(cx).next().unwrap().read(cx).id()
25956 });
25957
25958 let handle = workspace
25959 .update_in(cx, |workspace, window, cx| {
25960 let project_path = (worktree_id, rel_path("one.pdf"));
25961 workspace.open_path(project_path, None, true, window, cx)
25962 })
25963 .await
25964 .unwrap();
25965
25966 assert_eq!(
25967 handle.to_any().entity_type(),
25968 TypeId::of::<InvalidBufferView>()
25969 );
25970}
25971
25972#[gpui::test]
25973async fn test_select_next_prev_syntax_node(cx: &mut TestAppContext) {
25974 init_test(cx, |_| {});
25975
25976 let language = Arc::new(Language::new(
25977 LanguageConfig::default(),
25978 Some(tree_sitter_rust::LANGUAGE.into()),
25979 ));
25980
25981 // Test hierarchical sibling navigation
25982 let text = r#"
25983 fn outer() {
25984 if condition {
25985 let a = 1;
25986 }
25987 let b = 2;
25988 }
25989
25990 fn another() {
25991 let c = 3;
25992 }
25993 "#;
25994
25995 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
25996 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
25997 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
25998
25999 // Wait for parsing to complete
26000 editor
26001 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
26002 .await;
26003
26004 editor.update_in(cx, |editor, window, cx| {
26005 // Start by selecting "let a = 1;" inside the if block
26006 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26007 s.select_display_ranges([
26008 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 26)
26009 ]);
26010 });
26011
26012 let initial_selection = editor.selections.display_ranges(cx);
26013 assert_eq!(initial_selection.len(), 1, "Should have one selection");
26014
26015 // Test select next sibling - should move up levels to find the next sibling
26016 // Since "let a = 1;" has no siblings in the if block, it should move up
26017 // to find "let b = 2;" which is a sibling of the if block
26018 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
26019 let next_selection = editor.selections.display_ranges(cx);
26020
26021 // Should have a selection and it should be different from the initial
26022 assert_eq!(
26023 next_selection.len(),
26024 1,
26025 "Should have one selection after next"
26026 );
26027 assert_ne!(
26028 next_selection[0], initial_selection[0],
26029 "Next sibling selection should be different"
26030 );
26031
26032 // Test hierarchical navigation by going to the end of the current function
26033 // and trying to navigate to the next function
26034 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26035 s.select_display_ranges([
26036 DisplayPoint::new(DisplayRow(5), 12)..DisplayPoint::new(DisplayRow(5), 22)
26037 ]);
26038 });
26039
26040 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
26041 let function_next_selection = editor.selections.display_ranges(cx);
26042
26043 // Should move to the next function
26044 assert_eq!(
26045 function_next_selection.len(),
26046 1,
26047 "Should have one selection after function next"
26048 );
26049
26050 // Test select previous sibling navigation
26051 editor.select_prev_syntax_node(&SelectPreviousSyntaxNode, window, cx);
26052 let prev_selection = editor.selections.display_ranges(cx);
26053
26054 // Should have a selection and it should be different
26055 assert_eq!(
26056 prev_selection.len(),
26057 1,
26058 "Should have one selection after prev"
26059 );
26060 assert_ne!(
26061 prev_selection[0], function_next_selection[0],
26062 "Previous sibling selection should be different from next"
26063 );
26064 });
26065}
26066
26067#[gpui::test]
26068async fn test_next_prev_document_highlight(cx: &mut TestAppContext) {
26069 init_test(cx, |_| {});
26070
26071 let mut cx = EditorTestContext::new(cx).await;
26072 cx.set_state(
26073 "let ˇvariable = 42;
26074let another = variable + 1;
26075let result = variable * 2;",
26076 );
26077
26078 // Set up document highlights manually (simulating LSP response)
26079 cx.update_editor(|editor, _window, cx| {
26080 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
26081
26082 // Create highlights for "variable" occurrences
26083 let highlight_ranges = [
26084 Point::new(0, 4)..Point::new(0, 12), // First "variable"
26085 Point::new(1, 14)..Point::new(1, 22), // Second "variable"
26086 Point::new(2, 13)..Point::new(2, 21), // Third "variable"
26087 ];
26088
26089 let anchor_ranges: Vec<_> = highlight_ranges
26090 .iter()
26091 .map(|range| range.clone().to_anchors(&buffer_snapshot))
26092 .collect();
26093
26094 editor.highlight_background::<DocumentHighlightRead>(
26095 &anchor_ranges,
26096 |theme| theme.colors().editor_document_highlight_read_background,
26097 cx,
26098 );
26099 });
26100
26101 // Go to next highlight - should move to second "variable"
26102 cx.update_editor(|editor, window, cx| {
26103 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26104 });
26105 cx.assert_editor_state(
26106 "let variable = 42;
26107let another = ˇvariable + 1;
26108let result = variable * 2;",
26109 );
26110
26111 // Go to next highlight - should move to third "variable"
26112 cx.update_editor(|editor, window, cx| {
26113 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26114 });
26115 cx.assert_editor_state(
26116 "let variable = 42;
26117let another = variable + 1;
26118let result = ˇvariable * 2;",
26119 );
26120
26121 // Go to next highlight - should stay at third "variable" (no wrap-around)
26122 cx.update_editor(|editor, window, cx| {
26123 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26124 });
26125 cx.assert_editor_state(
26126 "let variable = 42;
26127let another = variable + 1;
26128let result = ˇvariable * 2;",
26129 );
26130
26131 // Now test going backwards from third position
26132 cx.update_editor(|editor, window, cx| {
26133 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26134 });
26135 cx.assert_editor_state(
26136 "let variable = 42;
26137let another = ˇvariable + 1;
26138let result = variable * 2;",
26139 );
26140
26141 // Go to previous highlight - should move to first "variable"
26142 cx.update_editor(|editor, window, cx| {
26143 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26144 });
26145 cx.assert_editor_state(
26146 "let ˇvariable = 42;
26147let another = variable + 1;
26148let result = variable * 2;",
26149 );
26150
26151 // Go to previous highlight - should stay on first "variable"
26152 cx.update_editor(|editor, window, cx| {
26153 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26154 });
26155 cx.assert_editor_state(
26156 "let ˇvariable = 42;
26157let another = variable + 1;
26158let result = variable * 2;",
26159 );
26160}
26161
26162#[gpui::test]
26163async fn test_paste_url_from_other_app_creates_markdown_link_over_selected_text(
26164 cx: &mut gpui::TestAppContext,
26165) {
26166 init_test(cx, |_| {});
26167
26168 let url = "https://zed.dev";
26169
26170 let markdown_language = Arc::new(Language::new(
26171 LanguageConfig {
26172 name: "Markdown".into(),
26173 ..LanguageConfig::default()
26174 },
26175 None,
26176 ));
26177
26178 let mut cx = EditorTestContext::new(cx).await;
26179 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26180 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)");
26181
26182 cx.update_editor(|editor, window, cx| {
26183 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26184 editor.paste(&Paste, window, cx);
26185 });
26186
26187 cx.assert_editor_state(&format!(
26188 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)"
26189 ));
26190}
26191
26192#[gpui::test]
26193async fn test_paste_url_from_zed_copy_creates_markdown_link_over_selected_text(
26194 cx: &mut gpui::TestAppContext,
26195) {
26196 init_test(cx, |_| {});
26197
26198 let url = "https://zed.dev";
26199
26200 let markdown_language = Arc::new(Language::new(
26201 LanguageConfig {
26202 name: "Markdown".into(),
26203 ..LanguageConfig::default()
26204 },
26205 None,
26206 ));
26207
26208 let mut cx = EditorTestContext::new(cx).await;
26209 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26210 cx.set_state(&format!(
26211 "Hello, editor.\nZed is great (see this link: )\n«{url}ˇ»"
26212 ));
26213
26214 cx.update_editor(|editor, window, cx| {
26215 editor.copy(&Copy, window, cx);
26216 });
26217
26218 cx.set_state(&format!(
26219 "Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)\n{url}"
26220 ));
26221
26222 cx.update_editor(|editor, window, cx| {
26223 editor.paste(&Paste, window, cx);
26224 });
26225
26226 cx.assert_editor_state(&format!(
26227 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)\n{url}"
26228 ));
26229}
26230
26231#[gpui::test]
26232async fn test_paste_url_from_other_app_replaces_existing_url_without_creating_markdown_link(
26233 cx: &mut gpui::TestAppContext,
26234) {
26235 init_test(cx, |_| {});
26236
26237 let url = "https://zed.dev";
26238
26239 let markdown_language = Arc::new(Language::new(
26240 LanguageConfig {
26241 name: "Markdown".into(),
26242 ..LanguageConfig::default()
26243 },
26244 None,
26245 ));
26246
26247 let mut cx = EditorTestContext::new(cx).await;
26248 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26249 cx.set_state("Please visit zed's homepage: «https://www.apple.comˇ»");
26250
26251 cx.update_editor(|editor, window, cx| {
26252 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26253 editor.paste(&Paste, window, cx);
26254 });
26255
26256 cx.assert_editor_state(&format!("Please visit zed's homepage: {url}ˇ"));
26257}
26258
26259#[gpui::test]
26260async fn test_paste_plain_text_from_other_app_replaces_selection_without_creating_markdown_link(
26261 cx: &mut gpui::TestAppContext,
26262) {
26263 init_test(cx, |_| {});
26264
26265 let text = "Awesome";
26266
26267 let markdown_language = Arc::new(Language::new(
26268 LanguageConfig {
26269 name: "Markdown".into(),
26270 ..LanguageConfig::default()
26271 },
26272 None,
26273 ));
26274
26275 let mut cx = EditorTestContext::new(cx).await;
26276 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26277 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat»");
26278
26279 cx.update_editor(|editor, window, cx| {
26280 cx.write_to_clipboard(ClipboardItem::new_string(text.to_string()));
26281 editor.paste(&Paste, window, cx);
26282 });
26283
26284 cx.assert_editor_state(&format!("Hello, {text}ˇ.\nZed is {text}ˇ"));
26285}
26286
26287#[gpui::test]
26288async fn test_paste_url_from_other_app_without_creating_markdown_link_in_non_markdown_language(
26289 cx: &mut gpui::TestAppContext,
26290) {
26291 init_test(cx, |_| {});
26292
26293 let url = "https://zed.dev";
26294
26295 let markdown_language = Arc::new(Language::new(
26296 LanguageConfig {
26297 name: "Rust".into(),
26298 ..LanguageConfig::default()
26299 },
26300 None,
26301 ));
26302
26303 let mut cx = EditorTestContext::new(cx).await;
26304 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26305 cx.set_state("// Hello, «editorˇ».\n// Zed is «ˇgreat» (see this link: ˇ)");
26306
26307 cx.update_editor(|editor, window, cx| {
26308 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26309 editor.paste(&Paste, window, cx);
26310 });
26311
26312 cx.assert_editor_state(&format!(
26313 "// Hello, {url}ˇ.\n// Zed is {url}ˇ (see this link: {url}ˇ)"
26314 ));
26315}
26316
26317#[gpui::test]
26318async fn test_paste_url_from_other_app_creates_markdown_link_selectively_in_multi_buffer(
26319 cx: &mut TestAppContext,
26320) {
26321 init_test(cx, |_| {});
26322
26323 let url = "https://zed.dev";
26324
26325 let markdown_language = Arc::new(Language::new(
26326 LanguageConfig {
26327 name: "Markdown".into(),
26328 ..LanguageConfig::default()
26329 },
26330 None,
26331 ));
26332
26333 let (editor, cx) = cx.add_window_view(|window, cx| {
26334 let multi_buffer = MultiBuffer::build_multi(
26335 [
26336 ("this will embed -> link", vec![Point::row_range(0..1)]),
26337 ("this will replace -> link", vec![Point::row_range(0..1)]),
26338 ],
26339 cx,
26340 );
26341 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
26342 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26343 s.select_ranges(vec![
26344 Point::new(0, 19)..Point::new(0, 23),
26345 Point::new(1, 21)..Point::new(1, 25),
26346 ])
26347 });
26348 let first_buffer_id = multi_buffer
26349 .read(cx)
26350 .excerpt_buffer_ids()
26351 .into_iter()
26352 .next()
26353 .unwrap();
26354 let first_buffer = multi_buffer.read(cx).buffer(first_buffer_id).unwrap();
26355 first_buffer.update(cx, |buffer, cx| {
26356 buffer.set_language(Some(markdown_language.clone()), cx);
26357 });
26358
26359 editor
26360 });
26361 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
26362
26363 cx.update_editor(|editor, window, cx| {
26364 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26365 editor.paste(&Paste, window, cx);
26366 });
26367
26368 cx.assert_editor_state(&format!(
26369 "this will embed -> [link]({url})ˇ\nthis will replace -> {url}ˇ"
26370 ));
26371}
26372
26373#[gpui::test]
26374async fn test_race_in_multibuffer_save(cx: &mut TestAppContext) {
26375 init_test(cx, |_| {});
26376
26377 let fs = FakeFs::new(cx.executor());
26378 fs.insert_tree(
26379 path!("/project"),
26380 json!({
26381 "first.rs": "# First Document\nSome content here.",
26382 "second.rs": "Plain text content for second file.",
26383 }),
26384 )
26385 .await;
26386
26387 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
26388 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
26389 let cx = &mut VisualTestContext::from_window(*workspace, cx);
26390
26391 let language = rust_lang();
26392 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
26393 language_registry.add(language.clone());
26394 let mut fake_servers = language_registry.register_fake_lsp(
26395 "Rust",
26396 FakeLspAdapter {
26397 ..FakeLspAdapter::default()
26398 },
26399 );
26400
26401 let buffer1 = project
26402 .update(cx, |project, cx| {
26403 project.open_local_buffer(PathBuf::from(path!("/project/first.rs")), cx)
26404 })
26405 .await
26406 .unwrap();
26407 let buffer2 = project
26408 .update(cx, |project, cx| {
26409 project.open_local_buffer(PathBuf::from(path!("/project/second.rs")), cx)
26410 })
26411 .await
26412 .unwrap();
26413
26414 let multi_buffer = cx.new(|cx| {
26415 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
26416 multi_buffer.set_excerpts_for_path(
26417 PathKey::for_buffer(&buffer1, cx),
26418 buffer1.clone(),
26419 [Point::zero()..buffer1.read(cx).max_point()],
26420 3,
26421 cx,
26422 );
26423 multi_buffer.set_excerpts_for_path(
26424 PathKey::for_buffer(&buffer2, cx),
26425 buffer2.clone(),
26426 [Point::zero()..buffer1.read(cx).max_point()],
26427 3,
26428 cx,
26429 );
26430 multi_buffer
26431 });
26432
26433 let (editor, cx) = cx.add_window_view(|window, cx| {
26434 Editor::new(
26435 EditorMode::full(),
26436 multi_buffer,
26437 Some(project.clone()),
26438 window,
26439 cx,
26440 )
26441 });
26442
26443 let fake_language_server = fake_servers.next().await.unwrap();
26444
26445 buffer1.update(cx, |buffer, cx| buffer.edit([(0..0, "hello!")], None, cx));
26446
26447 let save = editor.update_in(cx, |editor, window, cx| {
26448 assert!(editor.is_dirty(cx));
26449
26450 editor.save(
26451 SaveOptions {
26452 format: true,
26453 autosave: true,
26454 },
26455 project,
26456 window,
26457 cx,
26458 )
26459 });
26460 let (start_edit_tx, start_edit_rx) = oneshot::channel();
26461 let (done_edit_tx, done_edit_rx) = oneshot::channel();
26462 let mut done_edit_rx = Some(done_edit_rx);
26463 let mut start_edit_tx = Some(start_edit_tx);
26464
26465 fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| {
26466 start_edit_tx.take().unwrap().send(()).unwrap();
26467 let done_edit_rx = done_edit_rx.take().unwrap();
26468 async move {
26469 done_edit_rx.await.unwrap();
26470 Ok(None)
26471 }
26472 });
26473
26474 start_edit_rx.await.unwrap();
26475 buffer2
26476 .update(cx, |buffer, cx| buffer.edit([(0..0, "world!")], None, cx))
26477 .unwrap();
26478
26479 done_edit_tx.send(()).unwrap();
26480
26481 save.await.unwrap();
26482 cx.update(|_, cx| assert!(editor.is_dirty(cx)));
26483}
26484
26485#[track_caller]
26486fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
26487 editor
26488 .all_inlays(cx)
26489 .into_iter()
26490 .filter_map(|inlay| inlay.get_color())
26491 .map(Rgba::from)
26492 .collect()
26493}