1use super::*;
2use crate::{
3 JoinLines,
4 code_context_menus::CodeContextMenu,
5 edit_prediction_tests::FakeEditPredictionProvider,
6 linked_editing_ranges::LinkedEditingRanges,
7 scroll::scroll_amount::ScrollAmount,
8 test::{
9 assert_text_with_selections, build_editor,
10 editor_lsp_test_context::{EditorLspTestContext, git_commit_lang},
11 editor_test_context::EditorTestContext,
12 select_ranges,
13 },
14};
15use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
16use futures::StreamExt;
17use gpui::{
18 BackgroundExecutor, DismissEvent, Rgba, SemanticVersion, TestAppContext, UpdateGlobal,
19 VisualTestContext, WindowBounds, WindowOptions, div,
20};
21use indoc::indoc;
22use language::{
23 BracketPairConfig,
24 Capability::ReadWrite,
25 DiagnosticSourceKind, FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher,
26 LanguageName, Override, Point,
27 language_settings::{
28 AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings, FormatterList,
29 LanguageSettingsContent, LspInsertMode, PrettierSettings, SelectedFormatter,
30 },
31 tree_sitter_python,
32};
33use language_settings::{Formatter, IndentGuideSettings};
34use lsp::CompletionParams;
35use multi_buffer::{IndentGuide, PathKey};
36use parking_lot::Mutex;
37use pretty_assertions::{assert_eq, assert_ne};
38use project::{
39 FakeFs,
40 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
41 project_settings::{LspSettings, ProjectSettings},
42};
43use serde_json::{self, json};
44use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
45use std::{
46 iter,
47 sync::atomic::{self, AtomicUsize},
48};
49use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
50use text::ToPoint as _;
51use unindent::Unindent;
52use util::{
53 assert_set_eq, path,
54 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
55 uri,
56};
57use workspace::{
58 CloseActiveItem, CloseAllItems, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry,
59 OpenOptions, ViewId,
60 invalid_buffer_view::InvalidBufferView,
61 item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
62 register_project_item,
63};
64
65#[gpui::test]
66fn test_edit_events(cx: &mut TestAppContext) {
67 init_test(cx, |_| {});
68
69 let buffer = cx.new(|cx| {
70 let mut buffer = language::Buffer::local("123456", cx);
71 buffer.set_group_interval(Duration::from_secs(1));
72 buffer
73 });
74
75 let events = Rc::new(RefCell::new(Vec::new()));
76 let editor1 = cx.add_window({
77 let events = events.clone();
78 |window, cx| {
79 let entity = cx.entity();
80 cx.subscribe_in(
81 &entity,
82 window,
83 move |_, _, event: &EditorEvent, _, _| match event {
84 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
85 EditorEvent::BufferEdited => {
86 events.borrow_mut().push(("editor1", "buffer edited"))
87 }
88 _ => {}
89 },
90 )
91 .detach();
92 Editor::for_buffer(buffer.clone(), None, window, cx)
93 }
94 });
95
96 let editor2 = cx.add_window({
97 let events = events.clone();
98 |window, cx| {
99 cx.subscribe_in(
100 &cx.entity(),
101 window,
102 move |_, _, event: &EditorEvent, _, _| match event {
103 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
104 EditorEvent::BufferEdited => {
105 events.borrow_mut().push(("editor2", "buffer edited"))
106 }
107 _ => {}
108 },
109 )
110 .detach();
111 Editor::for_buffer(buffer.clone(), None, window, cx)
112 }
113 });
114
115 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
116
117 // Mutating editor 1 will emit an `Edited` event only for that editor.
118 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
119 assert_eq!(
120 mem::take(&mut *events.borrow_mut()),
121 [
122 ("editor1", "edited"),
123 ("editor1", "buffer edited"),
124 ("editor2", "buffer edited"),
125 ]
126 );
127
128 // Mutating editor 2 will emit an `Edited` event only for that editor.
129 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
130 assert_eq!(
131 mem::take(&mut *events.borrow_mut()),
132 [
133 ("editor2", "edited"),
134 ("editor1", "buffer edited"),
135 ("editor2", "buffer edited"),
136 ]
137 );
138
139 // Undoing on editor 1 will emit an `Edited` event only for that editor.
140 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
141 assert_eq!(
142 mem::take(&mut *events.borrow_mut()),
143 [
144 ("editor1", "edited"),
145 ("editor1", "buffer edited"),
146 ("editor2", "buffer edited"),
147 ]
148 );
149
150 // Redoing on editor 1 will emit an `Edited` event only for that editor.
151 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
152 assert_eq!(
153 mem::take(&mut *events.borrow_mut()),
154 [
155 ("editor1", "edited"),
156 ("editor1", "buffer edited"),
157 ("editor2", "buffer edited"),
158 ]
159 );
160
161 // Undoing on editor 2 will emit an `Edited` event only for that editor.
162 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
163 assert_eq!(
164 mem::take(&mut *events.borrow_mut()),
165 [
166 ("editor2", "edited"),
167 ("editor1", "buffer edited"),
168 ("editor2", "buffer edited"),
169 ]
170 );
171
172 // Redoing on editor 2 will emit an `Edited` event only for that editor.
173 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
174 assert_eq!(
175 mem::take(&mut *events.borrow_mut()),
176 [
177 ("editor2", "edited"),
178 ("editor1", "buffer edited"),
179 ("editor2", "buffer edited"),
180 ]
181 );
182
183 // No event is emitted when the mutation is a no-op.
184 _ = editor2.update(cx, |editor, window, cx| {
185 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
186 s.select_ranges([0..0])
187 });
188
189 editor.backspace(&Backspace, window, cx);
190 });
191 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
192}
193
194#[gpui::test]
195fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
196 init_test(cx, |_| {});
197
198 let mut now = Instant::now();
199 let group_interval = Duration::from_millis(1);
200 let buffer = cx.new(|cx| {
201 let mut buf = language::Buffer::local("123456", cx);
202 buf.set_group_interval(group_interval);
203 buf
204 });
205 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
206 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
207
208 _ = editor.update(cx, |editor, window, cx| {
209 editor.start_transaction_at(now, window, cx);
210 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
211 s.select_ranges([2..4])
212 });
213
214 editor.insert("cd", window, cx);
215 editor.end_transaction_at(now, cx);
216 assert_eq!(editor.text(cx), "12cd56");
217 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
218
219 editor.start_transaction_at(now, window, cx);
220 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
221 s.select_ranges([4..5])
222 });
223 editor.insert("e", window, cx);
224 editor.end_transaction_at(now, cx);
225 assert_eq!(editor.text(cx), "12cde6");
226 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
227
228 now += group_interval + Duration::from_millis(1);
229 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
230 s.select_ranges([2..2])
231 });
232
233 // Simulate an edit in another editor
234 buffer.update(cx, |buffer, cx| {
235 buffer.start_transaction_at(now, cx);
236 buffer.edit([(0..1, "a")], None, cx);
237 buffer.edit([(1..1, "b")], None, cx);
238 buffer.end_transaction_at(now, cx);
239 });
240
241 assert_eq!(editor.text(cx), "ab2cde6");
242 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
243
244 // Last transaction happened past the group interval in a different editor.
245 // Undo it individually and don't restore selections.
246 editor.undo(&Undo, window, cx);
247 assert_eq!(editor.text(cx), "12cde6");
248 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
249
250 // First two transactions happened within the group interval in this editor.
251 // Undo them together and restore selections.
252 editor.undo(&Undo, window, cx);
253 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
254 assert_eq!(editor.text(cx), "123456");
255 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
256
257 // Redo the first two transactions together.
258 editor.redo(&Redo, window, cx);
259 assert_eq!(editor.text(cx), "12cde6");
260 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
261
262 // Redo the last transaction on its own.
263 editor.redo(&Redo, window, cx);
264 assert_eq!(editor.text(cx), "ab2cde6");
265 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
266
267 // Test empty transactions.
268 editor.start_transaction_at(now, window, cx);
269 editor.end_transaction_at(now, cx);
270 editor.undo(&Undo, window, cx);
271 assert_eq!(editor.text(cx), "12cde6");
272 });
273}
274
275#[gpui::test]
276fn test_ime_composition(cx: &mut TestAppContext) {
277 init_test(cx, |_| {});
278
279 let buffer = cx.new(|cx| {
280 let mut buffer = language::Buffer::local("abcde", cx);
281 // Ensure automatic grouping doesn't occur.
282 buffer.set_group_interval(Duration::ZERO);
283 buffer
284 });
285
286 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
287 cx.add_window(|window, cx| {
288 let mut editor = build_editor(buffer.clone(), window, cx);
289
290 // Start a new IME composition.
291 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
292 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
293 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
294 assert_eq!(editor.text(cx), "äbcde");
295 assert_eq!(
296 editor.marked_text_ranges(cx),
297 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
298 );
299
300 // Finalize IME composition.
301 editor.replace_text_in_range(None, "ā", window, cx);
302 assert_eq!(editor.text(cx), "ābcde");
303 assert_eq!(editor.marked_text_ranges(cx), None);
304
305 // IME composition edits are grouped and are undone/redone at once.
306 editor.undo(&Default::default(), window, cx);
307 assert_eq!(editor.text(cx), "abcde");
308 assert_eq!(editor.marked_text_ranges(cx), None);
309 editor.redo(&Default::default(), window, cx);
310 assert_eq!(editor.text(cx), "ābcde");
311 assert_eq!(editor.marked_text_ranges(cx), None);
312
313 // Start a new IME composition.
314 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
315 assert_eq!(
316 editor.marked_text_ranges(cx),
317 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
318 );
319
320 // Undoing during an IME composition cancels it.
321 editor.undo(&Default::default(), window, cx);
322 assert_eq!(editor.text(cx), "ābcde");
323 assert_eq!(editor.marked_text_ranges(cx), None);
324
325 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
326 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
327 assert_eq!(editor.text(cx), "ābcdè");
328 assert_eq!(
329 editor.marked_text_ranges(cx),
330 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
331 );
332
333 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
334 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
335 assert_eq!(editor.text(cx), "ābcdę");
336 assert_eq!(editor.marked_text_ranges(cx), None);
337
338 // Start a new IME composition with multiple cursors.
339 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
340 s.select_ranges([
341 OffsetUtf16(1)..OffsetUtf16(1),
342 OffsetUtf16(3)..OffsetUtf16(3),
343 OffsetUtf16(5)..OffsetUtf16(5),
344 ])
345 });
346 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
347 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
348 assert_eq!(
349 editor.marked_text_ranges(cx),
350 Some(vec![
351 OffsetUtf16(0)..OffsetUtf16(3),
352 OffsetUtf16(4)..OffsetUtf16(7),
353 OffsetUtf16(8)..OffsetUtf16(11)
354 ])
355 );
356
357 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
358 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
359 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
360 assert_eq!(
361 editor.marked_text_ranges(cx),
362 Some(vec![
363 OffsetUtf16(1)..OffsetUtf16(2),
364 OffsetUtf16(5)..OffsetUtf16(6),
365 OffsetUtf16(9)..OffsetUtf16(10)
366 ])
367 );
368
369 // Finalize IME composition with multiple cursors.
370 editor.replace_text_in_range(Some(9..10), "2", window, cx);
371 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
372 assert_eq!(editor.marked_text_ranges(cx), None);
373
374 editor
375 });
376}
377
378#[gpui::test]
379fn test_selection_with_mouse(cx: &mut TestAppContext) {
380 init_test(cx, |_| {});
381
382 let editor = cx.add_window(|window, cx| {
383 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
384 build_editor(buffer, window, cx)
385 });
386
387 _ = editor.update(cx, |editor, window, cx| {
388 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
389 });
390 assert_eq!(
391 editor
392 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
393 .unwrap(),
394 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
395 );
396
397 _ = editor.update(cx, |editor, window, cx| {
398 editor.update_selection(
399 DisplayPoint::new(DisplayRow(3), 3),
400 0,
401 gpui::Point::<f32>::default(),
402 window,
403 cx,
404 );
405 });
406
407 assert_eq!(
408 editor
409 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
410 .unwrap(),
411 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
412 );
413
414 _ = editor.update(cx, |editor, window, cx| {
415 editor.update_selection(
416 DisplayPoint::new(DisplayRow(1), 1),
417 0,
418 gpui::Point::<f32>::default(),
419 window,
420 cx,
421 );
422 });
423
424 assert_eq!(
425 editor
426 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
427 .unwrap(),
428 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
429 );
430
431 _ = editor.update(cx, |editor, window, cx| {
432 editor.end_selection(window, cx);
433 editor.update_selection(
434 DisplayPoint::new(DisplayRow(3), 3),
435 0,
436 gpui::Point::<f32>::default(),
437 window,
438 cx,
439 );
440 });
441
442 assert_eq!(
443 editor
444 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
445 .unwrap(),
446 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
447 );
448
449 _ = editor.update(cx, |editor, window, cx| {
450 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
451 editor.update_selection(
452 DisplayPoint::new(DisplayRow(0), 0),
453 0,
454 gpui::Point::<f32>::default(),
455 window,
456 cx,
457 );
458 });
459
460 assert_eq!(
461 editor
462 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
463 .unwrap(),
464 [
465 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
466 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
467 ]
468 );
469
470 _ = editor.update(cx, |editor, window, cx| {
471 editor.end_selection(window, cx);
472 });
473
474 assert_eq!(
475 editor
476 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
477 .unwrap(),
478 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
479 );
480}
481
482#[gpui::test]
483fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
484 init_test(cx, |_| {});
485
486 let editor = cx.add_window(|window, cx| {
487 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
488 build_editor(buffer, window, cx)
489 });
490
491 _ = editor.update(cx, |editor, window, cx| {
492 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
493 });
494
495 _ = editor.update(cx, |editor, window, cx| {
496 editor.end_selection(window, cx);
497 });
498
499 _ = editor.update(cx, |editor, window, cx| {
500 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
501 });
502
503 _ = editor.update(cx, |editor, window, cx| {
504 editor.end_selection(window, cx);
505 });
506
507 assert_eq!(
508 editor
509 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
510 .unwrap(),
511 [
512 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
513 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
514 ]
515 );
516
517 _ = editor.update(cx, |editor, window, cx| {
518 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
519 });
520
521 _ = editor.update(cx, |editor, window, cx| {
522 editor.end_selection(window, cx);
523 });
524
525 assert_eq!(
526 editor
527 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
528 .unwrap(),
529 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
530 );
531}
532
533#[gpui::test]
534fn test_canceling_pending_selection(cx: &mut TestAppContext) {
535 init_test(cx, |_| {});
536
537 let editor = cx.add_window(|window, cx| {
538 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
539 build_editor(buffer, window, cx)
540 });
541
542 _ = editor.update(cx, |editor, window, cx| {
543 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
544 assert_eq!(
545 editor.selections.display_ranges(cx),
546 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
547 );
548 });
549
550 _ = editor.update(cx, |editor, window, cx| {
551 editor.update_selection(
552 DisplayPoint::new(DisplayRow(3), 3),
553 0,
554 gpui::Point::<f32>::default(),
555 window,
556 cx,
557 );
558 assert_eq!(
559 editor.selections.display_ranges(cx),
560 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
561 );
562 });
563
564 _ = editor.update(cx, |editor, window, cx| {
565 editor.cancel(&Cancel, window, cx);
566 editor.update_selection(
567 DisplayPoint::new(DisplayRow(1), 1),
568 0,
569 gpui::Point::<f32>::default(),
570 window,
571 cx,
572 );
573 assert_eq!(
574 editor.selections.display_ranges(cx),
575 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
576 );
577 });
578}
579
580#[gpui::test]
581fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
582 init_test(cx, |_| {});
583
584 let editor = cx.add_window(|window, cx| {
585 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
586 build_editor(buffer, window, cx)
587 });
588
589 _ = editor.update(cx, |editor, window, cx| {
590 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
591 assert_eq!(
592 editor.selections.display_ranges(cx),
593 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
594 );
595
596 editor.move_down(&Default::default(), window, cx);
597 assert_eq!(
598 editor.selections.display_ranges(cx),
599 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
600 );
601
602 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
603 assert_eq!(
604 editor.selections.display_ranges(cx),
605 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
606 );
607
608 editor.move_up(&Default::default(), window, cx);
609 assert_eq!(
610 editor.selections.display_ranges(cx),
611 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
612 );
613 });
614}
615
616#[gpui::test]
617fn test_clone(cx: &mut TestAppContext) {
618 init_test(cx, |_| {});
619
620 let (text, selection_ranges) = marked_text_ranges(
621 indoc! {"
622 one
623 two
624 threeˇ
625 four
626 fiveˇ
627 "},
628 true,
629 );
630
631 let editor = cx.add_window(|window, cx| {
632 let buffer = MultiBuffer::build_simple(&text, cx);
633 build_editor(buffer, window, cx)
634 });
635
636 _ = editor.update(cx, |editor, window, cx| {
637 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
638 s.select_ranges(selection_ranges.clone())
639 });
640 editor.fold_creases(
641 vec![
642 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
643 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
644 ],
645 true,
646 window,
647 cx,
648 );
649 });
650
651 let cloned_editor = editor
652 .update(cx, |editor, _, cx| {
653 cx.open_window(Default::default(), |window, cx| {
654 cx.new(|cx| editor.clone(window, cx))
655 })
656 })
657 .unwrap()
658 .unwrap();
659
660 let snapshot = editor
661 .update(cx, |e, window, cx| e.snapshot(window, cx))
662 .unwrap();
663 let cloned_snapshot = cloned_editor
664 .update(cx, |e, window, cx| e.snapshot(window, cx))
665 .unwrap();
666
667 assert_eq!(
668 cloned_editor
669 .update(cx, |e, _, cx| e.display_text(cx))
670 .unwrap(),
671 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
672 );
673 assert_eq!(
674 cloned_snapshot
675 .folds_in_range(0..text.len())
676 .collect::<Vec<_>>(),
677 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
678 );
679 assert_set_eq!(
680 cloned_editor
681 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
682 .unwrap(),
683 editor
684 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
685 .unwrap()
686 );
687 assert_set_eq!(
688 cloned_editor
689 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
690 .unwrap(),
691 editor
692 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
693 .unwrap()
694 );
695}
696
697#[gpui::test]
698async fn test_navigation_history(cx: &mut TestAppContext) {
699 init_test(cx, |_| {});
700
701 use workspace::item::Item;
702
703 let fs = FakeFs::new(cx.executor());
704 let project = Project::test(fs, [], cx).await;
705 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
706 let pane = workspace
707 .update(cx, |workspace, _, _| workspace.active_pane().clone())
708 .unwrap();
709
710 _ = workspace.update(cx, |_v, window, cx| {
711 cx.new(|cx| {
712 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
713 let mut editor = build_editor(buffer, window, cx);
714 let handle = cx.entity();
715 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
716
717 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
718 editor.nav_history.as_mut().unwrap().pop_backward(cx)
719 }
720
721 // Move the cursor a small distance.
722 // Nothing is added to the navigation history.
723 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
724 s.select_display_ranges([
725 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
726 ])
727 });
728 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
729 s.select_display_ranges([
730 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
731 ])
732 });
733 assert!(pop_history(&mut editor, cx).is_none());
734
735 // Move the cursor a large distance.
736 // The history can jump back to the previous position.
737 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
738 s.select_display_ranges([
739 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
740 ])
741 });
742 let nav_entry = pop_history(&mut editor, cx).unwrap();
743 editor.navigate(nav_entry.data.unwrap(), window, cx);
744 assert_eq!(nav_entry.item.id(), cx.entity_id());
745 assert_eq!(
746 editor.selections.display_ranges(cx),
747 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
748 );
749 assert!(pop_history(&mut editor, cx).is_none());
750
751 // Move the cursor a small distance via the mouse.
752 // Nothing is added to the navigation history.
753 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
754 editor.end_selection(window, cx);
755 assert_eq!(
756 editor.selections.display_ranges(cx),
757 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
758 );
759 assert!(pop_history(&mut editor, cx).is_none());
760
761 // Move the cursor a large distance via the mouse.
762 // The history can jump back to the previous position.
763 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
764 editor.end_selection(window, cx);
765 assert_eq!(
766 editor.selections.display_ranges(cx),
767 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
768 );
769 let nav_entry = pop_history(&mut editor, cx).unwrap();
770 editor.navigate(nav_entry.data.unwrap(), window, cx);
771 assert_eq!(nav_entry.item.id(), cx.entity_id());
772 assert_eq!(
773 editor.selections.display_ranges(cx),
774 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
775 );
776 assert!(pop_history(&mut editor, cx).is_none());
777
778 // Set scroll position to check later
779 editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), window, cx);
780 let original_scroll_position = editor.scroll_manager.anchor();
781
782 // Jump to the end of the document and adjust scroll
783 editor.move_to_end(&MoveToEnd, window, cx);
784 editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), window, cx);
785 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
786
787 let nav_entry = pop_history(&mut editor, cx).unwrap();
788 editor.navigate(nav_entry.data.unwrap(), window, cx);
789 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
790
791 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
792 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
793 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
794 let invalid_point = Point::new(9999, 0);
795 editor.navigate(
796 Box::new(NavigationData {
797 cursor_anchor: invalid_anchor,
798 cursor_position: invalid_point,
799 scroll_anchor: ScrollAnchor {
800 anchor: invalid_anchor,
801 offset: Default::default(),
802 },
803 scroll_top_row: invalid_point.row,
804 }),
805 window,
806 cx,
807 );
808 assert_eq!(
809 editor.selections.display_ranges(cx),
810 &[editor.max_point(cx)..editor.max_point(cx)]
811 );
812 assert_eq!(
813 editor.scroll_position(cx),
814 gpui::Point::new(0., editor.max_point(cx).row().as_f32())
815 );
816
817 editor
818 })
819 });
820}
821
822#[gpui::test]
823fn test_cancel(cx: &mut TestAppContext) {
824 init_test(cx, |_| {});
825
826 let editor = cx.add_window(|window, cx| {
827 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
828 build_editor(buffer, window, cx)
829 });
830
831 _ = editor.update(cx, |editor, window, cx| {
832 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
833 editor.update_selection(
834 DisplayPoint::new(DisplayRow(1), 1),
835 0,
836 gpui::Point::<f32>::default(),
837 window,
838 cx,
839 );
840 editor.end_selection(window, cx);
841
842 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
843 editor.update_selection(
844 DisplayPoint::new(DisplayRow(0), 3),
845 0,
846 gpui::Point::<f32>::default(),
847 window,
848 cx,
849 );
850 editor.end_selection(window, cx);
851 assert_eq!(
852 editor.selections.display_ranges(cx),
853 [
854 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
855 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
856 ]
857 );
858 });
859
860 _ = editor.update(cx, |editor, window, cx| {
861 editor.cancel(&Cancel, window, cx);
862 assert_eq!(
863 editor.selections.display_ranges(cx),
864 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
865 );
866 });
867
868 _ = editor.update(cx, |editor, window, cx| {
869 editor.cancel(&Cancel, window, cx);
870 assert_eq!(
871 editor.selections.display_ranges(cx),
872 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
873 );
874 });
875}
876
877#[gpui::test]
878fn test_fold_action(cx: &mut TestAppContext) {
879 init_test(cx, |_| {});
880
881 let editor = cx.add_window(|window, cx| {
882 let buffer = MultiBuffer::build_simple(
883 &"
884 impl Foo {
885 // Hello!
886
887 fn a() {
888 1
889 }
890
891 fn b() {
892 2
893 }
894
895 fn c() {
896 3
897 }
898 }
899 "
900 .unindent(),
901 cx,
902 );
903 build_editor(buffer, window, cx)
904 });
905
906 _ = editor.update(cx, |editor, window, cx| {
907 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
908 s.select_display_ranges([
909 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
910 ]);
911 });
912 editor.fold(&Fold, window, cx);
913 assert_eq!(
914 editor.display_text(cx),
915 "
916 impl Foo {
917 // Hello!
918
919 fn a() {
920 1
921 }
922
923 fn b() {⋯
924 }
925
926 fn c() {⋯
927 }
928 }
929 "
930 .unindent(),
931 );
932
933 editor.fold(&Fold, window, cx);
934 assert_eq!(
935 editor.display_text(cx),
936 "
937 impl Foo {⋯
938 }
939 "
940 .unindent(),
941 );
942
943 editor.unfold_lines(&UnfoldLines, window, cx);
944 assert_eq!(
945 editor.display_text(cx),
946 "
947 impl Foo {
948 // Hello!
949
950 fn a() {
951 1
952 }
953
954 fn b() {⋯
955 }
956
957 fn c() {⋯
958 }
959 }
960 "
961 .unindent(),
962 );
963
964 editor.unfold_lines(&UnfoldLines, window, cx);
965 assert_eq!(
966 editor.display_text(cx),
967 editor.buffer.read(cx).read(cx).text()
968 );
969 });
970}
971
972#[gpui::test]
973fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
974 init_test(cx, |_| {});
975
976 let editor = cx.add_window(|window, cx| {
977 let buffer = MultiBuffer::build_simple(
978 &"
979 class Foo:
980 # Hello!
981
982 def a():
983 print(1)
984
985 def b():
986 print(2)
987
988 def c():
989 print(3)
990 "
991 .unindent(),
992 cx,
993 );
994 build_editor(buffer, window, cx)
995 });
996
997 _ = editor.update(cx, |editor, window, cx| {
998 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
999 s.select_display_ranges([
1000 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
1001 ]);
1002 });
1003 editor.fold(&Fold, window, cx);
1004 assert_eq!(
1005 editor.display_text(cx),
1006 "
1007 class Foo:
1008 # Hello!
1009
1010 def a():
1011 print(1)
1012
1013 def b():⋯
1014
1015 def c():⋯
1016 "
1017 .unindent(),
1018 );
1019
1020 editor.fold(&Fold, window, cx);
1021 assert_eq!(
1022 editor.display_text(cx),
1023 "
1024 class Foo:⋯
1025 "
1026 .unindent(),
1027 );
1028
1029 editor.unfold_lines(&UnfoldLines, window, cx);
1030 assert_eq!(
1031 editor.display_text(cx),
1032 "
1033 class Foo:
1034 # Hello!
1035
1036 def a():
1037 print(1)
1038
1039 def b():⋯
1040
1041 def c():⋯
1042 "
1043 .unindent(),
1044 );
1045
1046 editor.unfold_lines(&UnfoldLines, window, cx);
1047 assert_eq!(
1048 editor.display_text(cx),
1049 editor.buffer.read(cx).read(cx).text()
1050 );
1051 });
1052}
1053
1054#[gpui::test]
1055fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1056 init_test(cx, |_| {});
1057
1058 let editor = cx.add_window(|window, cx| {
1059 let buffer = MultiBuffer::build_simple(
1060 &"
1061 class Foo:
1062 # Hello!
1063
1064 def a():
1065 print(1)
1066
1067 def b():
1068 print(2)
1069
1070
1071 def c():
1072 print(3)
1073
1074
1075 "
1076 .unindent(),
1077 cx,
1078 );
1079 build_editor(buffer, window, cx)
1080 });
1081
1082 _ = editor.update(cx, |editor, window, cx| {
1083 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1084 s.select_display_ranges([
1085 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1086 ]);
1087 });
1088 editor.fold(&Fold, window, cx);
1089 assert_eq!(
1090 editor.display_text(cx),
1091 "
1092 class Foo:
1093 # Hello!
1094
1095 def a():
1096 print(1)
1097
1098 def b():⋯
1099
1100
1101 def c():⋯
1102
1103
1104 "
1105 .unindent(),
1106 );
1107
1108 editor.fold(&Fold, window, cx);
1109 assert_eq!(
1110 editor.display_text(cx),
1111 "
1112 class Foo:⋯
1113
1114
1115 "
1116 .unindent(),
1117 );
1118
1119 editor.unfold_lines(&UnfoldLines, window, cx);
1120 assert_eq!(
1121 editor.display_text(cx),
1122 "
1123 class Foo:
1124 # Hello!
1125
1126 def a():
1127 print(1)
1128
1129 def b():⋯
1130
1131
1132 def c():⋯
1133
1134
1135 "
1136 .unindent(),
1137 );
1138
1139 editor.unfold_lines(&UnfoldLines, window, cx);
1140 assert_eq!(
1141 editor.display_text(cx),
1142 editor.buffer.read(cx).read(cx).text()
1143 );
1144 });
1145}
1146
1147#[gpui::test]
1148fn test_fold_at_level(cx: &mut TestAppContext) {
1149 init_test(cx, |_| {});
1150
1151 let editor = cx.add_window(|window, cx| {
1152 let buffer = MultiBuffer::build_simple(
1153 &"
1154 class Foo:
1155 # Hello!
1156
1157 def a():
1158 print(1)
1159
1160 def b():
1161 print(2)
1162
1163
1164 class Bar:
1165 # World!
1166
1167 def a():
1168 print(1)
1169
1170 def b():
1171 print(2)
1172
1173
1174 "
1175 .unindent(),
1176 cx,
1177 );
1178 build_editor(buffer, window, cx)
1179 });
1180
1181 _ = editor.update(cx, |editor, window, cx| {
1182 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1183 assert_eq!(
1184 editor.display_text(cx),
1185 "
1186 class Foo:
1187 # Hello!
1188
1189 def a():⋯
1190
1191 def b():⋯
1192
1193
1194 class Bar:
1195 # World!
1196
1197 def a():⋯
1198
1199 def b():⋯
1200
1201
1202 "
1203 .unindent(),
1204 );
1205
1206 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1207 assert_eq!(
1208 editor.display_text(cx),
1209 "
1210 class Foo:⋯
1211
1212
1213 class Bar:⋯
1214
1215
1216 "
1217 .unindent(),
1218 );
1219
1220 editor.unfold_all(&UnfoldAll, window, cx);
1221 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1222 assert_eq!(
1223 editor.display_text(cx),
1224 "
1225 class Foo:
1226 # Hello!
1227
1228 def a():
1229 print(1)
1230
1231 def b():
1232 print(2)
1233
1234
1235 class Bar:
1236 # World!
1237
1238 def a():
1239 print(1)
1240
1241 def b():
1242 print(2)
1243
1244
1245 "
1246 .unindent(),
1247 );
1248
1249 assert_eq!(
1250 editor.display_text(cx),
1251 editor.buffer.read(cx).read(cx).text()
1252 );
1253 });
1254}
1255
1256#[gpui::test]
1257fn test_move_cursor(cx: &mut TestAppContext) {
1258 init_test(cx, |_| {});
1259
1260 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1261 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1262
1263 buffer.update(cx, |buffer, cx| {
1264 buffer.edit(
1265 vec![
1266 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1267 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1268 ],
1269 None,
1270 cx,
1271 );
1272 });
1273 _ = editor.update(cx, |editor, window, cx| {
1274 assert_eq!(
1275 editor.selections.display_ranges(cx),
1276 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1277 );
1278
1279 editor.move_down(&MoveDown, window, cx);
1280 assert_eq!(
1281 editor.selections.display_ranges(cx),
1282 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1283 );
1284
1285 editor.move_right(&MoveRight, window, cx);
1286 assert_eq!(
1287 editor.selections.display_ranges(cx),
1288 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1289 );
1290
1291 editor.move_left(&MoveLeft, window, cx);
1292 assert_eq!(
1293 editor.selections.display_ranges(cx),
1294 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1295 );
1296
1297 editor.move_up(&MoveUp, window, cx);
1298 assert_eq!(
1299 editor.selections.display_ranges(cx),
1300 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1301 );
1302
1303 editor.move_to_end(&MoveToEnd, window, cx);
1304 assert_eq!(
1305 editor.selections.display_ranges(cx),
1306 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1307 );
1308
1309 editor.move_to_beginning(&MoveToBeginning, window, cx);
1310 assert_eq!(
1311 editor.selections.display_ranges(cx),
1312 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1313 );
1314
1315 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1316 s.select_display_ranges([
1317 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1318 ]);
1319 });
1320 editor.select_to_beginning(&SelectToBeginning, window, cx);
1321 assert_eq!(
1322 editor.selections.display_ranges(cx),
1323 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1324 );
1325
1326 editor.select_to_end(&SelectToEnd, window, cx);
1327 assert_eq!(
1328 editor.selections.display_ranges(cx),
1329 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1330 );
1331 });
1332}
1333
1334#[gpui::test]
1335fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1336 init_test(cx, |_| {});
1337
1338 let editor = cx.add_window(|window, cx| {
1339 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1340 build_editor(buffer, window, cx)
1341 });
1342
1343 assert_eq!('🟥'.len_utf8(), 4);
1344 assert_eq!('α'.len_utf8(), 2);
1345
1346 _ = editor.update(cx, |editor, window, cx| {
1347 editor.fold_creases(
1348 vec![
1349 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1350 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1351 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1352 ],
1353 true,
1354 window,
1355 cx,
1356 );
1357 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1358
1359 editor.move_right(&MoveRight, window, cx);
1360 assert_eq!(
1361 editor.selections.display_ranges(cx),
1362 &[empty_range(0, "🟥".len())]
1363 );
1364 editor.move_right(&MoveRight, window, cx);
1365 assert_eq!(
1366 editor.selections.display_ranges(cx),
1367 &[empty_range(0, "🟥🟧".len())]
1368 );
1369 editor.move_right(&MoveRight, window, cx);
1370 assert_eq!(
1371 editor.selections.display_ranges(cx),
1372 &[empty_range(0, "🟥🟧⋯".len())]
1373 );
1374
1375 editor.move_down(&MoveDown, window, cx);
1376 assert_eq!(
1377 editor.selections.display_ranges(cx),
1378 &[empty_range(1, "ab⋯e".len())]
1379 );
1380 editor.move_left(&MoveLeft, window, cx);
1381 assert_eq!(
1382 editor.selections.display_ranges(cx),
1383 &[empty_range(1, "ab⋯".len())]
1384 );
1385 editor.move_left(&MoveLeft, window, cx);
1386 assert_eq!(
1387 editor.selections.display_ranges(cx),
1388 &[empty_range(1, "ab".len())]
1389 );
1390 editor.move_left(&MoveLeft, window, cx);
1391 assert_eq!(
1392 editor.selections.display_ranges(cx),
1393 &[empty_range(1, "a".len())]
1394 );
1395
1396 editor.move_down(&MoveDown, window, cx);
1397 assert_eq!(
1398 editor.selections.display_ranges(cx),
1399 &[empty_range(2, "α".len())]
1400 );
1401 editor.move_right(&MoveRight, window, cx);
1402 assert_eq!(
1403 editor.selections.display_ranges(cx),
1404 &[empty_range(2, "αβ".len())]
1405 );
1406 editor.move_right(&MoveRight, window, cx);
1407 assert_eq!(
1408 editor.selections.display_ranges(cx),
1409 &[empty_range(2, "αβ⋯".len())]
1410 );
1411 editor.move_right(&MoveRight, window, cx);
1412 assert_eq!(
1413 editor.selections.display_ranges(cx),
1414 &[empty_range(2, "αβ⋯ε".len())]
1415 );
1416
1417 editor.move_up(&MoveUp, window, cx);
1418 assert_eq!(
1419 editor.selections.display_ranges(cx),
1420 &[empty_range(1, "ab⋯e".len())]
1421 );
1422 editor.move_down(&MoveDown, window, cx);
1423 assert_eq!(
1424 editor.selections.display_ranges(cx),
1425 &[empty_range(2, "αβ⋯ε".len())]
1426 );
1427 editor.move_up(&MoveUp, window, cx);
1428 assert_eq!(
1429 editor.selections.display_ranges(cx),
1430 &[empty_range(1, "ab⋯e".len())]
1431 );
1432
1433 editor.move_up(&MoveUp, window, cx);
1434 assert_eq!(
1435 editor.selections.display_ranges(cx),
1436 &[empty_range(0, "🟥🟧".len())]
1437 );
1438 editor.move_left(&MoveLeft, window, cx);
1439 assert_eq!(
1440 editor.selections.display_ranges(cx),
1441 &[empty_range(0, "🟥".len())]
1442 );
1443 editor.move_left(&MoveLeft, window, cx);
1444 assert_eq!(
1445 editor.selections.display_ranges(cx),
1446 &[empty_range(0, "".len())]
1447 );
1448 });
1449}
1450
1451#[gpui::test]
1452fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1453 init_test(cx, |_| {});
1454
1455 let editor = cx.add_window(|window, cx| {
1456 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1457 build_editor(buffer, window, cx)
1458 });
1459 _ = editor.update(cx, |editor, window, cx| {
1460 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1461 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1462 });
1463
1464 // moving above start of document should move selection to start of document,
1465 // but the next move down should still be at the original goal_x
1466 editor.move_up(&MoveUp, window, cx);
1467 assert_eq!(
1468 editor.selections.display_ranges(cx),
1469 &[empty_range(0, "".len())]
1470 );
1471
1472 editor.move_down(&MoveDown, window, cx);
1473 assert_eq!(
1474 editor.selections.display_ranges(cx),
1475 &[empty_range(1, "abcd".len())]
1476 );
1477
1478 editor.move_down(&MoveDown, window, cx);
1479 assert_eq!(
1480 editor.selections.display_ranges(cx),
1481 &[empty_range(2, "αβγ".len())]
1482 );
1483
1484 editor.move_down(&MoveDown, window, cx);
1485 assert_eq!(
1486 editor.selections.display_ranges(cx),
1487 &[empty_range(3, "abcd".len())]
1488 );
1489
1490 editor.move_down(&MoveDown, window, cx);
1491 assert_eq!(
1492 editor.selections.display_ranges(cx),
1493 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1494 );
1495
1496 // moving past end of document should not change goal_x
1497 editor.move_down(&MoveDown, window, cx);
1498 assert_eq!(
1499 editor.selections.display_ranges(cx),
1500 &[empty_range(5, "".len())]
1501 );
1502
1503 editor.move_down(&MoveDown, window, cx);
1504 assert_eq!(
1505 editor.selections.display_ranges(cx),
1506 &[empty_range(5, "".len())]
1507 );
1508
1509 editor.move_up(&MoveUp, window, cx);
1510 assert_eq!(
1511 editor.selections.display_ranges(cx),
1512 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1513 );
1514
1515 editor.move_up(&MoveUp, window, cx);
1516 assert_eq!(
1517 editor.selections.display_ranges(cx),
1518 &[empty_range(3, "abcd".len())]
1519 );
1520
1521 editor.move_up(&MoveUp, window, cx);
1522 assert_eq!(
1523 editor.selections.display_ranges(cx),
1524 &[empty_range(2, "αβγ".len())]
1525 );
1526 });
1527}
1528
1529#[gpui::test]
1530fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1531 init_test(cx, |_| {});
1532 let move_to_beg = MoveToBeginningOfLine {
1533 stop_at_soft_wraps: true,
1534 stop_at_indent: true,
1535 };
1536
1537 let delete_to_beg = DeleteToBeginningOfLine {
1538 stop_at_indent: false,
1539 };
1540
1541 let move_to_end = MoveToEndOfLine {
1542 stop_at_soft_wraps: true,
1543 };
1544
1545 let editor = cx.add_window(|window, cx| {
1546 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1547 build_editor(buffer, window, cx)
1548 });
1549 _ = editor.update(cx, |editor, window, cx| {
1550 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1551 s.select_display_ranges([
1552 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1553 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1554 ]);
1555 });
1556 });
1557
1558 _ = editor.update(cx, |editor, window, cx| {
1559 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1560 assert_eq!(
1561 editor.selections.display_ranges(cx),
1562 &[
1563 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1564 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1565 ]
1566 );
1567 });
1568
1569 _ = editor.update(cx, |editor, window, cx| {
1570 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1571 assert_eq!(
1572 editor.selections.display_ranges(cx),
1573 &[
1574 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1575 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1576 ]
1577 );
1578 });
1579
1580 _ = editor.update(cx, |editor, window, cx| {
1581 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1582 assert_eq!(
1583 editor.selections.display_ranges(cx),
1584 &[
1585 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1586 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1587 ]
1588 );
1589 });
1590
1591 _ = editor.update(cx, |editor, window, cx| {
1592 editor.move_to_end_of_line(&move_to_end, window, cx);
1593 assert_eq!(
1594 editor.selections.display_ranges(cx),
1595 &[
1596 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1597 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1598 ]
1599 );
1600 });
1601
1602 // Moving to the end of line again is a no-op.
1603 _ = editor.update(cx, |editor, window, cx| {
1604 editor.move_to_end_of_line(&move_to_end, window, cx);
1605 assert_eq!(
1606 editor.selections.display_ranges(cx),
1607 &[
1608 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1609 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1610 ]
1611 );
1612 });
1613
1614 _ = editor.update(cx, |editor, window, cx| {
1615 editor.move_left(&MoveLeft, window, cx);
1616 editor.select_to_beginning_of_line(
1617 &SelectToBeginningOfLine {
1618 stop_at_soft_wraps: true,
1619 stop_at_indent: true,
1620 },
1621 window,
1622 cx,
1623 );
1624 assert_eq!(
1625 editor.selections.display_ranges(cx),
1626 &[
1627 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1628 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1629 ]
1630 );
1631 });
1632
1633 _ = editor.update(cx, |editor, window, cx| {
1634 editor.select_to_beginning_of_line(
1635 &SelectToBeginningOfLine {
1636 stop_at_soft_wraps: true,
1637 stop_at_indent: true,
1638 },
1639 window,
1640 cx,
1641 );
1642 assert_eq!(
1643 editor.selections.display_ranges(cx),
1644 &[
1645 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1646 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1647 ]
1648 );
1649 });
1650
1651 _ = editor.update(cx, |editor, window, cx| {
1652 editor.select_to_beginning_of_line(
1653 &SelectToBeginningOfLine {
1654 stop_at_soft_wraps: true,
1655 stop_at_indent: true,
1656 },
1657 window,
1658 cx,
1659 );
1660 assert_eq!(
1661 editor.selections.display_ranges(cx),
1662 &[
1663 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1664 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1665 ]
1666 );
1667 });
1668
1669 _ = editor.update(cx, |editor, window, cx| {
1670 editor.select_to_end_of_line(
1671 &SelectToEndOfLine {
1672 stop_at_soft_wraps: true,
1673 },
1674 window,
1675 cx,
1676 );
1677 assert_eq!(
1678 editor.selections.display_ranges(cx),
1679 &[
1680 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1681 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1682 ]
1683 );
1684 });
1685
1686 _ = editor.update(cx, |editor, window, cx| {
1687 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1688 assert_eq!(editor.display_text(cx), "ab\n de");
1689 assert_eq!(
1690 editor.selections.display_ranges(cx),
1691 &[
1692 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1693 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1694 ]
1695 );
1696 });
1697
1698 _ = editor.update(cx, |editor, window, cx| {
1699 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1700 assert_eq!(editor.display_text(cx), "\n");
1701 assert_eq!(
1702 editor.selections.display_ranges(cx),
1703 &[
1704 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1705 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1706 ]
1707 );
1708 });
1709}
1710
1711#[gpui::test]
1712fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1713 init_test(cx, |_| {});
1714 let move_to_beg = MoveToBeginningOfLine {
1715 stop_at_soft_wraps: false,
1716 stop_at_indent: false,
1717 };
1718
1719 let move_to_end = MoveToEndOfLine {
1720 stop_at_soft_wraps: false,
1721 };
1722
1723 let editor = cx.add_window(|window, cx| {
1724 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1725 build_editor(buffer, window, cx)
1726 });
1727
1728 _ = editor.update(cx, |editor, window, cx| {
1729 editor.set_wrap_width(Some(140.0.into()), cx);
1730
1731 // We expect the following lines after wrapping
1732 // ```
1733 // thequickbrownfox
1734 // jumpedoverthelazydo
1735 // gs
1736 // ```
1737 // The final `gs` was soft-wrapped onto a new line.
1738 assert_eq!(
1739 "thequickbrownfox\njumpedoverthelaz\nydogs",
1740 editor.display_text(cx),
1741 );
1742
1743 // First, let's assert behavior on the first line, that was not soft-wrapped.
1744 // Start the cursor at the `k` on the first line
1745 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1746 s.select_display_ranges([
1747 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1748 ]);
1749 });
1750
1751 // Moving to the beginning of the line should put us at the beginning of the line.
1752 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1753 assert_eq!(
1754 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1755 editor.selections.display_ranges(cx)
1756 );
1757
1758 // Moving to the end of the line should put us at the end of the line.
1759 editor.move_to_end_of_line(&move_to_end, window, cx);
1760 assert_eq!(
1761 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1762 editor.selections.display_ranges(cx)
1763 );
1764
1765 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1766 // Start the cursor at the last line (`y` that was wrapped to a new line)
1767 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1768 s.select_display_ranges([
1769 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1770 ]);
1771 });
1772
1773 // Moving to the beginning of the line should put us at the start of the second line of
1774 // display text, i.e., the `j`.
1775 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1776 assert_eq!(
1777 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1778 editor.selections.display_ranges(cx)
1779 );
1780
1781 // Moving to the beginning of the line again should be a no-op.
1782 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1783 assert_eq!(
1784 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1785 editor.selections.display_ranges(cx)
1786 );
1787
1788 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1789 // next display line.
1790 editor.move_to_end_of_line(&move_to_end, window, cx);
1791 assert_eq!(
1792 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1793 editor.selections.display_ranges(cx)
1794 );
1795
1796 // Moving to the end of the line again should be a no-op.
1797 editor.move_to_end_of_line(&move_to_end, window, cx);
1798 assert_eq!(
1799 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1800 editor.selections.display_ranges(cx)
1801 );
1802 });
1803}
1804
1805#[gpui::test]
1806fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1807 init_test(cx, |_| {});
1808
1809 let move_to_beg = MoveToBeginningOfLine {
1810 stop_at_soft_wraps: true,
1811 stop_at_indent: true,
1812 };
1813
1814 let select_to_beg = SelectToBeginningOfLine {
1815 stop_at_soft_wraps: true,
1816 stop_at_indent: true,
1817 };
1818
1819 let delete_to_beg = DeleteToBeginningOfLine {
1820 stop_at_indent: true,
1821 };
1822
1823 let move_to_end = MoveToEndOfLine {
1824 stop_at_soft_wraps: false,
1825 };
1826
1827 let editor = cx.add_window(|window, cx| {
1828 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1829 build_editor(buffer, window, cx)
1830 });
1831
1832 _ = editor.update(cx, |editor, window, cx| {
1833 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1834 s.select_display_ranges([
1835 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1836 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1837 ]);
1838 });
1839
1840 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1841 // and the second cursor at the first non-whitespace character in the line.
1842 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1843 assert_eq!(
1844 editor.selections.display_ranges(cx),
1845 &[
1846 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1847 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1848 ]
1849 );
1850
1851 // Moving to the beginning of the line again should be a no-op for the first cursor,
1852 // and should move the second cursor to the beginning of the line.
1853 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1854 assert_eq!(
1855 editor.selections.display_ranges(cx),
1856 &[
1857 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1858 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1859 ]
1860 );
1861
1862 // Moving to the beginning of the line again should still be a no-op for the first cursor,
1863 // and should move the second cursor back to the first non-whitespace character in the line.
1864 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1865 assert_eq!(
1866 editor.selections.display_ranges(cx),
1867 &[
1868 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1869 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1870 ]
1871 );
1872
1873 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
1874 // and to the first non-whitespace character in the line for the second cursor.
1875 editor.move_to_end_of_line(&move_to_end, window, cx);
1876 editor.move_left(&MoveLeft, window, cx);
1877 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1878 assert_eq!(
1879 editor.selections.display_ranges(cx),
1880 &[
1881 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1882 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1883 ]
1884 );
1885
1886 // Selecting to the beginning of the line again should be a no-op for the first cursor,
1887 // and should select to the beginning of the line for the second cursor.
1888 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1889 assert_eq!(
1890 editor.selections.display_ranges(cx),
1891 &[
1892 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1893 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1894 ]
1895 );
1896
1897 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
1898 // and should delete to the first non-whitespace character in the line for the second cursor.
1899 editor.move_to_end_of_line(&move_to_end, window, cx);
1900 editor.move_left(&MoveLeft, window, cx);
1901 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1902 assert_eq!(editor.text(cx), "c\n f");
1903 });
1904}
1905
1906#[gpui::test]
1907fn test_beginning_of_line_with_cursor_between_line_start_and_indent(cx: &mut TestAppContext) {
1908 init_test(cx, |_| {});
1909
1910 let move_to_beg = MoveToBeginningOfLine {
1911 stop_at_soft_wraps: true,
1912 stop_at_indent: true,
1913 };
1914
1915 let editor = cx.add_window(|window, cx| {
1916 let buffer = MultiBuffer::build_simple(" hello\nworld", cx);
1917 build_editor(buffer, window, cx)
1918 });
1919
1920 _ = editor.update(cx, |editor, window, cx| {
1921 // test cursor between line_start and indent_start
1922 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1923 s.select_display_ranges([
1924 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3)
1925 ]);
1926 });
1927
1928 // cursor should move to line_start
1929 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1930 assert_eq!(
1931 editor.selections.display_ranges(cx),
1932 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1933 );
1934
1935 // cursor should move to indent_start
1936 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1937 assert_eq!(
1938 editor.selections.display_ranges(cx),
1939 &[DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 4)]
1940 );
1941
1942 // cursor should move to back to line_start
1943 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1944 assert_eq!(
1945 editor.selections.display_ranges(cx),
1946 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1947 );
1948 });
1949}
1950
1951#[gpui::test]
1952fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1953 init_test(cx, |_| {});
1954
1955 let editor = cx.add_window(|window, cx| {
1956 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1957 build_editor(buffer, window, cx)
1958 });
1959 _ = editor.update(cx, |editor, window, cx| {
1960 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1961 s.select_display_ranges([
1962 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
1963 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
1964 ])
1965 });
1966 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1967 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1968
1969 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1970 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
1971
1972 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1973 assert_selection_ranges("use ˇstd::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1974
1975 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1976 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1977
1978 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1979 assert_selection_ranges("ˇuse std::str::{foo, ˇbar}\n\n {baz.qux()}", editor, cx);
1980
1981 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1982 assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1983
1984 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1985 assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
1986
1987 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1988 assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1989
1990 editor.move_right(&MoveRight, window, cx);
1991 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1992 assert_selection_ranges(
1993 "use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}",
1994 editor,
1995 cx,
1996 );
1997
1998 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1999 assert_selection_ranges(
2000 "use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
2001 editor,
2002 cx,
2003 );
2004
2005 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
2006 assert_selection_ranges(
2007 "use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
2008 editor,
2009 cx,
2010 );
2011 });
2012}
2013
2014#[gpui::test]
2015fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
2016 init_test(cx, |_| {});
2017
2018 let editor = cx.add_window(|window, cx| {
2019 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
2020 build_editor(buffer, window, cx)
2021 });
2022
2023 _ = editor.update(cx, |editor, window, cx| {
2024 editor.set_wrap_width(Some(140.0.into()), cx);
2025 assert_eq!(
2026 editor.display_text(cx),
2027 "use one::{\n two::three::\n four::five\n};"
2028 );
2029
2030 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2031 s.select_display_ranges([
2032 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
2033 ]);
2034 });
2035
2036 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2037 assert_eq!(
2038 editor.selections.display_ranges(cx),
2039 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
2040 );
2041
2042 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2043 assert_eq!(
2044 editor.selections.display_ranges(cx),
2045 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2046 );
2047
2048 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2049 assert_eq!(
2050 editor.selections.display_ranges(cx),
2051 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2052 );
2053
2054 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2055 assert_eq!(
2056 editor.selections.display_ranges(cx),
2057 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2058 );
2059
2060 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2061 assert_eq!(
2062 editor.selections.display_ranges(cx),
2063 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2064 );
2065
2066 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2067 assert_eq!(
2068 editor.selections.display_ranges(cx),
2069 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2070 );
2071 });
2072}
2073
2074#[gpui::test]
2075async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2076 init_test(cx, |_| {});
2077 let mut cx = EditorTestContext::new(cx).await;
2078
2079 let line_height = cx.editor(|editor, window, _| {
2080 editor
2081 .style()
2082 .unwrap()
2083 .text
2084 .line_height_in_pixels(window.rem_size())
2085 });
2086 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2087
2088 cx.set_state(
2089 &r#"ˇone
2090 two
2091
2092 three
2093 fourˇ
2094 five
2095
2096 six"#
2097 .unindent(),
2098 );
2099
2100 cx.update_editor(|editor, window, cx| {
2101 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2102 });
2103 cx.assert_editor_state(
2104 &r#"one
2105 two
2106 ˇ
2107 three
2108 four
2109 five
2110 ˇ
2111 six"#
2112 .unindent(),
2113 );
2114
2115 cx.update_editor(|editor, window, cx| {
2116 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2117 });
2118 cx.assert_editor_state(
2119 &r#"one
2120 two
2121
2122 three
2123 four
2124 five
2125 ˇ
2126 sixˇ"#
2127 .unindent(),
2128 );
2129
2130 cx.update_editor(|editor, window, cx| {
2131 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2132 });
2133 cx.assert_editor_state(
2134 &r#"one
2135 two
2136
2137 three
2138 four
2139 five
2140
2141 sixˇ"#
2142 .unindent(),
2143 );
2144
2145 cx.update_editor(|editor, window, cx| {
2146 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2147 });
2148 cx.assert_editor_state(
2149 &r#"one
2150 two
2151
2152 three
2153 four
2154 five
2155 ˇ
2156 six"#
2157 .unindent(),
2158 );
2159
2160 cx.update_editor(|editor, window, cx| {
2161 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2162 });
2163 cx.assert_editor_state(
2164 &r#"one
2165 two
2166 ˇ
2167 three
2168 four
2169 five
2170
2171 six"#
2172 .unindent(),
2173 );
2174
2175 cx.update_editor(|editor, window, cx| {
2176 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2177 });
2178 cx.assert_editor_state(
2179 &r#"ˇone
2180 two
2181
2182 three
2183 four
2184 five
2185
2186 six"#
2187 .unindent(),
2188 );
2189}
2190
2191#[gpui::test]
2192async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2193 init_test(cx, |_| {});
2194 let mut cx = EditorTestContext::new(cx).await;
2195 let line_height = cx.editor(|editor, window, _| {
2196 editor
2197 .style()
2198 .unwrap()
2199 .text
2200 .line_height_in_pixels(window.rem_size())
2201 });
2202 let window = cx.window;
2203 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2204
2205 cx.set_state(
2206 r#"ˇone
2207 two
2208 three
2209 four
2210 five
2211 six
2212 seven
2213 eight
2214 nine
2215 ten
2216 "#,
2217 );
2218
2219 cx.update_editor(|editor, window, cx| {
2220 assert_eq!(
2221 editor.snapshot(window, cx).scroll_position(),
2222 gpui::Point::new(0., 0.)
2223 );
2224 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2225 assert_eq!(
2226 editor.snapshot(window, cx).scroll_position(),
2227 gpui::Point::new(0., 3.)
2228 );
2229 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2230 assert_eq!(
2231 editor.snapshot(window, cx).scroll_position(),
2232 gpui::Point::new(0., 6.)
2233 );
2234 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2235 assert_eq!(
2236 editor.snapshot(window, cx).scroll_position(),
2237 gpui::Point::new(0., 3.)
2238 );
2239
2240 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2241 assert_eq!(
2242 editor.snapshot(window, cx).scroll_position(),
2243 gpui::Point::new(0., 1.)
2244 );
2245 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2246 assert_eq!(
2247 editor.snapshot(window, cx).scroll_position(),
2248 gpui::Point::new(0., 3.)
2249 );
2250 });
2251}
2252
2253#[gpui::test]
2254async fn test_autoscroll(cx: &mut TestAppContext) {
2255 init_test(cx, |_| {});
2256 let mut cx = EditorTestContext::new(cx).await;
2257
2258 let line_height = cx.update_editor(|editor, window, cx| {
2259 editor.set_vertical_scroll_margin(2, cx);
2260 editor
2261 .style()
2262 .unwrap()
2263 .text
2264 .line_height_in_pixels(window.rem_size())
2265 });
2266 let window = cx.window;
2267 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2268
2269 cx.set_state(
2270 r#"ˇone
2271 two
2272 three
2273 four
2274 five
2275 six
2276 seven
2277 eight
2278 nine
2279 ten
2280 "#,
2281 );
2282 cx.update_editor(|editor, window, cx| {
2283 assert_eq!(
2284 editor.snapshot(window, cx).scroll_position(),
2285 gpui::Point::new(0., 0.0)
2286 );
2287 });
2288
2289 // Add a cursor below the visible area. Since both cursors cannot fit
2290 // on screen, the editor autoscrolls to reveal the newest cursor, and
2291 // allows the vertical scroll margin below that cursor.
2292 cx.update_editor(|editor, window, cx| {
2293 editor.change_selections(Default::default(), window, cx, |selections| {
2294 selections.select_ranges([
2295 Point::new(0, 0)..Point::new(0, 0),
2296 Point::new(6, 0)..Point::new(6, 0),
2297 ]);
2298 })
2299 });
2300 cx.update_editor(|editor, window, cx| {
2301 assert_eq!(
2302 editor.snapshot(window, cx).scroll_position(),
2303 gpui::Point::new(0., 3.0)
2304 );
2305 });
2306
2307 // Move down. The editor cursor scrolls down to track the newest cursor.
2308 cx.update_editor(|editor, window, cx| {
2309 editor.move_down(&Default::default(), window, cx);
2310 });
2311 cx.update_editor(|editor, window, cx| {
2312 assert_eq!(
2313 editor.snapshot(window, cx).scroll_position(),
2314 gpui::Point::new(0., 4.0)
2315 );
2316 });
2317
2318 // Add a cursor above the visible area. Since both cursors fit on screen,
2319 // the editor scrolls to show both.
2320 cx.update_editor(|editor, window, cx| {
2321 editor.change_selections(Default::default(), window, cx, |selections| {
2322 selections.select_ranges([
2323 Point::new(1, 0)..Point::new(1, 0),
2324 Point::new(6, 0)..Point::new(6, 0),
2325 ]);
2326 })
2327 });
2328 cx.update_editor(|editor, window, cx| {
2329 assert_eq!(
2330 editor.snapshot(window, cx).scroll_position(),
2331 gpui::Point::new(0., 1.0)
2332 );
2333 });
2334}
2335
2336#[gpui::test]
2337async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2338 init_test(cx, |_| {});
2339 let mut cx = EditorTestContext::new(cx).await;
2340
2341 let line_height = cx.editor(|editor, window, _cx| {
2342 editor
2343 .style()
2344 .unwrap()
2345 .text
2346 .line_height_in_pixels(window.rem_size())
2347 });
2348 let window = cx.window;
2349 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2350 cx.set_state(
2351 &r#"
2352 ˇone
2353 two
2354 threeˇ
2355 four
2356 five
2357 six
2358 seven
2359 eight
2360 nine
2361 ten
2362 "#
2363 .unindent(),
2364 );
2365
2366 cx.update_editor(|editor, window, cx| {
2367 editor.move_page_down(&MovePageDown::default(), window, cx)
2368 });
2369 cx.assert_editor_state(
2370 &r#"
2371 one
2372 two
2373 three
2374 ˇfour
2375 five
2376 sixˇ
2377 seven
2378 eight
2379 nine
2380 ten
2381 "#
2382 .unindent(),
2383 );
2384
2385 cx.update_editor(|editor, window, cx| {
2386 editor.move_page_down(&MovePageDown::default(), window, cx)
2387 });
2388 cx.assert_editor_state(
2389 &r#"
2390 one
2391 two
2392 three
2393 four
2394 five
2395 six
2396 ˇseven
2397 eight
2398 nineˇ
2399 ten
2400 "#
2401 .unindent(),
2402 );
2403
2404 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2405 cx.assert_editor_state(
2406 &r#"
2407 one
2408 two
2409 three
2410 ˇfour
2411 five
2412 sixˇ
2413 seven
2414 eight
2415 nine
2416 ten
2417 "#
2418 .unindent(),
2419 );
2420
2421 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2422 cx.assert_editor_state(
2423 &r#"
2424 ˇone
2425 two
2426 threeˇ
2427 four
2428 five
2429 six
2430 seven
2431 eight
2432 nine
2433 ten
2434 "#
2435 .unindent(),
2436 );
2437
2438 // Test select collapsing
2439 cx.update_editor(|editor, window, cx| {
2440 editor.move_page_down(&MovePageDown::default(), window, cx);
2441 editor.move_page_down(&MovePageDown::default(), window, cx);
2442 editor.move_page_down(&MovePageDown::default(), window, cx);
2443 });
2444 cx.assert_editor_state(
2445 &r#"
2446 one
2447 two
2448 three
2449 four
2450 five
2451 six
2452 seven
2453 eight
2454 nine
2455 ˇten
2456 ˇ"#
2457 .unindent(),
2458 );
2459}
2460
2461#[gpui::test]
2462async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2463 init_test(cx, |_| {});
2464 let mut cx = EditorTestContext::new(cx).await;
2465 cx.set_state("one «two threeˇ» four");
2466 cx.update_editor(|editor, window, cx| {
2467 editor.delete_to_beginning_of_line(
2468 &DeleteToBeginningOfLine {
2469 stop_at_indent: false,
2470 },
2471 window,
2472 cx,
2473 );
2474 assert_eq!(editor.text(cx), " four");
2475 });
2476}
2477
2478#[gpui::test]
2479async fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2480 init_test(cx, |_| {});
2481
2482 let mut cx = EditorTestContext::new(cx).await;
2483
2484 // For an empty selection, the preceding word fragment is deleted.
2485 // For non-empty selections, only selected characters are deleted.
2486 cx.set_state("onˇe two t«hreˇ»e four");
2487 cx.update_editor(|editor, window, cx| {
2488 editor.delete_to_previous_word_start(
2489 &DeleteToPreviousWordStart {
2490 ignore_newlines: false,
2491 ignore_brackets: false,
2492 },
2493 window,
2494 cx,
2495 );
2496 });
2497 cx.assert_editor_state("ˇe two tˇe four");
2498
2499 cx.set_state("e tˇwo te «fˇ»our");
2500 cx.update_editor(|editor, window, cx| {
2501 editor.delete_to_next_word_end(
2502 &DeleteToNextWordEnd {
2503 ignore_newlines: false,
2504 ignore_brackets: false,
2505 },
2506 window,
2507 cx,
2508 );
2509 });
2510 cx.assert_editor_state("e tˇ te ˇour");
2511}
2512
2513#[gpui::test]
2514async fn test_delete_whitespaces(cx: &mut TestAppContext) {
2515 init_test(cx, |_| {});
2516
2517 let mut cx = EditorTestContext::new(cx).await;
2518
2519 cx.set_state("here is some text ˇwith a space");
2520 cx.update_editor(|editor, window, cx| {
2521 editor.delete_to_previous_word_start(
2522 &DeleteToPreviousWordStart {
2523 ignore_newlines: false,
2524 ignore_brackets: true,
2525 },
2526 window,
2527 cx,
2528 );
2529 });
2530 // Continuous whitespace sequences are removed entirely, words behind them are not affected by the deletion action.
2531 cx.assert_editor_state("here is some textˇwith a space");
2532
2533 cx.set_state("here is some text ˇwith a space");
2534 cx.update_editor(|editor, window, cx| {
2535 editor.delete_to_previous_word_start(
2536 &DeleteToPreviousWordStart {
2537 ignore_newlines: false,
2538 ignore_brackets: false,
2539 },
2540 window,
2541 cx,
2542 );
2543 });
2544 cx.assert_editor_state("here is some textˇwith a space");
2545
2546 cx.set_state("here is some textˇ with a space");
2547 cx.update_editor(|editor, window, cx| {
2548 editor.delete_to_next_word_end(
2549 &DeleteToNextWordEnd {
2550 ignore_newlines: false,
2551 ignore_brackets: true,
2552 },
2553 window,
2554 cx,
2555 );
2556 });
2557 // Same happens in the other direction.
2558 cx.assert_editor_state("here is some textˇwith a space");
2559
2560 cx.set_state("here is some textˇ with a space");
2561 cx.update_editor(|editor, window, cx| {
2562 editor.delete_to_next_word_end(
2563 &DeleteToNextWordEnd {
2564 ignore_newlines: false,
2565 ignore_brackets: false,
2566 },
2567 window,
2568 cx,
2569 );
2570 });
2571 cx.assert_editor_state("here is some textˇwith a space");
2572
2573 cx.set_state("here is some textˇ with a space");
2574 cx.update_editor(|editor, window, cx| {
2575 editor.delete_to_next_word_end(
2576 &DeleteToNextWordEnd {
2577 ignore_newlines: true,
2578 ignore_brackets: false,
2579 },
2580 window,
2581 cx,
2582 );
2583 });
2584 cx.assert_editor_state("here is some textˇwith a space");
2585 cx.update_editor(|editor, window, cx| {
2586 editor.delete_to_previous_word_start(
2587 &DeleteToPreviousWordStart {
2588 ignore_newlines: true,
2589 ignore_brackets: false,
2590 },
2591 window,
2592 cx,
2593 );
2594 });
2595 cx.assert_editor_state("here is some ˇwith a space");
2596 cx.update_editor(|editor, window, cx| {
2597 editor.delete_to_previous_word_start(
2598 &DeleteToPreviousWordStart {
2599 ignore_newlines: true,
2600 ignore_brackets: false,
2601 },
2602 window,
2603 cx,
2604 );
2605 });
2606 // Single whitespaces are removed with the word behind them.
2607 cx.assert_editor_state("here is ˇwith a space");
2608 cx.update_editor(|editor, window, cx| {
2609 editor.delete_to_previous_word_start(
2610 &DeleteToPreviousWordStart {
2611 ignore_newlines: true,
2612 ignore_brackets: false,
2613 },
2614 window,
2615 cx,
2616 );
2617 });
2618 cx.assert_editor_state("here ˇwith a space");
2619 cx.update_editor(|editor, window, cx| {
2620 editor.delete_to_previous_word_start(
2621 &DeleteToPreviousWordStart {
2622 ignore_newlines: true,
2623 ignore_brackets: false,
2624 },
2625 window,
2626 cx,
2627 );
2628 });
2629 cx.assert_editor_state("ˇwith a space");
2630 cx.update_editor(|editor, window, cx| {
2631 editor.delete_to_previous_word_start(
2632 &DeleteToPreviousWordStart {
2633 ignore_newlines: true,
2634 ignore_brackets: false,
2635 },
2636 window,
2637 cx,
2638 );
2639 });
2640 cx.assert_editor_state("ˇwith a space");
2641 cx.update_editor(|editor, window, cx| {
2642 editor.delete_to_next_word_end(
2643 &DeleteToNextWordEnd {
2644 ignore_newlines: true,
2645 ignore_brackets: false,
2646 },
2647 window,
2648 cx,
2649 );
2650 });
2651 // Same happens in the other direction.
2652 cx.assert_editor_state("ˇ a space");
2653 cx.update_editor(|editor, window, cx| {
2654 editor.delete_to_next_word_end(
2655 &DeleteToNextWordEnd {
2656 ignore_newlines: true,
2657 ignore_brackets: false,
2658 },
2659 window,
2660 cx,
2661 );
2662 });
2663 cx.assert_editor_state("ˇ space");
2664 cx.update_editor(|editor, window, cx| {
2665 editor.delete_to_next_word_end(
2666 &DeleteToNextWordEnd {
2667 ignore_newlines: true,
2668 ignore_brackets: false,
2669 },
2670 window,
2671 cx,
2672 );
2673 });
2674 cx.assert_editor_state("ˇ");
2675 cx.update_editor(|editor, window, cx| {
2676 editor.delete_to_next_word_end(
2677 &DeleteToNextWordEnd {
2678 ignore_newlines: true,
2679 ignore_brackets: false,
2680 },
2681 window,
2682 cx,
2683 );
2684 });
2685 cx.assert_editor_state("ˇ");
2686 cx.update_editor(|editor, window, cx| {
2687 editor.delete_to_previous_word_start(
2688 &DeleteToPreviousWordStart {
2689 ignore_newlines: true,
2690 ignore_brackets: false,
2691 },
2692 window,
2693 cx,
2694 );
2695 });
2696 cx.assert_editor_state("ˇ");
2697}
2698
2699#[gpui::test]
2700async fn test_delete_to_bracket(cx: &mut TestAppContext) {
2701 init_test(cx, |_| {});
2702
2703 let language = Arc::new(
2704 Language::new(
2705 LanguageConfig {
2706 brackets: BracketPairConfig {
2707 pairs: vec![
2708 BracketPair {
2709 start: "\"".to_string(),
2710 end: "\"".to_string(),
2711 close: true,
2712 surround: true,
2713 newline: false,
2714 },
2715 BracketPair {
2716 start: "(".to_string(),
2717 end: ")".to_string(),
2718 close: true,
2719 surround: true,
2720 newline: true,
2721 },
2722 ],
2723 ..BracketPairConfig::default()
2724 },
2725 ..LanguageConfig::default()
2726 },
2727 Some(tree_sitter_rust::LANGUAGE.into()),
2728 )
2729 .with_brackets_query(
2730 r#"
2731 ("(" @open ")" @close)
2732 ("\"" @open "\"" @close)
2733 "#,
2734 )
2735 .unwrap(),
2736 );
2737
2738 let mut cx = EditorTestContext::new(cx).await;
2739 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2740
2741 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2742 cx.update_editor(|editor, window, cx| {
2743 editor.delete_to_previous_word_start(
2744 &DeleteToPreviousWordStart {
2745 ignore_newlines: true,
2746 ignore_brackets: false,
2747 },
2748 window,
2749 cx,
2750 );
2751 });
2752 // Deletion stops before brackets if asked to not ignore them.
2753 cx.assert_editor_state(r#"macro!("ˇCOMMENT");"#);
2754 cx.update_editor(|editor, window, cx| {
2755 editor.delete_to_previous_word_start(
2756 &DeleteToPreviousWordStart {
2757 ignore_newlines: true,
2758 ignore_brackets: false,
2759 },
2760 window,
2761 cx,
2762 );
2763 });
2764 // Deletion has to remove a single bracket and then stop again.
2765 cx.assert_editor_state(r#"macro!(ˇCOMMENT");"#);
2766
2767 cx.update_editor(|editor, window, cx| {
2768 editor.delete_to_previous_word_start(
2769 &DeleteToPreviousWordStart {
2770 ignore_newlines: true,
2771 ignore_brackets: false,
2772 },
2773 window,
2774 cx,
2775 );
2776 });
2777 cx.assert_editor_state(r#"macro!ˇCOMMENT");"#);
2778
2779 cx.update_editor(|editor, window, cx| {
2780 editor.delete_to_previous_word_start(
2781 &DeleteToPreviousWordStart {
2782 ignore_newlines: true,
2783 ignore_brackets: false,
2784 },
2785 window,
2786 cx,
2787 );
2788 });
2789 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2790
2791 cx.update_editor(|editor, window, cx| {
2792 editor.delete_to_previous_word_start(
2793 &DeleteToPreviousWordStart {
2794 ignore_newlines: true,
2795 ignore_brackets: false,
2796 },
2797 window,
2798 cx,
2799 );
2800 });
2801 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2802
2803 cx.update_editor(|editor, window, cx| {
2804 editor.delete_to_next_word_end(
2805 &DeleteToNextWordEnd {
2806 ignore_newlines: true,
2807 ignore_brackets: false,
2808 },
2809 window,
2810 cx,
2811 );
2812 });
2813 // Brackets on the right are not paired anymore, hence deletion does not stop at them
2814 cx.assert_editor_state(r#"ˇ");"#);
2815
2816 cx.update_editor(|editor, window, cx| {
2817 editor.delete_to_next_word_end(
2818 &DeleteToNextWordEnd {
2819 ignore_newlines: true,
2820 ignore_brackets: false,
2821 },
2822 window,
2823 cx,
2824 );
2825 });
2826 cx.assert_editor_state(r#"ˇ"#);
2827
2828 cx.update_editor(|editor, window, cx| {
2829 editor.delete_to_next_word_end(
2830 &DeleteToNextWordEnd {
2831 ignore_newlines: true,
2832 ignore_brackets: false,
2833 },
2834 window,
2835 cx,
2836 );
2837 });
2838 cx.assert_editor_state(r#"ˇ"#);
2839
2840 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2841 cx.update_editor(|editor, window, cx| {
2842 editor.delete_to_previous_word_start(
2843 &DeleteToPreviousWordStart {
2844 ignore_newlines: true,
2845 ignore_brackets: true,
2846 },
2847 window,
2848 cx,
2849 );
2850 });
2851 cx.assert_editor_state(r#"macroˇCOMMENT");"#);
2852}
2853
2854#[gpui::test]
2855fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2856 init_test(cx, |_| {});
2857
2858 let editor = cx.add_window(|window, cx| {
2859 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2860 build_editor(buffer, window, cx)
2861 });
2862 let del_to_prev_word_start = DeleteToPreviousWordStart {
2863 ignore_newlines: false,
2864 ignore_brackets: false,
2865 };
2866 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2867 ignore_newlines: true,
2868 ignore_brackets: false,
2869 };
2870
2871 _ = editor.update(cx, |editor, window, cx| {
2872 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2873 s.select_display_ranges([
2874 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2875 ])
2876 });
2877 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2878 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2879 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2880 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2881 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2882 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2883 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2884 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
2885 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2886 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
2887 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2888 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2889 });
2890}
2891
2892#[gpui::test]
2893fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2894 init_test(cx, |_| {});
2895
2896 let editor = cx.add_window(|window, cx| {
2897 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2898 build_editor(buffer, window, cx)
2899 });
2900 let del_to_next_word_end = DeleteToNextWordEnd {
2901 ignore_newlines: false,
2902 ignore_brackets: false,
2903 };
2904 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2905 ignore_newlines: true,
2906 ignore_brackets: false,
2907 };
2908
2909 _ = editor.update(cx, |editor, window, cx| {
2910 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2911 s.select_display_ranges([
2912 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2913 ])
2914 });
2915 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2916 assert_eq!(
2917 editor.buffer.read(cx).read(cx).text(),
2918 "one\n two\nthree\n four"
2919 );
2920 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2921 assert_eq!(
2922 editor.buffer.read(cx).read(cx).text(),
2923 "\n two\nthree\n four"
2924 );
2925 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2926 assert_eq!(
2927 editor.buffer.read(cx).read(cx).text(),
2928 "two\nthree\n four"
2929 );
2930 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2931 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2932 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2933 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
2934 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2935 assert_eq!(editor.buffer.read(cx).read(cx).text(), "four");
2936 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2937 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2938 });
2939}
2940
2941#[gpui::test]
2942fn test_newline(cx: &mut TestAppContext) {
2943 init_test(cx, |_| {});
2944
2945 let editor = cx.add_window(|window, cx| {
2946 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2947 build_editor(buffer, window, cx)
2948 });
2949
2950 _ = editor.update(cx, |editor, window, cx| {
2951 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2952 s.select_display_ranges([
2953 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2954 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2955 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2956 ])
2957 });
2958
2959 editor.newline(&Newline, window, cx);
2960 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2961 });
2962}
2963
2964#[gpui::test]
2965fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2966 init_test(cx, |_| {});
2967
2968 let editor = cx.add_window(|window, cx| {
2969 let buffer = MultiBuffer::build_simple(
2970 "
2971 a
2972 b(
2973 X
2974 )
2975 c(
2976 X
2977 )
2978 "
2979 .unindent()
2980 .as_str(),
2981 cx,
2982 );
2983 let mut editor = build_editor(buffer, window, cx);
2984 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2985 s.select_ranges([
2986 Point::new(2, 4)..Point::new(2, 5),
2987 Point::new(5, 4)..Point::new(5, 5),
2988 ])
2989 });
2990 editor
2991 });
2992
2993 _ = editor.update(cx, |editor, window, cx| {
2994 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2995 editor.buffer.update(cx, |buffer, cx| {
2996 buffer.edit(
2997 [
2998 (Point::new(1, 2)..Point::new(3, 0), ""),
2999 (Point::new(4, 2)..Point::new(6, 0), ""),
3000 ],
3001 None,
3002 cx,
3003 );
3004 assert_eq!(
3005 buffer.read(cx).text(),
3006 "
3007 a
3008 b()
3009 c()
3010 "
3011 .unindent()
3012 );
3013 });
3014 assert_eq!(
3015 editor.selections.ranges(cx),
3016 &[
3017 Point::new(1, 2)..Point::new(1, 2),
3018 Point::new(2, 2)..Point::new(2, 2),
3019 ],
3020 );
3021
3022 editor.newline(&Newline, window, cx);
3023 assert_eq!(
3024 editor.text(cx),
3025 "
3026 a
3027 b(
3028 )
3029 c(
3030 )
3031 "
3032 .unindent()
3033 );
3034
3035 // The selections are moved after the inserted newlines
3036 assert_eq!(
3037 editor.selections.ranges(cx),
3038 &[
3039 Point::new(2, 0)..Point::new(2, 0),
3040 Point::new(4, 0)..Point::new(4, 0),
3041 ],
3042 );
3043 });
3044}
3045
3046#[gpui::test]
3047async fn test_newline_above(cx: &mut TestAppContext) {
3048 init_test(cx, |settings| {
3049 settings.defaults.tab_size = NonZeroU32::new(4)
3050 });
3051
3052 let language = Arc::new(
3053 Language::new(
3054 LanguageConfig::default(),
3055 Some(tree_sitter_rust::LANGUAGE.into()),
3056 )
3057 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3058 .unwrap(),
3059 );
3060
3061 let mut cx = EditorTestContext::new(cx).await;
3062 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3063 cx.set_state(indoc! {"
3064 const a: ˇA = (
3065 (ˇ
3066 «const_functionˇ»(ˇ),
3067 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3068 )ˇ
3069 ˇ);ˇ
3070 "});
3071
3072 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
3073 cx.assert_editor_state(indoc! {"
3074 ˇ
3075 const a: A = (
3076 ˇ
3077 (
3078 ˇ
3079 ˇ
3080 const_function(),
3081 ˇ
3082 ˇ
3083 ˇ
3084 ˇ
3085 something_else,
3086 ˇ
3087 )
3088 ˇ
3089 ˇ
3090 );
3091 "});
3092}
3093
3094#[gpui::test]
3095async fn test_newline_below(cx: &mut TestAppContext) {
3096 init_test(cx, |settings| {
3097 settings.defaults.tab_size = NonZeroU32::new(4)
3098 });
3099
3100 let language = Arc::new(
3101 Language::new(
3102 LanguageConfig::default(),
3103 Some(tree_sitter_rust::LANGUAGE.into()),
3104 )
3105 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3106 .unwrap(),
3107 );
3108
3109 let mut cx = EditorTestContext::new(cx).await;
3110 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3111 cx.set_state(indoc! {"
3112 const a: ˇA = (
3113 (ˇ
3114 «const_functionˇ»(ˇ),
3115 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3116 )ˇ
3117 ˇ);ˇ
3118 "});
3119
3120 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
3121 cx.assert_editor_state(indoc! {"
3122 const a: A = (
3123 ˇ
3124 (
3125 ˇ
3126 const_function(),
3127 ˇ
3128 ˇ
3129 something_else,
3130 ˇ
3131 ˇ
3132 ˇ
3133 ˇ
3134 )
3135 ˇ
3136 );
3137 ˇ
3138 ˇ
3139 "});
3140}
3141
3142#[gpui::test]
3143async fn test_newline_comments(cx: &mut TestAppContext) {
3144 init_test(cx, |settings| {
3145 settings.defaults.tab_size = NonZeroU32::new(4)
3146 });
3147
3148 let language = Arc::new(Language::new(
3149 LanguageConfig {
3150 line_comments: vec!["// ".into()],
3151 ..LanguageConfig::default()
3152 },
3153 None,
3154 ));
3155 {
3156 let mut cx = EditorTestContext::new(cx).await;
3157 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3158 cx.set_state(indoc! {"
3159 // Fooˇ
3160 "});
3161
3162 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3163 cx.assert_editor_state(indoc! {"
3164 // Foo
3165 // ˇ
3166 "});
3167 // Ensure that we add comment prefix when existing line contains space
3168 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3169 cx.assert_editor_state(
3170 indoc! {"
3171 // Foo
3172 //s
3173 // ˇ
3174 "}
3175 .replace("s", " ") // s is used as space placeholder to prevent format on save
3176 .as_str(),
3177 );
3178 // Ensure that we add comment prefix when existing line does not contain space
3179 cx.set_state(indoc! {"
3180 // Foo
3181 //ˇ
3182 "});
3183 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3184 cx.assert_editor_state(indoc! {"
3185 // Foo
3186 //
3187 // ˇ
3188 "});
3189 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
3190 cx.set_state(indoc! {"
3191 ˇ// Foo
3192 "});
3193 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3194 cx.assert_editor_state(indoc! {"
3195
3196 ˇ// Foo
3197 "});
3198 }
3199 // Ensure that comment continuations can be disabled.
3200 update_test_language_settings(cx, |settings| {
3201 settings.defaults.extend_comment_on_newline = Some(false);
3202 });
3203 let mut cx = EditorTestContext::new(cx).await;
3204 cx.set_state(indoc! {"
3205 // Fooˇ
3206 "});
3207 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3208 cx.assert_editor_state(indoc! {"
3209 // Foo
3210 ˇ
3211 "});
3212}
3213
3214#[gpui::test]
3215async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
3216 init_test(cx, |settings| {
3217 settings.defaults.tab_size = NonZeroU32::new(4)
3218 });
3219
3220 let language = Arc::new(Language::new(
3221 LanguageConfig {
3222 line_comments: vec!["// ".into(), "/// ".into()],
3223 ..LanguageConfig::default()
3224 },
3225 None,
3226 ));
3227 {
3228 let mut cx = EditorTestContext::new(cx).await;
3229 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3230 cx.set_state(indoc! {"
3231 //ˇ
3232 "});
3233 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3234 cx.assert_editor_state(indoc! {"
3235 //
3236 // ˇ
3237 "});
3238
3239 cx.set_state(indoc! {"
3240 ///ˇ
3241 "});
3242 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3243 cx.assert_editor_state(indoc! {"
3244 ///
3245 /// ˇ
3246 "});
3247 }
3248}
3249
3250#[gpui::test]
3251async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
3252 init_test(cx, |settings| {
3253 settings.defaults.tab_size = NonZeroU32::new(4)
3254 });
3255
3256 let language = Arc::new(
3257 Language::new(
3258 LanguageConfig {
3259 documentation_comment: Some(language::BlockCommentConfig {
3260 start: "/**".into(),
3261 end: "*/".into(),
3262 prefix: "* ".into(),
3263 tab_size: 1,
3264 }),
3265
3266 ..LanguageConfig::default()
3267 },
3268 Some(tree_sitter_rust::LANGUAGE.into()),
3269 )
3270 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
3271 .unwrap(),
3272 );
3273
3274 {
3275 let mut cx = EditorTestContext::new(cx).await;
3276 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3277 cx.set_state(indoc! {"
3278 /**ˇ
3279 "});
3280
3281 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3282 cx.assert_editor_state(indoc! {"
3283 /**
3284 * ˇ
3285 "});
3286 // Ensure that if cursor is before the comment start,
3287 // we do not actually insert a comment prefix.
3288 cx.set_state(indoc! {"
3289 ˇ/**
3290 "});
3291 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3292 cx.assert_editor_state(indoc! {"
3293
3294 ˇ/**
3295 "});
3296 // Ensure that if cursor is between it doesn't add comment prefix.
3297 cx.set_state(indoc! {"
3298 /*ˇ*
3299 "});
3300 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3301 cx.assert_editor_state(indoc! {"
3302 /*
3303 ˇ*
3304 "});
3305 // Ensure that if suffix exists on same line after cursor it adds new line.
3306 cx.set_state(indoc! {"
3307 /**ˇ*/
3308 "});
3309 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3310 cx.assert_editor_state(indoc! {"
3311 /**
3312 * ˇ
3313 */
3314 "});
3315 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3316 cx.set_state(indoc! {"
3317 /**ˇ */
3318 "});
3319 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3320 cx.assert_editor_state(indoc! {"
3321 /**
3322 * ˇ
3323 */
3324 "});
3325 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3326 cx.set_state(indoc! {"
3327 /** ˇ*/
3328 "});
3329 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3330 cx.assert_editor_state(
3331 indoc! {"
3332 /**s
3333 * ˇ
3334 */
3335 "}
3336 .replace("s", " ") // s is used as space placeholder to prevent format on save
3337 .as_str(),
3338 );
3339 // Ensure that delimiter space is preserved when newline on already
3340 // spaced delimiter.
3341 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3342 cx.assert_editor_state(
3343 indoc! {"
3344 /**s
3345 *s
3346 * ˇ
3347 */
3348 "}
3349 .replace("s", " ") // s is used as space placeholder to prevent format on save
3350 .as_str(),
3351 );
3352 // Ensure that delimiter space is preserved when space is not
3353 // on existing delimiter.
3354 cx.set_state(indoc! {"
3355 /**
3356 *ˇ
3357 */
3358 "});
3359 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3360 cx.assert_editor_state(indoc! {"
3361 /**
3362 *
3363 * ˇ
3364 */
3365 "});
3366 // Ensure that if suffix exists on same line after cursor it
3367 // doesn't add extra new line if prefix is not on same line.
3368 cx.set_state(indoc! {"
3369 /**
3370 ˇ*/
3371 "});
3372 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3373 cx.assert_editor_state(indoc! {"
3374 /**
3375
3376 ˇ*/
3377 "});
3378 // Ensure that it detects suffix after existing prefix.
3379 cx.set_state(indoc! {"
3380 /**ˇ/
3381 "});
3382 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3383 cx.assert_editor_state(indoc! {"
3384 /**
3385 ˇ/
3386 "});
3387 // Ensure that if suffix exists on same line before
3388 // cursor it does not add comment prefix.
3389 cx.set_state(indoc! {"
3390 /** */ˇ
3391 "});
3392 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3393 cx.assert_editor_state(indoc! {"
3394 /** */
3395 ˇ
3396 "});
3397 // Ensure that if suffix exists on same line before
3398 // cursor it does not add comment prefix.
3399 cx.set_state(indoc! {"
3400 /**
3401 *
3402 */ˇ
3403 "});
3404 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3405 cx.assert_editor_state(indoc! {"
3406 /**
3407 *
3408 */
3409 ˇ
3410 "});
3411
3412 // Ensure that inline comment followed by code
3413 // doesn't add comment prefix on newline
3414 cx.set_state(indoc! {"
3415 /** */ textˇ
3416 "});
3417 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3418 cx.assert_editor_state(indoc! {"
3419 /** */ text
3420 ˇ
3421 "});
3422
3423 // Ensure that text after comment end tag
3424 // doesn't add comment prefix on newline
3425 cx.set_state(indoc! {"
3426 /**
3427 *
3428 */ˇtext
3429 "});
3430 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3431 cx.assert_editor_state(indoc! {"
3432 /**
3433 *
3434 */
3435 ˇtext
3436 "});
3437
3438 // Ensure if not comment block it doesn't
3439 // add comment prefix on newline
3440 cx.set_state(indoc! {"
3441 * textˇ
3442 "});
3443 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3444 cx.assert_editor_state(indoc! {"
3445 * text
3446 ˇ
3447 "});
3448 }
3449 // Ensure that comment continuations can be disabled.
3450 update_test_language_settings(cx, |settings| {
3451 settings.defaults.extend_comment_on_newline = Some(false);
3452 });
3453 let mut cx = EditorTestContext::new(cx).await;
3454 cx.set_state(indoc! {"
3455 /**ˇ
3456 "});
3457 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3458 cx.assert_editor_state(indoc! {"
3459 /**
3460 ˇ
3461 "});
3462}
3463
3464#[gpui::test]
3465async fn test_newline_comments_with_block_comment(cx: &mut TestAppContext) {
3466 init_test(cx, |settings| {
3467 settings.defaults.tab_size = NonZeroU32::new(4)
3468 });
3469
3470 let lua_language = Arc::new(Language::new(
3471 LanguageConfig {
3472 line_comments: vec!["--".into()],
3473 block_comment: Some(language::BlockCommentConfig {
3474 start: "--[[".into(),
3475 prefix: "".into(),
3476 end: "]]".into(),
3477 tab_size: 0,
3478 }),
3479 ..LanguageConfig::default()
3480 },
3481 None,
3482 ));
3483
3484 let mut cx = EditorTestContext::new(cx).await;
3485 cx.update_buffer(|buffer, cx| buffer.set_language(Some(lua_language), cx));
3486
3487 // Line with line comment should extend
3488 cx.set_state(indoc! {"
3489 --ˇ
3490 "});
3491 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3492 cx.assert_editor_state(indoc! {"
3493 --
3494 --ˇ
3495 "});
3496
3497 // Line with block comment that matches line comment should not extend
3498 cx.set_state(indoc! {"
3499 --[[ˇ
3500 "});
3501 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3502 cx.assert_editor_state(indoc! {"
3503 --[[
3504 ˇ
3505 "});
3506}
3507
3508#[gpui::test]
3509fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3510 init_test(cx, |_| {});
3511
3512 let editor = cx.add_window(|window, cx| {
3513 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3514 let mut editor = build_editor(buffer, window, cx);
3515 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3516 s.select_ranges([3..4, 11..12, 19..20])
3517 });
3518 editor
3519 });
3520
3521 _ = editor.update(cx, |editor, window, cx| {
3522 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3523 editor.buffer.update(cx, |buffer, cx| {
3524 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3525 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3526 });
3527 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
3528
3529 editor.insert("Z", window, cx);
3530 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3531
3532 // The selections are moved after the inserted characters
3533 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
3534 });
3535}
3536
3537#[gpui::test]
3538async fn test_tab(cx: &mut TestAppContext) {
3539 init_test(cx, |settings| {
3540 settings.defaults.tab_size = NonZeroU32::new(3)
3541 });
3542
3543 let mut cx = EditorTestContext::new(cx).await;
3544 cx.set_state(indoc! {"
3545 ˇabˇc
3546 ˇ🏀ˇ🏀ˇefg
3547 dˇ
3548 "});
3549 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3550 cx.assert_editor_state(indoc! {"
3551 ˇab ˇc
3552 ˇ🏀 ˇ🏀 ˇefg
3553 d ˇ
3554 "});
3555
3556 cx.set_state(indoc! {"
3557 a
3558 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3559 "});
3560 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3561 cx.assert_editor_state(indoc! {"
3562 a
3563 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3564 "});
3565}
3566
3567#[gpui::test]
3568async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3569 init_test(cx, |_| {});
3570
3571 let mut cx = EditorTestContext::new(cx).await;
3572 let language = Arc::new(
3573 Language::new(
3574 LanguageConfig::default(),
3575 Some(tree_sitter_rust::LANGUAGE.into()),
3576 )
3577 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3578 .unwrap(),
3579 );
3580 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3581
3582 // test when all cursors are not at suggested indent
3583 // then simply move to their suggested indent location
3584 cx.set_state(indoc! {"
3585 const a: B = (
3586 c(
3587 ˇ
3588 ˇ )
3589 );
3590 "});
3591 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3592 cx.assert_editor_state(indoc! {"
3593 const a: B = (
3594 c(
3595 ˇ
3596 ˇ)
3597 );
3598 "});
3599
3600 // test cursor already at suggested indent not moving when
3601 // other cursors are yet to reach their suggested indents
3602 cx.set_state(indoc! {"
3603 ˇ
3604 const a: B = (
3605 c(
3606 d(
3607 ˇ
3608 )
3609 ˇ
3610 ˇ )
3611 );
3612 "});
3613 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3614 cx.assert_editor_state(indoc! {"
3615 ˇ
3616 const a: B = (
3617 c(
3618 d(
3619 ˇ
3620 )
3621 ˇ
3622 ˇ)
3623 );
3624 "});
3625 // test when all cursors are at suggested indent then tab is inserted
3626 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3627 cx.assert_editor_state(indoc! {"
3628 ˇ
3629 const a: B = (
3630 c(
3631 d(
3632 ˇ
3633 )
3634 ˇ
3635 ˇ)
3636 );
3637 "});
3638
3639 // test when current indent is less than suggested indent,
3640 // we adjust line to match suggested indent and move cursor to it
3641 //
3642 // when no other cursor is at word boundary, all of them should move
3643 cx.set_state(indoc! {"
3644 const a: B = (
3645 c(
3646 d(
3647 ˇ
3648 ˇ )
3649 ˇ )
3650 );
3651 "});
3652 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3653 cx.assert_editor_state(indoc! {"
3654 const a: B = (
3655 c(
3656 d(
3657 ˇ
3658 ˇ)
3659 ˇ)
3660 );
3661 "});
3662
3663 // test when current indent is less than suggested indent,
3664 // we adjust line to match suggested indent and move cursor to it
3665 //
3666 // when some other cursor is at word boundary, it should not move
3667 cx.set_state(indoc! {"
3668 const a: B = (
3669 c(
3670 d(
3671 ˇ
3672 ˇ )
3673 ˇ)
3674 );
3675 "});
3676 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3677 cx.assert_editor_state(indoc! {"
3678 const a: B = (
3679 c(
3680 d(
3681 ˇ
3682 ˇ)
3683 ˇ)
3684 );
3685 "});
3686
3687 // test when current indent is more than suggested indent,
3688 // we just move cursor to current indent instead of suggested indent
3689 //
3690 // when no other cursor is at word boundary, all of them should move
3691 cx.set_state(indoc! {"
3692 const a: B = (
3693 c(
3694 d(
3695 ˇ
3696 ˇ )
3697 ˇ )
3698 );
3699 "});
3700 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3701 cx.assert_editor_state(indoc! {"
3702 const a: B = (
3703 c(
3704 d(
3705 ˇ
3706 ˇ)
3707 ˇ)
3708 );
3709 "});
3710 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3711 cx.assert_editor_state(indoc! {"
3712 const a: B = (
3713 c(
3714 d(
3715 ˇ
3716 ˇ)
3717 ˇ)
3718 );
3719 "});
3720
3721 // test when current indent is more than suggested indent,
3722 // we just move cursor to current indent instead of suggested indent
3723 //
3724 // when some other cursor is at word boundary, it doesn't move
3725 cx.set_state(indoc! {"
3726 const a: B = (
3727 c(
3728 d(
3729 ˇ
3730 ˇ )
3731 ˇ)
3732 );
3733 "});
3734 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3735 cx.assert_editor_state(indoc! {"
3736 const a: B = (
3737 c(
3738 d(
3739 ˇ
3740 ˇ)
3741 ˇ)
3742 );
3743 "});
3744
3745 // handle auto-indent when there are multiple cursors on the same line
3746 cx.set_state(indoc! {"
3747 const a: B = (
3748 c(
3749 ˇ ˇ
3750 ˇ )
3751 );
3752 "});
3753 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3754 cx.assert_editor_state(indoc! {"
3755 const a: B = (
3756 c(
3757 ˇ
3758 ˇ)
3759 );
3760 "});
3761}
3762
3763#[gpui::test]
3764async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3765 init_test(cx, |settings| {
3766 settings.defaults.tab_size = NonZeroU32::new(3)
3767 });
3768
3769 let mut cx = EditorTestContext::new(cx).await;
3770 cx.set_state(indoc! {"
3771 ˇ
3772 \t ˇ
3773 \t ˇ
3774 \t ˇ
3775 \t \t\t \t \t\t \t\t \t \t ˇ
3776 "});
3777
3778 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3779 cx.assert_editor_state(indoc! {"
3780 ˇ
3781 \t ˇ
3782 \t ˇ
3783 \t ˇ
3784 \t \t\t \t \t\t \t\t \t \t ˇ
3785 "});
3786}
3787
3788#[gpui::test]
3789async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3790 init_test(cx, |settings| {
3791 settings.defaults.tab_size = NonZeroU32::new(4)
3792 });
3793
3794 let language = Arc::new(
3795 Language::new(
3796 LanguageConfig::default(),
3797 Some(tree_sitter_rust::LANGUAGE.into()),
3798 )
3799 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3800 .unwrap(),
3801 );
3802
3803 let mut cx = EditorTestContext::new(cx).await;
3804 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3805 cx.set_state(indoc! {"
3806 fn a() {
3807 if b {
3808 \t ˇc
3809 }
3810 }
3811 "});
3812
3813 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3814 cx.assert_editor_state(indoc! {"
3815 fn a() {
3816 if b {
3817 ˇc
3818 }
3819 }
3820 "});
3821}
3822
3823#[gpui::test]
3824async fn test_indent_outdent(cx: &mut TestAppContext) {
3825 init_test(cx, |settings| {
3826 settings.defaults.tab_size = NonZeroU32::new(4);
3827 });
3828
3829 let mut cx = EditorTestContext::new(cx).await;
3830
3831 cx.set_state(indoc! {"
3832 «oneˇ» «twoˇ»
3833 three
3834 four
3835 "});
3836 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3837 cx.assert_editor_state(indoc! {"
3838 «oneˇ» «twoˇ»
3839 three
3840 four
3841 "});
3842
3843 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3844 cx.assert_editor_state(indoc! {"
3845 «oneˇ» «twoˇ»
3846 three
3847 four
3848 "});
3849
3850 // select across line ending
3851 cx.set_state(indoc! {"
3852 one two
3853 t«hree
3854 ˇ» four
3855 "});
3856 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3857 cx.assert_editor_state(indoc! {"
3858 one two
3859 t«hree
3860 ˇ» four
3861 "});
3862
3863 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3864 cx.assert_editor_state(indoc! {"
3865 one two
3866 t«hree
3867 ˇ» four
3868 "});
3869
3870 // Ensure that indenting/outdenting works when the cursor is at column 0.
3871 cx.set_state(indoc! {"
3872 one two
3873 ˇthree
3874 four
3875 "});
3876 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3877 cx.assert_editor_state(indoc! {"
3878 one two
3879 ˇthree
3880 four
3881 "});
3882
3883 cx.set_state(indoc! {"
3884 one two
3885 ˇ three
3886 four
3887 "});
3888 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3889 cx.assert_editor_state(indoc! {"
3890 one two
3891 ˇthree
3892 four
3893 "});
3894}
3895
3896#[gpui::test]
3897async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) {
3898 // This is a regression test for issue #33761
3899 init_test(cx, |_| {});
3900
3901 let mut cx = EditorTestContext::new(cx).await;
3902 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3903 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3904
3905 cx.set_state(
3906 r#"ˇ# ingress:
3907ˇ# api:
3908ˇ# enabled: false
3909ˇ# pathType: Prefix
3910ˇ# console:
3911ˇ# enabled: false
3912ˇ# pathType: Prefix
3913"#,
3914 );
3915
3916 // Press tab to indent all lines
3917 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3918
3919 cx.assert_editor_state(
3920 r#" ˇ# ingress:
3921 ˇ# api:
3922 ˇ# enabled: false
3923 ˇ# pathType: Prefix
3924 ˇ# console:
3925 ˇ# enabled: false
3926 ˇ# pathType: Prefix
3927"#,
3928 );
3929}
3930
3931#[gpui::test]
3932async fn test_indent_yaml_non_comments_with_multiple_cursors(cx: &mut TestAppContext) {
3933 // This is a test to make sure our fix for issue #33761 didn't break anything
3934 init_test(cx, |_| {});
3935
3936 let mut cx = EditorTestContext::new(cx).await;
3937 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3938 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3939
3940 cx.set_state(
3941 r#"ˇingress:
3942ˇ api:
3943ˇ enabled: false
3944ˇ pathType: Prefix
3945"#,
3946 );
3947
3948 // Press tab to indent all lines
3949 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3950
3951 cx.assert_editor_state(
3952 r#"ˇingress:
3953 ˇapi:
3954 ˇenabled: false
3955 ˇpathType: Prefix
3956"#,
3957 );
3958}
3959
3960#[gpui::test]
3961async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3962 init_test(cx, |settings| {
3963 settings.defaults.hard_tabs = Some(true);
3964 });
3965
3966 let mut cx = EditorTestContext::new(cx).await;
3967
3968 // select two ranges on one line
3969 cx.set_state(indoc! {"
3970 «oneˇ» «twoˇ»
3971 three
3972 four
3973 "});
3974 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3975 cx.assert_editor_state(indoc! {"
3976 \t«oneˇ» «twoˇ»
3977 three
3978 four
3979 "});
3980 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3981 cx.assert_editor_state(indoc! {"
3982 \t\t«oneˇ» «twoˇ»
3983 three
3984 four
3985 "});
3986 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3987 cx.assert_editor_state(indoc! {"
3988 \t«oneˇ» «twoˇ»
3989 three
3990 four
3991 "});
3992 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3993 cx.assert_editor_state(indoc! {"
3994 «oneˇ» «twoˇ»
3995 three
3996 four
3997 "});
3998
3999 // select across a line ending
4000 cx.set_state(indoc! {"
4001 one two
4002 t«hree
4003 ˇ»four
4004 "});
4005 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4006 cx.assert_editor_state(indoc! {"
4007 one two
4008 \tt«hree
4009 ˇ»four
4010 "});
4011 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4012 cx.assert_editor_state(indoc! {"
4013 one two
4014 \t\tt«hree
4015 ˇ»four
4016 "});
4017 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4018 cx.assert_editor_state(indoc! {"
4019 one two
4020 \tt«hree
4021 ˇ»four
4022 "});
4023 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4024 cx.assert_editor_state(indoc! {"
4025 one two
4026 t«hree
4027 ˇ»four
4028 "});
4029
4030 // Ensure that indenting/outdenting works when the cursor is at column 0.
4031 cx.set_state(indoc! {"
4032 one two
4033 ˇthree
4034 four
4035 "});
4036 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4037 cx.assert_editor_state(indoc! {"
4038 one two
4039 ˇthree
4040 four
4041 "});
4042 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4043 cx.assert_editor_state(indoc! {"
4044 one two
4045 \tˇthree
4046 four
4047 "});
4048 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4049 cx.assert_editor_state(indoc! {"
4050 one two
4051 ˇthree
4052 four
4053 "});
4054}
4055
4056#[gpui::test]
4057fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
4058 init_test(cx, |settings| {
4059 settings.languages.0.extend([
4060 (
4061 "TOML".into(),
4062 LanguageSettingsContent {
4063 tab_size: NonZeroU32::new(2),
4064 ..Default::default()
4065 },
4066 ),
4067 (
4068 "Rust".into(),
4069 LanguageSettingsContent {
4070 tab_size: NonZeroU32::new(4),
4071 ..Default::default()
4072 },
4073 ),
4074 ]);
4075 });
4076
4077 let toml_language = Arc::new(Language::new(
4078 LanguageConfig {
4079 name: "TOML".into(),
4080 ..Default::default()
4081 },
4082 None,
4083 ));
4084 let rust_language = Arc::new(Language::new(
4085 LanguageConfig {
4086 name: "Rust".into(),
4087 ..Default::default()
4088 },
4089 None,
4090 ));
4091
4092 let toml_buffer =
4093 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
4094 let rust_buffer =
4095 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
4096 let multibuffer = cx.new(|cx| {
4097 let mut multibuffer = MultiBuffer::new(ReadWrite);
4098 multibuffer.push_excerpts(
4099 toml_buffer.clone(),
4100 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
4101 cx,
4102 );
4103 multibuffer.push_excerpts(
4104 rust_buffer.clone(),
4105 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
4106 cx,
4107 );
4108 multibuffer
4109 });
4110
4111 cx.add_window(|window, cx| {
4112 let mut editor = build_editor(multibuffer, window, cx);
4113
4114 assert_eq!(
4115 editor.text(cx),
4116 indoc! {"
4117 a = 1
4118 b = 2
4119
4120 const c: usize = 3;
4121 "}
4122 );
4123
4124 select_ranges(
4125 &mut editor,
4126 indoc! {"
4127 «aˇ» = 1
4128 b = 2
4129
4130 «const c:ˇ» usize = 3;
4131 "},
4132 window,
4133 cx,
4134 );
4135
4136 editor.tab(&Tab, window, cx);
4137 assert_text_with_selections(
4138 &mut editor,
4139 indoc! {"
4140 «aˇ» = 1
4141 b = 2
4142
4143 «const c:ˇ» usize = 3;
4144 "},
4145 cx,
4146 );
4147 editor.backtab(&Backtab, window, cx);
4148 assert_text_with_selections(
4149 &mut editor,
4150 indoc! {"
4151 «aˇ» = 1
4152 b = 2
4153
4154 «const c:ˇ» usize = 3;
4155 "},
4156 cx,
4157 );
4158
4159 editor
4160 });
4161}
4162
4163#[gpui::test]
4164async fn test_backspace(cx: &mut TestAppContext) {
4165 init_test(cx, |_| {});
4166
4167 let mut cx = EditorTestContext::new(cx).await;
4168
4169 // Basic backspace
4170 cx.set_state(indoc! {"
4171 onˇe two three
4172 fou«rˇ» five six
4173 seven «ˇeight nine
4174 »ten
4175 "});
4176 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4177 cx.assert_editor_state(indoc! {"
4178 oˇe two three
4179 fouˇ five six
4180 seven ˇten
4181 "});
4182
4183 // Test backspace inside and around indents
4184 cx.set_state(indoc! {"
4185 zero
4186 ˇone
4187 ˇtwo
4188 ˇ ˇ ˇ three
4189 ˇ ˇ four
4190 "});
4191 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4192 cx.assert_editor_state(indoc! {"
4193 zero
4194 ˇone
4195 ˇtwo
4196 ˇ threeˇ four
4197 "});
4198}
4199
4200#[gpui::test]
4201async fn test_delete(cx: &mut TestAppContext) {
4202 init_test(cx, |_| {});
4203
4204 let mut cx = EditorTestContext::new(cx).await;
4205 cx.set_state(indoc! {"
4206 onˇe two three
4207 fou«rˇ» five six
4208 seven «ˇeight nine
4209 »ten
4210 "});
4211 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
4212 cx.assert_editor_state(indoc! {"
4213 onˇ two three
4214 fouˇ five six
4215 seven ˇten
4216 "});
4217}
4218
4219#[gpui::test]
4220fn test_delete_line(cx: &mut TestAppContext) {
4221 init_test(cx, |_| {});
4222
4223 let editor = cx.add_window(|window, cx| {
4224 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4225 build_editor(buffer, window, cx)
4226 });
4227 _ = editor.update(cx, |editor, window, cx| {
4228 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4229 s.select_display_ranges([
4230 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4231 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4232 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4233 ])
4234 });
4235 editor.delete_line(&DeleteLine, window, cx);
4236 assert_eq!(editor.display_text(cx), "ghi");
4237 assert_eq!(
4238 editor.selections.display_ranges(cx),
4239 vec![
4240 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
4241 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
4242 ]
4243 );
4244 });
4245
4246 let editor = cx.add_window(|window, cx| {
4247 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4248 build_editor(buffer, window, cx)
4249 });
4250 _ = editor.update(cx, |editor, window, cx| {
4251 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4252 s.select_display_ranges([
4253 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
4254 ])
4255 });
4256 editor.delete_line(&DeleteLine, window, cx);
4257 assert_eq!(editor.display_text(cx), "ghi\n");
4258 assert_eq!(
4259 editor.selections.display_ranges(cx),
4260 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
4261 );
4262 });
4263}
4264
4265#[gpui::test]
4266fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
4267 init_test(cx, |_| {});
4268
4269 cx.add_window(|window, cx| {
4270 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4271 let mut editor = build_editor(buffer.clone(), window, cx);
4272 let buffer = buffer.read(cx).as_singleton().unwrap();
4273
4274 assert_eq!(
4275 editor.selections.ranges::<Point>(cx),
4276 &[Point::new(0, 0)..Point::new(0, 0)]
4277 );
4278
4279 // When on single line, replace newline at end by space
4280 editor.join_lines(&JoinLines, window, cx);
4281 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4282 assert_eq!(
4283 editor.selections.ranges::<Point>(cx),
4284 &[Point::new(0, 3)..Point::new(0, 3)]
4285 );
4286
4287 // When multiple lines are selected, remove newlines that are spanned by the selection
4288 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4289 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
4290 });
4291 editor.join_lines(&JoinLines, window, cx);
4292 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
4293 assert_eq!(
4294 editor.selections.ranges::<Point>(cx),
4295 &[Point::new(0, 11)..Point::new(0, 11)]
4296 );
4297
4298 // Undo should be transactional
4299 editor.undo(&Undo, window, cx);
4300 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4301 assert_eq!(
4302 editor.selections.ranges::<Point>(cx),
4303 &[Point::new(0, 5)..Point::new(2, 2)]
4304 );
4305
4306 // When joining an empty line don't insert a space
4307 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4308 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
4309 });
4310 editor.join_lines(&JoinLines, window, cx);
4311 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
4312 assert_eq!(
4313 editor.selections.ranges::<Point>(cx),
4314 [Point::new(2, 3)..Point::new(2, 3)]
4315 );
4316
4317 // We can remove trailing newlines
4318 editor.join_lines(&JoinLines, window, cx);
4319 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4320 assert_eq!(
4321 editor.selections.ranges::<Point>(cx),
4322 [Point::new(2, 3)..Point::new(2, 3)]
4323 );
4324
4325 // We don't blow up on the last line
4326 editor.join_lines(&JoinLines, window, cx);
4327 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4328 assert_eq!(
4329 editor.selections.ranges::<Point>(cx),
4330 [Point::new(2, 3)..Point::new(2, 3)]
4331 );
4332
4333 // reset to test indentation
4334 editor.buffer.update(cx, |buffer, cx| {
4335 buffer.edit(
4336 [
4337 (Point::new(1, 0)..Point::new(1, 2), " "),
4338 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
4339 ],
4340 None,
4341 cx,
4342 )
4343 });
4344
4345 // We remove any leading spaces
4346 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
4347 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4348 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
4349 });
4350 editor.join_lines(&JoinLines, window, cx);
4351 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
4352
4353 // We don't insert a space for a line containing only spaces
4354 editor.join_lines(&JoinLines, window, cx);
4355 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
4356
4357 // We ignore any leading tabs
4358 editor.join_lines(&JoinLines, window, cx);
4359 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
4360
4361 editor
4362 });
4363}
4364
4365#[gpui::test]
4366fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
4367 init_test(cx, |_| {});
4368
4369 cx.add_window(|window, cx| {
4370 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4371 let mut editor = build_editor(buffer.clone(), window, cx);
4372 let buffer = buffer.read(cx).as_singleton().unwrap();
4373
4374 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4375 s.select_ranges([
4376 Point::new(0, 2)..Point::new(1, 1),
4377 Point::new(1, 2)..Point::new(1, 2),
4378 Point::new(3, 1)..Point::new(3, 2),
4379 ])
4380 });
4381
4382 editor.join_lines(&JoinLines, window, cx);
4383 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
4384
4385 assert_eq!(
4386 editor.selections.ranges::<Point>(cx),
4387 [
4388 Point::new(0, 7)..Point::new(0, 7),
4389 Point::new(1, 3)..Point::new(1, 3)
4390 ]
4391 );
4392 editor
4393 });
4394}
4395
4396#[gpui::test]
4397async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
4398 init_test(cx, |_| {});
4399
4400 let mut cx = EditorTestContext::new(cx).await;
4401
4402 let diff_base = r#"
4403 Line 0
4404 Line 1
4405 Line 2
4406 Line 3
4407 "#
4408 .unindent();
4409
4410 cx.set_state(
4411 &r#"
4412 ˇLine 0
4413 Line 1
4414 Line 2
4415 Line 3
4416 "#
4417 .unindent(),
4418 );
4419
4420 cx.set_head_text(&diff_base);
4421 executor.run_until_parked();
4422
4423 // Join lines
4424 cx.update_editor(|editor, window, cx| {
4425 editor.join_lines(&JoinLines, window, cx);
4426 });
4427 executor.run_until_parked();
4428
4429 cx.assert_editor_state(
4430 &r#"
4431 Line 0ˇ Line 1
4432 Line 2
4433 Line 3
4434 "#
4435 .unindent(),
4436 );
4437 // Join again
4438 cx.update_editor(|editor, window, cx| {
4439 editor.join_lines(&JoinLines, window, cx);
4440 });
4441 executor.run_until_parked();
4442
4443 cx.assert_editor_state(
4444 &r#"
4445 Line 0 Line 1ˇ Line 2
4446 Line 3
4447 "#
4448 .unindent(),
4449 );
4450}
4451
4452#[gpui::test]
4453async fn test_custom_newlines_cause_no_false_positive_diffs(
4454 executor: BackgroundExecutor,
4455 cx: &mut TestAppContext,
4456) {
4457 init_test(cx, |_| {});
4458 let mut cx = EditorTestContext::new(cx).await;
4459 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
4460 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
4461 executor.run_until_parked();
4462
4463 cx.update_editor(|editor, window, cx| {
4464 let snapshot = editor.snapshot(window, cx);
4465 assert_eq!(
4466 snapshot
4467 .buffer_snapshot
4468 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
4469 .collect::<Vec<_>>(),
4470 Vec::new(),
4471 "Should not have any diffs for files with custom newlines"
4472 );
4473 });
4474}
4475
4476#[gpui::test]
4477async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
4478 init_test(cx, |_| {});
4479
4480 let mut cx = EditorTestContext::new(cx).await;
4481
4482 // Test sort_lines_case_insensitive()
4483 cx.set_state(indoc! {"
4484 «z
4485 y
4486 x
4487 Z
4488 Y
4489 Xˇ»
4490 "});
4491 cx.update_editor(|e, window, cx| {
4492 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
4493 });
4494 cx.assert_editor_state(indoc! {"
4495 «x
4496 X
4497 y
4498 Y
4499 z
4500 Zˇ»
4501 "});
4502
4503 // Test sort_lines_by_length()
4504 //
4505 // Demonstrates:
4506 // - ∞ is 3 bytes UTF-8, but sorted by its char count (1)
4507 // - sort is stable
4508 cx.set_state(indoc! {"
4509 «123
4510 æ
4511 12
4512 ∞
4513 1
4514 æˇ»
4515 "});
4516 cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
4517 cx.assert_editor_state(indoc! {"
4518 «æ
4519 ∞
4520 1
4521 æ
4522 12
4523 123ˇ»
4524 "});
4525
4526 // Test reverse_lines()
4527 cx.set_state(indoc! {"
4528 «5
4529 4
4530 3
4531 2
4532 1ˇ»
4533 "});
4534 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4535 cx.assert_editor_state(indoc! {"
4536 «1
4537 2
4538 3
4539 4
4540 5ˇ»
4541 "});
4542
4543 // Skip testing shuffle_line()
4544
4545 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4546 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4547
4548 // Don't manipulate when cursor is on single line, but expand the selection
4549 cx.set_state(indoc! {"
4550 ddˇdd
4551 ccc
4552 bb
4553 a
4554 "});
4555 cx.update_editor(|e, window, cx| {
4556 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4557 });
4558 cx.assert_editor_state(indoc! {"
4559 «ddddˇ»
4560 ccc
4561 bb
4562 a
4563 "});
4564
4565 // Basic manipulate case
4566 // Start selection moves to column 0
4567 // End of selection shrinks to fit shorter line
4568 cx.set_state(indoc! {"
4569 dd«d
4570 ccc
4571 bb
4572 aaaaaˇ»
4573 "});
4574 cx.update_editor(|e, window, cx| {
4575 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4576 });
4577 cx.assert_editor_state(indoc! {"
4578 «aaaaa
4579 bb
4580 ccc
4581 dddˇ»
4582 "});
4583
4584 // Manipulate case with newlines
4585 cx.set_state(indoc! {"
4586 dd«d
4587 ccc
4588
4589 bb
4590 aaaaa
4591
4592 ˇ»
4593 "});
4594 cx.update_editor(|e, window, cx| {
4595 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4596 });
4597 cx.assert_editor_state(indoc! {"
4598 «
4599
4600 aaaaa
4601 bb
4602 ccc
4603 dddˇ»
4604
4605 "});
4606
4607 // Adding new line
4608 cx.set_state(indoc! {"
4609 aa«a
4610 bbˇ»b
4611 "});
4612 cx.update_editor(|e, window, cx| {
4613 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4614 });
4615 cx.assert_editor_state(indoc! {"
4616 «aaa
4617 bbb
4618 added_lineˇ»
4619 "});
4620
4621 // Removing line
4622 cx.set_state(indoc! {"
4623 aa«a
4624 bbbˇ»
4625 "});
4626 cx.update_editor(|e, window, cx| {
4627 e.manipulate_immutable_lines(window, cx, |lines| {
4628 lines.pop();
4629 })
4630 });
4631 cx.assert_editor_state(indoc! {"
4632 «aaaˇ»
4633 "});
4634
4635 // Removing all lines
4636 cx.set_state(indoc! {"
4637 aa«a
4638 bbbˇ»
4639 "});
4640 cx.update_editor(|e, window, cx| {
4641 e.manipulate_immutable_lines(window, cx, |lines| {
4642 lines.drain(..);
4643 })
4644 });
4645 cx.assert_editor_state(indoc! {"
4646 ˇ
4647 "});
4648}
4649
4650#[gpui::test]
4651async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4652 init_test(cx, |_| {});
4653
4654 let mut cx = EditorTestContext::new(cx).await;
4655
4656 // Consider continuous selection as single selection
4657 cx.set_state(indoc! {"
4658 Aaa«aa
4659 cˇ»c«c
4660 bb
4661 aaaˇ»aa
4662 "});
4663 cx.update_editor(|e, window, cx| {
4664 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4665 });
4666 cx.assert_editor_state(indoc! {"
4667 «Aaaaa
4668 ccc
4669 bb
4670 aaaaaˇ»
4671 "});
4672
4673 cx.set_state(indoc! {"
4674 Aaa«aa
4675 cˇ»c«c
4676 bb
4677 aaaˇ»aa
4678 "});
4679 cx.update_editor(|e, window, cx| {
4680 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4681 });
4682 cx.assert_editor_state(indoc! {"
4683 «Aaaaa
4684 ccc
4685 bbˇ»
4686 "});
4687
4688 // Consider non continuous selection as distinct dedup operations
4689 cx.set_state(indoc! {"
4690 «aaaaa
4691 bb
4692 aaaaa
4693 aaaaaˇ»
4694
4695 aaa«aaˇ»
4696 "});
4697 cx.update_editor(|e, window, cx| {
4698 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4699 });
4700 cx.assert_editor_state(indoc! {"
4701 «aaaaa
4702 bbˇ»
4703
4704 «aaaaaˇ»
4705 "});
4706}
4707
4708#[gpui::test]
4709async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4710 init_test(cx, |_| {});
4711
4712 let mut cx = EditorTestContext::new(cx).await;
4713
4714 cx.set_state(indoc! {"
4715 «Aaa
4716 aAa
4717 Aaaˇ»
4718 "});
4719 cx.update_editor(|e, window, cx| {
4720 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4721 });
4722 cx.assert_editor_state(indoc! {"
4723 «Aaa
4724 aAaˇ»
4725 "});
4726
4727 cx.set_state(indoc! {"
4728 «Aaa
4729 aAa
4730 aaAˇ»
4731 "});
4732 cx.update_editor(|e, window, cx| {
4733 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4734 });
4735 cx.assert_editor_state(indoc! {"
4736 «Aaaˇ»
4737 "});
4738}
4739
4740#[gpui::test]
4741async fn test_wrap_in_tag_single_selection(cx: &mut TestAppContext) {
4742 init_test(cx, |_| {});
4743
4744 let mut cx = EditorTestContext::new(cx).await;
4745
4746 let js_language = Arc::new(Language::new(
4747 LanguageConfig {
4748 name: "JavaScript".into(),
4749 wrap_characters: Some(language::WrapCharactersConfig {
4750 start_prefix: "<".into(),
4751 start_suffix: ">".into(),
4752 end_prefix: "</".into(),
4753 end_suffix: ">".into(),
4754 }),
4755 ..LanguageConfig::default()
4756 },
4757 None,
4758 ));
4759
4760 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
4761
4762 cx.set_state(indoc! {"
4763 «testˇ»
4764 "});
4765 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4766 cx.assert_editor_state(indoc! {"
4767 <«ˇ»>test</«ˇ»>
4768 "});
4769
4770 cx.set_state(indoc! {"
4771 «test
4772 testˇ»
4773 "});
4774 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4775 cx.assert_editor_state(indoc! {"
4776 <«ˇ»>test
4777 test</«ˇ»>
4778 "});
4779
4780 cx.set_state(indoc! {"
4781 teˇst
4782 "});
4783 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4784 cx.assert_editor_state(indoc! {"
4785 te<«ˇ»></«ˇ»>st
4786 "});
4787}
4788
4789#[gpui::test]
4790async fn test_wrap_in_tag_multi_selection(cx: &mut TestAppContext) {
4791 init_test(cx, |_| {});
4792
4793 let mut cx = EditorTestContext::new(cx).await;
4794
4795 let js_language = Arc::new(Language::new(
4796 LanguageConfig {
4797 name: "JavaScript".into(),
4798 wrap_characters: Some(language::WrapCharactersConfig {
4799 start_prefix: "<".into(),
4800 start_suffix: ">".into(),
4801 end_prefix: "</".into(),
4802 end_suffix: ">".into(),
4803 }),
4804 ..LanguageConfig::default()
4805 },
4806 None,
4807 ));
4808
4809 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
4810
4811 cx.set_state(indoc! {"
4812 «testˇ»
4813 «testˇ» «testˇ»
4814 «testˇ»
4815 "});
4816 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4817 cx.assert_editor_state(indoc! {"
4818 <«ˇ»>test</«ˇ»>
4819 <«ˇ»>test</«ˇ»> <«ˇ»>test</«ˇ»>
4820 <«ˇ»>test</«ˇ»>
4821 "});
4822
4823 cx.set_state(indoc! {"
4824 «test
4825 testˇ»
4826 «test
4827 testˇ»
4828 "});
4829 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4830 cx.assert_editor_state(indoc! {"
4831 <«ˇ»>test
4832 test</«ˇ»>
4833 <«ˇ»>test
4834 test</«ˇ»>
4835 "});
4836}
4837
4838#[gpui::test]
4839async fn test_wrap_in_tag_does_nothing_in_unsupported_languages(cx: &mut TestAppContext) {
4840 init_test(cx, |_| {});
4841
4842 let mut cx = EditorTestContext::new(cx).await;
4843
4844 let plaintext_language = Arc::new(Language::new(
4845 LanguageConfig {
4846 name: "Plain Text".into(),
4847 ..LanguageConfig::default()
4848 },
4849 None,
4850 ));
4851
4852 cx.update_buffer(|buffer, cx| buffer.set_language(Some(plaintext_language), cx));
4853
4854 cx.set_state(indoc! {"
4855 «testˇ»
4856 "});
4857 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4858 cx.assert_editor_state(indoc! {"
4859 «testˇ»
4860 "});
4861}
4862
4863#[gpui::test]
4864async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
4865 init_test(cx, |_| {});
4866
4867 let mut cx = EditorTestContext::new(cx).await;
4868
4869 // Manipulate with multiple selections on a single line
4870 cx.set_state(indoc! {"
4871 dd«dd
4872 cˇ»c«c
4873 bb
4874 aaaˇ»aa
4875 "});
4876 cx.update_editor(|e, window, cx| {
4877 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4878 });
4879 cx.assert_editor_state(indoc! {"
4880 «aaaaa
4881 bb
4882 ccc
4883 ddddˇ»
4884 "});
4885
4886 // Manipulate with multiple disjoin selections
4887 cx.set_state(indoc! {"
4888 5«
4889 4
4890 3
4891 2
4892 1ˇ»
4893
4894 dd«dd
4895 ccc
4896 bb
4897 aaaˇ»aa
4898 "});
4899 cx.update_editor(|e, window, cx| {
4900 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4901 });
4902 cx.assert_editor_state(indoc! {"
4903 «1
4904 2
4905 3
4906 4
4907 5ˇ»
4908
4909 «aaaaa
4910 bb
4911 ccc
4912 ddddˇ»
4913 "});
4914
4915 // Adding lines on each selection
4916 cx.set_state(indoc! {"
4917 2«
4918 1ˇ»
4919
4920 bb«bb
4921 aaaˇ»aa
4922 "});
4923 cx.update_editor(|e, window, cx| {
4924 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
4925 });
4926 cx.assert_editor_state(indoc! {"
4927 «2
4928 1
4929 added lineˇ»
4930
4931 «bbbb
4932 aaaaa
4933 added lineˇ»
4934 "});
4935
4936 // Removing lines on each selection
4937 cx.set_state(indoc! {"
4938 2«
4939 1ˇ»
4940
4941 bb«bb
4942 aaaˇ»aa
4943 "});
4944 cx.update_editor(|e, window, cx| {
4945 e.manipulate_immutable_lines(window, cx, |lines| {
4946 lines.pop();
4947 })
4948 });
4949 cx.assert_editor_state(indoc! {"
4950 «2ˇ»
4951
4952 «bbbbˇ»
4953 "});
4954}
4955
4956#[gpui::test]
4957async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
4958 init_test(cx, |settings| {
4959 settings.defaults.tab_size = NonZeroU32::new(3)
4960 });
4961
4962 let mut cx = EditorTestContext::new(cx).await;
4963
4964 // MULTI SELECTION
4965 // Ln.1 "«" tests empty lines
4966 // Ln.9 tests just leading whitespace
4967 cx.set_state(indoc! {"
4968 «
4969 abc // No indentationˇ»
4970 «\tabc // 1 tabˇ»
4971 \t\tabc « ˇ» // 2 tabs
4972 \t ab«c // Tab followed by space
4973 \tabc // Space followed by tab (3 spaces should be the result)
4974 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4975 abˇ»ˇc ˇ ˇ // Already space indented«
4976 \t
4977 \tabc\tdef // Only the leading tab is manipulatedˇ»
4978 "});
4979 cx.update_editor(|e, window, cx| {
4980 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4981 });
4982 cx.assert_editor_state(
4983 indoc! {"
4984 «
4985 abc // No indentation
4986 abc // 1 tab
4987 abc // 2 tabs
4988 abc // Tab followed by space
4989 abc // Space followed by tab (3 spaces should be the result)
4990 abc // Mixed indentation (tab conversion depends on the column)
4991 abc // Already space indented
4992 ·
4993 abc\tdef // Only the leading tab is manipulatedˇ»
4994 "}
4995 .replace("·", "")
4996 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4997 );
4998
4999 // Test on just a few lines, the others should remain unchanged
5000 // Only lines (3, 5, 10, 11) should change
5001 cx.set_state(
5002 indoc! {"
5003 ·
5004 abc // No indentation
5005 \tabcˇ // 1 tab
5006 \t\tabc // 2 tabs
5007 \t abcˇ // Tab followed by space
5008 \tabc // Space followed by tab (3 spaces should be the result)
5009 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5010 abc // Already space indented
5011 «\t
5012 \tabc\tdef // Only the leading tab is manipulatedˇ»
5013 "}
5014 .replace("·", "")
5015 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5016 );
5017 cx.update_editor(|e, window, cx| {
5018 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5019 });
5020 cx.assert_editor_state(
5021 indoc! {"
5022 ·
5023 abc // No indentation
5024 « abc // 1 tabˇ»
5025 \t\tabc // 2 tabs
5026 « abc // Tab followed by spaceˇ»
5027 \tabc // Space followed by tab (3 spaces should be the result)
5028 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5029 abc // Already space indented
5030 « ·
5031 abc\tdef // Only the leading tab is manipulatedˇ»
5032 "}
5033 .replace("·", "")
5034 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5035 );
5036
5037 // SINGLE SELECTION
5038 // Ln.1 "«" tests empty lines
5039 // Ln.9 tests just leading whitespace
5040 cx.set_state(indoc! {"
5041 «
5042 abc // No indentation
5043 \tabc // 1 tab
5044 \t\tabc // 2 tabs
5045 \t abc // Tab followed by space
5046 \tabc // Space followed by tab (3 spaces should be the result)
5047 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5048 abc // Already space indented
5049 \t
5050 \tabc\tdef // Only the leading tab is manipulatedˇ»
5051 "});
5052 cx.update_editor(|e, window, cx| {
5053 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5054 });
5055 cx.assert_editor_state(
5056 indoc! {"
5057 «
5058 abc // No indentation
5059 abc // 1 tab
5060 abc // 2 tabs
5061 abc // Tab followed by space
5062 abc // Space followed by tab (3 spaces should be the result)
5063 abc // Mixed indentation (tab conversion depends on the column)
5064 abc // Already space indented
5065 ·
5066 abc\tdef // Only the leading tab is manipulatedˇ»
5067 "}
5068 .replace("·", "")
5069 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5070 );
5071}
5072
5073#[gpui::test]
5074async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
5075 init_test(cx, |settings| {
5076 settings.defaults.tab_size = NonZeroU32::new(3)
5077 });
5078
5079 let mut cx = EditorTestContext::new(cx).await;
5080
5081 // MULTI SELECTION
5082 // Ln.1 "«" tests empty lines
5083 // Ln.11 tests just leading whitespace
5084 cx.set_state(indoc! {"
5085 «
5086 abˇ»ˇc // No indentation
5087 abc ˇ ˇ // 1 space (< 3 so dont convert)
5088 abc « // 2 spaces (< 3 so dont convert)
5089 abc // 3 spaces (convert)
5090 abc ˇ» // 5 spaces (1 tab + 2 spaces)
5091 «\tˇ»\t«\tˇ»abc // Already tab indented
5092 «\t abc // Tab followed by space
5093 \tabc // Space followed by tab (should be consumed due to tab)
5094 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5095 \tˇ» «\t
5096 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
5097 "});
5098 cx.update_editor(|e, window, cx| {
5099 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5100 });
5101 cx.assert_editor_state(indoc! {"
5102 «
5103 abc // No indentation
5104 abc // 1 space (< 3 so dont convert)
5105 abc // 2 spaces (< 3 so dont convert)
5106 \tabc // 3 spaces (convert)
5107 \t abc // 5 spaces (1 tab + 2 spaces)
5108 \t\t\tabc // Already tab indented
5109 \t abc // Tab followed by space
5110 \tabc // Space followed by tab (should be consumed due to tab)
5111 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5112 \t\t\t
5113 \tabc \t // Only the leading spaces should be convertedˇ»
5114 "});
5115
5116 // Test on just a few lines, the other should remain unchanged
5117 // Only lines (4, 8, 11, 12) should change
5118 cx.set_state(
5119 indoc! {"
5120 ·
5121 abc // No indentation
5122 abc // 1 space (< 3 so dont convert)
5123 abc // 2 spaces (< 3 so dont convert)
5124 « abc // 3 spaces (convert)ˇ»
5125 abc // 5 spaces (1 tab + 2 spaces)
5126 \t\t\tabc // Already tab indented
5127 \t abc // Tab followed by space
5128 \tabc ˇ // Space followed by tab (should be consumed due to tab)
5129 \t\t \tabc // Mixed indentation
5130 \t \t \t \tabc // Mixed indentation
5131 \t \tˇ
5132 « abc \t // Only the leading spaces should be convertedˇ»
5133 "}
5134 .replace("·", "")
5135 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5136 );
5137 cx.update_editor(|e, window, cx| {
5138 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5139 });
5140 cx.assert_editor_state(
5141 indoc! {"
5142 ·
5143 abc // No indentation
5144 abc // 1 space (< 3 so dont convert)
5145 abc // 2 spaces (< 3 so dont convert)
5146 «\tabc // 3 spaces (convert)ˇ»
5147 abc // 5 spaces (1 tab + 2 spaces)
5148 \t\t\tabc // Already tab indented
5149 \t abc // Tab followed by space
5150 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
5151 \t\t \tabc // Mixed indentation
5152 \t \t \t \tabc // Mixed indentation
5153 «\t\t\t
5154 \tabc \t // Only the leading spaces should be convertedˇ»
5155 "}
5156 .replace("·", "")
5157 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5158 );
5159
5160 // SINGLE SELECTION
5161 // Ln.1 "«" tests empty lines
5162 // Ln.11 tests just leading whitespace
5163 cx.set_state(indoc! {"
5164 «
5165 abc // No indentation
5166 abc // 1 space (< 3 so dont convert)
5167 abc // 2 spaces (< 3 so dont convert)
5168 abc // 3 spaces (convert)
5169 abc // 5 spaces (1 tab + 2 spaces)
5170 \t\t\tabc // Already tab indented
5171 \t abc // Tab followed by space
5172 \tabc // Space followed by tab (should be consumed due to tab)
5173 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5174 \t \t
5175 abc \t // Only the leading spaces should be convertedˇ»
5176 "});
5177 cx.update_editor(|e, window, cx| {
5178 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5179 });
5180 cx.assert_editor_state(indoc! {"
5181 «
5182 abc // No indentation
5183 abc // 1 space (< 3 so dont convert)
5184 abc // 2 spaces (< 3 so dont convert)
5185 \tabc // 3 spaces (convert)
5186 \t abc // 5 spaces (1 tab + 2 spaces)
5187 \t\t\tabc // Already tab indented
5188 \t abc // Tab followed by space
5189 \tabc // Space followed by tab (should be consumed due to tab)
5190 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5191 \t\t\t
5192 \tabc \t // Only the leading spaces should be convertedˇ»
5193 "});
5194}
5195
5196#[gpui::test]
5197async fn test_toggle_case(cx: &mut TestAppContext) {
5198 init_test(cx, |_| {});
5199
5200 let mut cx = EditorTestContext::new(cx).await;
5201
5202 // If all lower case -> upper case
5203 cx.set_state(indoc! {"
5204 «hello worldˇ»
5205 "});
5206 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5207 cx.assert_editor_state(indoc! {"
5208 «HELLO WORLDˇ»
5209 "});
5210
5211 // If all upper case -> lower case
5212 cx.set_state(indoc! {"
5213 «HELLO WORLDˇ»
5214 "});
5215 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5216 cx.assert_editor_state(indoc! {"
5217 «hello worldˇ»
5218 "});
5219
5220 // If any upper case characters are identified -> lower case
5221 // This matches JetBrains IDEs
5222 cx.set_state(indoc! {"
5223 «hEllo worldˇ»
5224 "});
5225 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5226 cx.assert_editor_state(indoc! {"
5227 «hello worldˇ»
5228 "});
5229}
5230
5231#[gpui::test]
5232async fn test_convert_to_sentence_case(cx: &mut TestAppContext) {
5233 init_test(cx, |_| {});
5234
5235 let mut cx = EditorTestContext::new(cx).await;
5236
5237 cx.set_state(indoc! {"
5238 «implement-windows-supportˇ»
5239 "});
5240 cx.update_editor(|e, window, cx| {
5241 e.convert_to_sentence_case(&ConvertToSentenceCase, window, cx)
5242 });
5243 cx.assert_editor_state(indoc! {"
5244 «Implement windows supportˇ»
5245 "});
5246}
5247
5248#[gpui::test]
5249async fn test_manipulate_text(cx: &mut TestAppContext) {
5250 init_test(cx, |_| {});
5251
5252 let mut cx = EditorTestContext::new(cx).await;
5253
5254 // Test convert_to_upper_case()
5255 cx.set_state(indoc! {"
5256 «hello worldˇ»
5257 "});
5258 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5259 cx.assert_editor_state(indoc! {"
5260 «HELLO WORLDˇ»
5261 "});
5262
5263 // Test convert_to_lower_case()
5264 cx.set_state(indoc! {"
5265 «HELLO WORLDˇ»
5266 "});
5267 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
5268 cx.assert_editor_state(indoc! {"
5269 «hello worldˇ»
5270 "});
5271
5272 // Test multiple line, single selection case
5273 cx.set_state(indoc! {"
5274 «The quick brown
5275 fox jumps over
5276 the lazy dogˇ»
5277 "});
5278 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
5279 cx.assert_editor_state(indoc! {"
5280 «The Quick Brown
5281 Fox Jumps Over
5282 The Lazy Dogˇ»
5283 "});
5284
5285 // Test multiple line, single selection case
5286 cx.set_state(indoc! {"
5287 «The quick brown
5288 fox jumps over
5289 the lazy dogˇ»
5290 "});
5291 cx.update_editor(|e, window, cx| {
5292 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
5293 });
5294 cx.assert_editor_state(indoc! {"
5295 «TheQuickBrown
5296 FoxJumpsOver
5297 TheLazyDogˇ»
5298 "});
5299
5300 // From here on out, test more complex cases of manipulate_text()
5301
5302 // Test no selection case - should affect words cursors are in
5303 // Cursor at beginning, middle, and end of word
5304 cx.set_state(indoc! {"
5305 ˇhello big beauˇtiful worldˇ
5306 "});
5307 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5308 cx.assert_editor_state(indoc! {"
5309 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
5310 "});
5311
5312 // Test multiple selections on a single line and across multiple lines
5313 cx.set_state(indoc! {"
5314 «Theˇ» quick «brown
5315 foxˇ» jumps «overˇ»
5316 the «lazyˇ» dog
5317 "});
5318 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5319 cx.assert_editor_state(indoc! {"
5320 «THEˇ» quick «BROWN
5321 FOXˇ» jumps «OVERˇ»
5322 the «LAZYˇ» dog
5323 "});
5324
5325 // Test case where text length grows
5326 cx.set_state(indoc! {"
5327 «tschüߡ»
5328 "});
5329 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5330 cx.assert_editor_state(indoc! {"
5331 «TSCHÜSSˇ»
5332 "});
5333
5334 // Test to make sure we don't crash when text shrinks
5335 cx.set_state(indoc! {"
5336 aaa_bbbˇ
5337 "});
5338 cx.update_editor(|e, window, cx| {
5339 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5340 });
5341 cx.assert_editor_state(indoc! {"
5342 «aaaBbbˇ»
5343 "});
5344
5345 // Test to make sure we all aware of the fact that each word can grow and shrink
5346 // Final selections should be aware of this fact
5347 cx.set_state(indoc! {"
5348 aaa_bˇbb bbˇb_ccc ˇccc_ddd
5349 "});
5350 cx.update_editor(|e, window, cx| {
5351 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5352 });
5353 cx.assert_editor_state(indoc! {"
5354 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
5355 "});
5356
5357 cx.set_state(indoc! {"
5358 «hElLo, WoRld!ˇ»
5359 "});
5360 cx.update_editor(|e, window, cx| {
5361 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
5362 });
5363 cx.assert_editor_state(indoc! {"
5364 «HeLlO, wOrLD!ˇ»
5365 "});
5366
5367 // Test selections with `line_mode = true`.
5368 cx.update_editor(|editor, _window, _cx| editor.selections.line_mode = true);
5369 cx.set_state(indoc! {"
5370 «The quick brown
5371 fox jumps over
5372 tˇ»he lazy dog
5373 "});
5374 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5375 cx.assert_editor_state(indoc! {"
5376 «THE QUICK BROWN
5377 FOX JUMPS OVER
5378 THE LAZY DOGˇ»
5379 "});
5380}
5381
5382#[gpui::test]
5383fn test_duplicate_line(cx: &mut TestAppContext) {
5384 init_test(cx, |_| {});
5385
5386 let editor = cx.add_window(|window, cx| {
5387 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5388 build_editor(buffer, window, cx)
5389 });
5390 _ = editor.update(cx, |editor, window, cx| {
5391 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5392 s.select_display_ranges([
5393 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5394 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5395 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5396 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5397 ])
5398 });
5399 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5400 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5401 assert_eq!(
5402 editor.selections.display_ranges(cx),
5403 vec![
5404 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
5405 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
5406 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5407 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5408 ]
5409 );
5410 });
5411
5412 let editor = cx.add_window(|window, cx| {
5413 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5414 build_editor(buffer, window, cx)
5415 });
5416 _ = editor.update(cx, |editor, window, cx| {
5417 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5418 s.select_display_ranges([
5419 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5420 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5421 ])
5422 });
5423 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5424 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5425 assert_eq!(
5426 editor.selections.display_ranges(cx),
5427 vec![
5428 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
5429 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
5430 ]
5431 );
5432 });
5433
5434 // With `move_upwards` the selections stay in place, except for
5435 // the lines inserted above them
5436 let editor = cx.add_window(|window, cx| {
5437 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5438 build_editor(buffer, window, cx)
5439 });
5440 _ = editor.update(cx, |editor, window, cx| {
5441 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5442 s.select_display_ranges([
5443 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5444 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5445 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5446 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5447 ])
5448 });
5449 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5450 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5451 assert_eq!(
5452 editor.selections.display_ranges(cx),
5453 vec![
5454 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5455 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5456 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
5457 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5458 ]
5459 );
5460 });
5461
5462 let editor = cx.add_window(|window, cx| {
5463 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5464 build_editor(buffer, window, cx)
5465 });
5466 _ = editor.update(cx, |editor, window, cx| {
5467 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5468 s.select_display_ranges([
5469 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5470 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5471 ])
5472 });
5473 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5474 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5475 assert_eq!(
5476 editor.selections.display_ranges(cx),
5477 vec![
5478 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5479 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5480 ]
5481 );
5482 });
5483
5484 let editor = cx.add_window(|window, cx| {
5485 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5486 build_editor(buffer, window, cx)
5487 });
5488 _ = editor.update(cx, |editor, window, cx| {
5489 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5490 s.select_display_ranges([
5491 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5492 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5493 ])
5494 });
5495 editor.duplicate_selection(&DuplicateSelection, window, cx);
5496 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
5497 assert_eq!(
5498 editor.selections.display_ranges(cx),
5499 vec![
5500 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5501 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
5502 ]
5503 );
5504 });
5505}
5506
5507#[gpui::test]
5508fn test_move_line_up_down(cx: &mut TestAppContext) {
5509 init_test(cx, |_| {});
5510
5511 let editor = cx.add_window(|window, cx| {
5512 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5513 build_editor(buffer, window, cx)
5514 });
5515 _ = editor.update(cx, |editor, window, cx| {
5516 editor.fold_creases(
5517 vec![
5518 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5519 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5520 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5521 ],
5522 true,
5523 window,
5524 cx,
5525 );
5526 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5527 s.select_display_ranges([
5528 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5529 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5530 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5531 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
5532 ])
5533 });
5534 assert_eq!(
5535 editor.display_text(cx),
5536 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
5537 );
5538
5539 editor.move_line_up(&MoveLineUp, window, cx);
5540 assert_eq!(
5541 editor.display_text(cx),
5542 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
5543 );
5544 assert_eq!(
5545 editor.selections.display_ranges(cx),
5546 vec![
5547 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5548 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5549 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5550 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5551 ]
5552 );
5553 });
5554
5555 _ = editor.update(cx, |editor, window, cx| {
5556 editor.move_line_down(&MoveLineDown, window, cx);
5557 assert_eq!(
5558 editor.display_text(cx),
5559 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
5560 );
5561 assert_eq!(
5562 editor.selections.display_ranges(cx),
5563 vec![
5564 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5565 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5566 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5567 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5568 ]
5569 );
5570 });
5571
5572 _ = editor.update(cx, |editor, window, cx| {
5573 editor.move_line_down(&MoveLineDown, window, cx);
5574 assert_eq!(
5575 editor.display_text(cx),
5576 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
5577 );
5578 assert_eq!(
5579 editor.selections.display_ranges(cx),
5580 vec![
5581 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5582 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5583 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5584 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5585 ]
5586 );
5587 });
5588
5589 _ = editor.update(cx, |editor, window, cx| {
5590 editor.move_line_up(&MoveLineUp, window, cx);
5591 assert_eq!(
5592 editor.display_text(cx),
5593 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
5594 );
5595 assert_eq!(
5596 editor.selections.display_ranges(cx),
5597 vec![
5598 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5599 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5600 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5601 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5602 ]
5603 );
5604 });
5605}
5606
5607#[gpui::test]
5608fn test_move_line_up_selection_at_end_of_fold(cx: &mut TestAppContext) {
5609 init_test(cx, |_| {});
5610 let editor = cx.add_window(|window, cx| {
5611 let buffer = MultiBuffer::build_simple("\n\n\n\n\n\naaaa\nbbbb\ncccc", cx);
5612 build_editor(buffer, window, cx)
5613 });
5614 _ = editor.update(cx, |editor, window, cx| {
5615 editor.fold_creases(
5616 vec![Crease::simple(
5617 Point::new(6, 4)..Point::new(7, 4),
5618 FoldPlaceholder::test(),
5619 )],
5620 true,
5621 window,
5622 cx,
5623 );
5624 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5625 s.select_ranges([Point::new(7, 4)..Point::new(7, 4)])
5626 });
5627 assert_eq!(editor.display_text(cx), "\n\n\n\n\n\naaaa⋯\ncccc");
5628 editor.move_line_up(&MoveLineUp, window, cx);
5629 let buffer_text = editor.buffer.read(cx).snapshot(cx).text();
5630 assert_eq!(buffer_text, "\n\n\n\n\naaaa\nbbbb\n\ncccc");
5631 });
5632}
5633
5634#[gpui::test]
5635fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
5636 init_test(cx, |_| {});
5637
5638 let editor = cx.add_window(|window, cx| {
5639 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5640 build_editor(buffer, window, cx)
5641 });
5642 _ = editor.update(cx, |editor, window, cx| {
5643 let snapshot = editor.buffer.read(cx).snapshot(cx);
5644 editor.insert_blocks(
5645 [BlockProperties {
5646 style: BlockStyle::Fixed,
5647 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
5648 height: Some(1),
5649 render: Arc::new(|_| div().into_any()),
5650 priority: 0,
5651 }],
5652 Some(Autoscroll::fit()),
5653 cx,
5654 );
5655 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5656 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
5657 });
5658 editor.move_line_down(&MoveLineDown, window, cx);
5659 });
5660}
5661
5662#[gpui::test]
5663async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
5664 init_test(cx, |_| {});
5665
5666 let mut cx = EditorTestContext::new(cx).await;
5667 cx.set_state(
5668 &"
5669 ˇzero
5670 one
5671 two
5672 three
5673 four
5674 five
5675 "
5676 .unindent(),
5677 );
5678
5679 // Create a four-line block that replaces three lines of text.
5680 cx.update_editor(|editor, window, cx| {
5681 let snapshot = editor.snapshot(window, cx);
5682 let snapshot = &snapshot.buffer_snapshot;
5683 let placement = BlockPlacement::Replace(
5684 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
5685 );
5686 editor.insert_blocks(
5687 [BlockProperties {
5688 placement,
5689 height: Some(4),
5690 style: BlockStyle::Sticky,
5691 render: Arc::new(|_| gpui::div().into_any_element()),
5692 priority: 0,
5693 }],
5694 None,
5695 cx,
5696 );
5697 });
5698
5699 // Move down so that the cursor touches the block.
5700 cx.update_editor(|editor, window, cx| {
5701 editor.move_down(&Default::default(), window, cx);
5702 });
5703 cx.assert_editor_state(
5704 &"
5705 zero
5706 «one
5707 two
5708 threeˇ»
5709 four
5710 five
5711 "
5712 .unindent(),
5713 );
5714
5715 // Move down past the block.
5716 cx.update_editor(|editor, window, cx| {
5717 editor.move_down(&Default::default(), window, cx);
5718 });
5719 cx.assert_editor_state(
5720 &"
5721 zero
5722 one
5723 two
5724 three
5725 ˇfour
5726 five
5727 "
5728 .unindent(),
5729 );
5730}
5731
5732#[gpui::test]
5733fn test_transpose(cx: &mut TestAppContext) {
5734 init_test(cx, |_| {});
5735
5736 _ = cx.add_window(|window, cx| {
5737 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
5738 editor.set_style(EditorStyle::default(), window, cx);
5739 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5740 s.select_ranges([1..1])
5741 });
5742 editor.transpose(&Default::default(), window, cx);
5743 assert_eq!(editor.text(cx), "bac");
5744 assert_eq!(editor.selections.ranges(cx), [2..2]);
5745
5746 editor.transpose(&Default::default(), window, cx);
5747 assert_eq!(editor.text(cx), "bca");
5748 assert_eq!(editor.selections.ranges(cx), [3..3]);
5749
5750 editor.transpose(&Default::default(), window, cx);
5751 assert_eq!(editor.text(cx), "bac");
5752 assert_eq!(editor.selections.ranges(cx), [3..3]);
5753
5754 editor
5755 });
5756
5757 _ = cx.add_window(|window, cx| {
5758 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5759 editor.set_style(EditorStyle::default(), window, cx);
5760 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5761 s.select_ranges([3..3])
5762 });
5763 editor.transpose(&Default::default(), window, cx);
5764 assert_eq!(editor.text(cx), "acb\nde");
5765 assert_eq!(editor.selections.ranges(cx), [3..3]);
5766
5767 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5768 s.select_ranges([4..4])
5769 });
5770 editor.transpose(&Default::default(), window, cx);
5771 assert_eq!(editor.text(cx), "acbd\ne");
5772 assert_eq!(editor.selections.ranges(cx), [5..5]);
5773
5774 editor.transpose(&Default::default(), window, cx);
5775 assert_eq!(editor.text(cx), "acbde\n");
5776 assert_eq!(editor.selections.ranges(cx), [6..6]);
5777
5778 editor.transpose(&Default::default(), window, cx);
5779 assert_eq!(editor.text(cx), "acbd\ne");
5780 assert_eq!(editor.selections.ranges(cx), [6..6]);
5781
5782 editor
5783 });
5784
5785 _ = cx.add_window(|window, cx| {
5786 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5787 editor.set_style(EditorStyle::default(), window, cx);
5788 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5789 s.select_ranges([1..1, 2..2, 4..4])
5790 });
5791 editor.transpose(&Default::default(), window, cx);
5792 assert_eq!(editor.text(cx), "bacd\ne");
5793 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
5794
5795 editor.transpose(&Default::default(), window, cx);
5796 assert_eq!(editor.text(cx), "bcade\n");
5797 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
5798
5799 editor.transpose(&Default::default(), window, cx);
5800 assert_eq!(editor.text(cx), "bcda\ne");
5801 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5802
5803 editor.transpose(&Default::default(), window, cx);
5804 assert_eq!(editor.text(cx), "bcade\n");
5805 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5806
5807 editor.transpose(&Default::default(), window, cx);
5808 assert_eq!(editor.text(cx), "bcaed\n");
5809 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
5810
5811 editor
5812 });
5813
5814 _ = cx.add_window(|window, cx| {
5815 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
5816 editor.set_style(EditorStyle::default(), window, cx);
5817 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5818 s.select_ranges([4..4])
5819 });
5820 editor.transpose(&Default::default(), window, cx);
5821 assert_eq!(editor.text(cx), "🏀🍐✋");
5822 assert_eq!(editor.selections.ranges(cx), [8..8]);
5823
5824 editor.transpose(&Default::default(), window, cx);
5825 assert_eq!(editor.text(cx), "🏀✋🍐");
5826 assert_eq!(editor.selections.ranges(cx), [11..11]);
5827
5828 editor.transpose(&Default::default(), window, cx);
5829 assert_eq!(editor.text(cx), "🏀🍐✋");
5830 assert_eq!(editor.selections.ranges(cx), [11..11]);
5831
5832 editor
5833 });
5834}
5835
5836#[gpui::test]
5837async fn test_rewrap(cx: &mut TestAppContext) {
5838 init_test(cx, |settings| {
5839 settings.languages.0.extend([
5840 (
5841 "Markdown".into(),
5842 LanguageSettingsContent {
5843 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5844 preferred_line_length: Some(40),
5845 ..Default::default()
5846 },
5847 ),
5848 (
5849 "Plain Text".into(),
5850 LanguageSettingsContent {
5851 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5852 preferred_line_length: Some(40),
5853 ..Default::default()
5854 },
5855 ),
5856 (
5857 "C++".into(),
5858 LanguageSettingsContent {
5859 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5860 preferred_line_length: Some(40),
5861 ..Default::default()
5862 },
5863 ),
5864 (
5865 "Python".into(),
5866 LanguageSettingsContent {
5867 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5868 preferred_line_length: Some(40),
5869 ..Default::default()
5870 },
5871 ),
5872 (
5873 "Rust".into(),
5874 LanguageSettingsContent {
5875 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5876 preferred_line_length: Some(40),
5877 ..Default::default()
5878 },
5879 ),
5880 ])
5881 });
5882
5883 let mut cx = EditorTestContext::new(cx).await;
5884
5885 let cpp_language = Arc::new(Language::new(
5886 LanguageConfig {
5887 name: "C++".into(),
5888 line_comments: vec!["// ".into()],
5889 ..LanguageConfig::default()
5890 },
5891 None,
5892 ));
5893 let python_language = Arc::new(Language::new(
5894 LanguageConfig {
5895 name: "Python".into(),
5896 line_comments: vec!["# ".into()],
5897 ..LanguageConfig::default()
5898 },
5899 None,
5900 ));
5901 let markdown_language = Arc::new(Language::new(
5902 LanguageConfig {
5903 name: "Markdown".into(),
5904 rewrap_prefixes: vec![
5905 regex::Regex::new("\\d+\\.\\s+").unwrap(),
5906 regex::Regex::new("[-*+]\\s+").unwrap(),
5907 ],
5908 ..LanguageConfig::default()
5909 },
5910 None,
5911 ));
5912 let rust_language = Arc::new(
5913 Language::new(
5914 LanguageConfig {
5915 name: "Rust".into(),
5916 line_comments: vec!["// ".into(), "/// ".into()],
5917 ..LanguageConfig::default()
5918 },
5919 Some(tree_sitter_rust::LANGUAGE.into()),
5920 )
5921 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
5922 .unwrap(),
5923 );
5924
5925 let plaintext_language = Arc::new(Language::new(
5926 LanguageConfig {
5927 name: "Plain Text".into(),
5928 ..LanguageConfig::default()
5929 },
5930 None,
5931 ));
5932
5933 // Test basic rewrapping of a long line with a cursor
5934 assert_rewrap(
5935 indoc! {"
5936 // ˇThis is a long comment that needs to be wrapped.
5937 "},
5938 indoc! {"
5939 // ˇThis is a long comment that needs to
5940 // be wrapped.
5941 "},
5942 cpp_language.clone(),
5943 &mut cx,
5944 );
5945
5946 // Test rewrapping a full selection
5947 assert_rewrap(
5948 indoc! {"
5949 «// This selected long comment needs to be wrapped.ˇ»"
5950 },
5951 indoc! {"
5952 «// This selected long comment needs to
5953 // be wrapped.ˇ»"
5954 },
5955 cpp_language.clone(),
5956 &mut cx,
5957 );
5958
5959 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
5960 assert_rewrap(
5961 indoc! {"
5962 // ˇThis is the first line.
5963 // Thisˇ is the second line.
5964 // This is the thirdˇ line, all part of one paragraph.
5965 "},
5966 indoc! {"
5967 // ˇThis is the first line. Thisˇ is the
5968 // second line. This is the thirdˇ line,
5969 // all part of one paragraph.
5970 "},
5971 cpp_language.clone(),
5972 &mut cx,
5973 );
5974
5975 // Test multiple cursors in different paragraphs trigger separate rewraps
5976 assert_rewrap(
5977 indoc! {"
5978 // ˇThis is the first paragraph, first line.
5979 // ˇThis is the first paragraph, second line.
5980
5981 // ˇThis is the second paragraph, first line.
5982 // ˇThis is the second paragraph, second line.
5983 "},
5984 indoc! {"
5985 // ˇThis is the first paragraph, first
5986 // line. ˇThis is the first paragraph,
5987 // second line.
5988
5989 // ˇThis is the second paragraph, first
5990 // line. ˇThis is the second paragraph,
5991 // second line.
5992 "},
5993 cpp_language.clone(),
5994 &mut cx,
5995 );
5996
5997 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
5998 assert_rewrap(
5999 indoc! {"
6000 «// A regular long long comment to be wrapped.
6001 /// A documentation long comment to be wrapped.ˇ»
6002 "},
6003 indoc! {"
6004 «// A regular long long comment to be
6005 // wrapped.
6006 /// A documentation long comment to be
6007 /// wrapped.ˇ»
6008 "},
6009 rust_language.clone(),
6010 &mut cx,
6011 );
6012
6013 // Test that change in indentation level trigger seperate rewraps
6014 assert_rewrap(
6015 indoc! {"
6016 fn foo() {
6017 «// This is a long comment at the base indent.
6018 // This is a long comment at the next indent.ˇ»
6019 }
6020 "},
6021 indoc! {"
6022 fn foo() {
6023 «// This is a long comment at the
6024 // base indent.
6025 // This is a long comment at the
6026 // next indent.ˇ»
6027 }
6028 "},
6029 rust_language.clone(),
6030 &mut cx,
6031 );
6032
6033 // Test that different comment prefix characters (e.g., '#') are handled correctly
6034 assert_rewrap(
6035 indoc! {"
6036 # ˇThis is a long comment using a pound sign.
6037 "},
6038 indoc! {"
6039 # ˇThis is a long comment using a pound
6040 # sign.
6041 "},
6042 python_language,
6043 &mut cx,
6044 );
6045
6046 // Test rewrapping only affects comments, not code even when selected
6047 assert_rewrap(
6048 indoc! {"
6049 «/// This doc comment is long and should be wrapped.
6050 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6051 "},
6052 indoc! {"
6053 «/// This doc comment is long and should
6054 /// be wrapped.
6055 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6056 "},
6057 rust_language.clone(),
6058 &mut cx,
6059 );
6060
6061 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
6062 assert_rewrap(
6063 indoc! {"
6064 # Header
6065
6066 A long long long line of markdown text to wrap.ˇ
6067 "},
6068 indoc! {"
6069 # Header
6070
6071 A long long long line of markdown text
6072 to wrap.ˇ
6073 "},
6074 markdown_language.clone(),
6075 &mut cx,
6076 );
6077
6078 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
6079 assert_rewrap(
6080 indoc! {"
6081 «1. This is a numbered list item that is very long and needs to be wrapped properly.
6082 2. This is a numbered list item that is very long and needs to be wrapped properly.
6083 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
6084 "},
6085 indoc! {"
6086 «1. This is a numbered list item that is
6087 very long and needs to be wrapped
6088 properly.
6089 2. This is a numbered list item that is
6090 very long and needs to be wrapped
6091 properly.
6092 - This is an unordered list item that is
6093 also very long and should not merge
6094 with the numbered item.ˇ»
6095 "},
6096 markdown_language.clone(),
6097 &mut cx,
6098 );
6099
6100 // Test that rewrapping add indents for rewrapping boundary if not exists already.
6101 assert_rewrap(
6102 indoc! {"
6103 «1. This is a numbered list item that is
6104 very long and needs to be wrapped
6105 properly.
6106 2. This is a numbered list item that is
6107 very long and needs to be wrapped
6108 properly.
6109 - This is an unordered list item that is
6110 also very long and should not merge with
6111 the numbered item.ˇ»
6112 "},
6113 indoc! {"
6114 «1. This is a numbered list item that is
6115 very long and needs to be wrapped
6116 properly.
6117 2. This is a numbered list item that is
6118 very long and needs to be wrapped
6119 properly.
6120 - This is an unordered list item that is
6121 also very long and should not merge
6122 with the numbered item.ˇ»
6123 "},
6124 markdown_language.clone(),
6125 &mut cx,
6126 );
6127
6128 // Test that rewrapping maintain indents even when they already exists.
6129 assert_rewrap(
6130 indoc! {"
6131 «1. This is a numbered list
6132 item that is very long and needs to be wrapped properly.
6133 2. This is a numbered list
6134 item that is very long and needs to be wrapped properly.
6135 - This is an unordered list item that is also very long and
6136 should not merge with the numbered item.ˇ»
6137 "},
6138 indoc! {"
6139 «1. This is a numbered list item that is
6140 very long and needs to be wrapped
6141 properly.
6142 2. This is a numbered list item that is
6143 very long and needs to be wrapped
6144 properly.
6145 - This is an unordered list item that is
6146 also very long and should not merge
6147 with the numbered item.ˇ»
6148 "},
6149 markdown_language,
6150 &mut cx,
6151 );
6152
6153 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
6154 assert_rewrap(
6155 indoc! {"
6156 ˇThis is a very long line of plain text that will be wrapped.
6157 "},
6158 indoc! {"
6159 ˇThis is a very long line of plain text
6160 that will be wrapped.
6161 "},
6162 plaintext_language.clone(),
6163 &mut cx,
6164 );
6165
6166 // Test that non-commented code acts as a paragraph boundary within a selection
6167 assert_rewrap(
6168 indoc! {"
6169 «// This is the first long comment block to be wrapped.
6170 fn my_func(a: u32);
6171 // This is the second long comment block to be wrapped.ˇ»
6172 "},
6173 indoc! {"
6174 «// This is the first long comment block
6175 // to be wrapped.
6176 fn my_func(a: u32);
6177 // This is the second long comment block
6178 // to be wrapped.ˇ»
6179 "},
6180 rust_language,
6181 &mut cx,
6182 );
6183
6184 // Test rewrapping multiple selections, including ones with blank lines or tabs
6185 assert_rewrap(
6186 indoc! {"
6187 «ˇThis is a very long line that will be wrapped.
6188
6189 This is another paragraph in the same selection.»
6190
6191 «\tThis is a very long indented line that will be wrapped.ˇ»
6192 "},
6193 indoc! {"
6194 «ˇThis is a very long line that will be
6195 wrapped.
6196
6197 This is another paragraph in the same
6198 selection.»
6199
6200 «\tThis is a very long indented line
6201 \tthat will be wrapped.ˇ»
6202 "},
6203 plaintext_language,
6204 &mut cx,
6205 );
6206
6207 // Test that an empty comment line acts as a paragraph boundary
6208 assert_rewrap(
6209 indoc! {"
6210 // ˇThis is a long comment that will be wrapped.
6211 //
6212 // And this is another long comment that will also be wrapped.ˇ
6213 "},
6214 indoc! {"
6215 // ˇThis is a long comment that will be
6216 // wrapped.
6217 //
6218 // And this is another long comment that
6219 // will also be wrapped.ˇ
6220 "},
6221 cpp_language,
6222 &mut cx,
6223 );
6224
6225 #[track_caller]
6226 fn assert_rewrap(
6227 unwrapped_text: &str,
6228 wrapped_text: &str,
6229 language: Arc<Language>,
6230 cx: &mut EditorTestContext,
6231 ) {
6232 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6233 cx.set_state(unwrapped_text);
6234 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6235 cx.assert_editor_state(wrapped_text);
6236 }
6237}
6238
6239#[gpui::test]
6240async fn test_rewrap_block_comments(cx: &mut TestAppContext) {
6241 init_test(cx, |settings| {
6242 settings.languages.0.extend([(
6243 "Rust".into(),
6244 LanguageSettingsContent {
6245 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6246 preferred_line_length: Some(40),
6247 ..Default::default()
6248 },
6249 )])
6250 });
6251
6252 let mut cx = EditorTestContext::new(cx).await;
6253
6254 let rust_lang = Arc::new(
6255 Language::new(
6256 LanguageConfig {
6257 name: "Rust".into(),
6258 line_comments: vec!["// ".into()],
6259 block_comment: Some(BlockCommentConfig {
6260 start: "/*".into(),
6261 end: "*/".into(),
6262 prefix: "* ".into(),
6263 tab_size: 1,
6264 }),
6265 documentation_comment: Some(BlockCommentConfig {
6266 start: "/**".into(),
6267 end: "*/".into(),
6268 prefix: "* ".into(),
6269 tab_size: 1,
6270 }),
6271
6272 ..LanguageConfig::default()
6273 },
6274 Some(tree_sitter_rust::LANGUAGE.into()),
6275 )
6276 .with_override_query("[(line_comment) (block_comment)] @comment.inclusive")
6277 .unwrap(),
6278 );
6279
6280 // regular block comment
6281 assert_rewrap(
6282 indoc! {"
6283 /*
6284 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6285 */
6286 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6287 "},
6288 indoc! {"
6289 /*
6290 *ˇ Lorem ipsum dolor sit amet,
6291 * consectetur adipiscing elit.
6292 */
6293 /*
6294 *ˇ Lorem ipsum dolor sit amet,
6295 * consectetur adipiscing elit.
6296 */
6297 "},
6298 rust_lang.clone(),
6299 &mut cx,
6300 );
6301
6302 // indent is respected
6303 assert_rewrap(
6304 indoc! {"
6305 {}
6306 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6307 "},
6308 indoc! {"
6309 {}
6310 /*
6311 *ˇ Lorem ipsum dolor sit amet,
6312 * consectetur adipiscing elit.
6313 */
6314 "},
6315 rust_lang.clone(),
6316 &mut cx,
6317 );
6318
6319 // short block comments with inline delimiters
6320 assert_rewrap(
6321 indoc! {"
6322 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6323 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6324 */
6325 /*
6326 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6327 "},
6328 indoc! {"
6329 /*
6330 *ˇ Lorem ipsum dolor sit amet,
6331 * consectetur adipiscing elit.
6332 */
6333 /*
6334 *ˇ Lorem ipsum dolor sit amet,
6335 * consectetur adipiscing elit.
6336 */
6337 /*
6338 *ˇ Lorem ipsum dolor sit amet,
6339 * consectetur adipiscing elit.
6340 */
6341 "},
6342 rust_lang.clone(),
6343 &mut cx,
6344 );
6345
6346 // multiline block comment with inline start/end delimiters
6347 assert_rewrap(
6348 indoc! {"
6349 /*ˇ Lorem ipsum dolor sit amet,
6350 * consectetur adipiscing elit. */
6351 "},
6352 indoc! {"
6353 /*
6354 *ˇ Lorem ipsum dolor sit amet,
6355 * consectetur adipiscing elit.
6356 */
6357 "},
6358 rust_lang.clone(),
6359 &mut cx,
6360 );
6361
6362 // block comment rewrap still respects paragraph bounds
6363 assert_rewrap(
6364 indoc! {"
6365 /*
6366 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6367 *
6368 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6369 */
6370 "},
6371 indoc! {"
6372 /*
6373 *ˇ Lorem ipsum dolor sit amet,
6374 * consectetur adipiscing elit.
6375 *
6376 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6377 */
6378 "},
6379 rust_lang.clone(),
6380 &mut cx,
6381 );
6382
6383 // documentation comments
6384 assert_rewrap(
6385 indoc! {"
6386 /**ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6387 /**
6388 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6389 */
6390 "},
6391 indoc! {"
6392 /**
6393 *ˇ Lorem ipsum dolor sit amet,
6394 * consectetur adipiscing elit.
6395 */
6396 /**
6397 *ˇ Lorem ipsum dolor sit amet,
6398 * consectetur adipiscing elit.
6399 */
6400 "},
6401 rust_lang.clone(),
6402 &mut cx,
6403 );
6404
6405 // different, adjacent comments
6406 assert_rewrap(
6407 indoc! {"
6408 /**
6409 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6410 */
6411 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6412 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6413 "},
6414 indoc! {"
6415 /**
6416 *ˇ Lorem ipsum dolor sit amet,
6417 * consectetur adipiscing elit.
6418 */
6419 /*
6420 *ˇ Lorem ipsum dolor sit amet,
6421 * consectetur adipiscing elit.
6422 */
6423 //ˇ Lorem ipsum dolor sit amet,
6424 // consectetur adipiscing elit.
6425 "},
6426 rust_lang.clone(),
6427 &mut cx,
6428 );
6429
6430 // selection w/ single short block comment
6431 assert_rewrap(
6432 indoc! {"
6433 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6434 "},
6435 indoc! {"
6436 «/*
6437 * Lorem ipsum dolor sit amet,
6438 * consectetur adipiscing elit.
6439 */ˇ»
6440 "},
6441 rust_lang.clone(),
6442 &mut cx,
6443 );
6444
6445 // rewrapping a single comment w/ abutting comments
6446 assert_rewrap(
6447 indoc! {"
6448 /* ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. */
6449 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6450 "},
6451 indoc! {"
6452 /*
6453 * ˇLorem ipsum dolor sit amet,
6454 * consectetur adipiscing elit.
6455 */
6456 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6457 "},
6458 rust_lang.clone(),
6459 &mut cx,
6460 );
6461
6462 // selection w/ non-abutting short block comments
6463 assert_rewrap(
6464 indoc! {"
6465 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6466
6467 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6468 "},
6469 indoc! {"
6470 «/*
6471 * Lorem ipsum dolor sit amet,
6472 * consectetur adipiscing elit.
6473 */
6474
6475 /*
6476 * Lorem ipsum dolor sit amet,
6477 * consectetur adipiscing elit.
6478 */ˇ»
6479 "},
6480 rust_lang.clone(),
6481 &mut cx,
6482 );
6483
6484 // selection of multiline block comments
6485 assert_rewrap(
6486 indoc! {"
6487 «/* Lorem ipsum dolor sit amet,
6488 * consectetur adipiscing elit. */ˇ»
6489 "},
6490 indoc! {"
6491 «/*
6492 * Lorem ipsum dolor sit amet,
6493 * consectetur adipiscing elit.
6494 */ˇ»
6495 "},
6496 rust_lang.clone(),
6497 &mut cx,
6498 );
6499
6500 // partial selection of multiline block comments
6501 assert_rewrap(
6502 indoc! {"
6503 «/* Lorem ipsum dolor sit amet,ˇ»
6504 * consectetur adipiscing elit. */
6505 /* Lorem ipsum dolor sit amet,
6506 «* consectetur adipiscing elit. */ˇ»
6507 "},
6508 indoc! {"
6509 «/*
6510 * Lorem ipsum dolor sit amet,ˇ»
6511 * consectetur adipiscing elit. */
6512 /* Lorem ipsum dolor sit amet,
6513 «* consectetur adipiscing elit.
6514 */ˇ»
6515 "},
6516 rust_lang.clone(),
6517 &mut cx,
6518 );
6519
6520 // selection w/ abutting short block comments
6521 // TODO: should not be combined; should rewrap as 2 comments
6522 assert_rewrap(
6523 indoc! {"
6524 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6525 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6526 "},
6527 // desired behavior:
6528 // indoc! {"
6529 // «/*
6530 // * Lorem ipsum dolor sit amet,
6531 // * consectetur adipiscing elit.
6532 // */
6533 // /*
6534 // * Lorem ipsum dolor sit amet,
6535 // * consectetur adipiscing elit.
6536 // */ˇ»
6537 // "},
6538 // actual behaviour:
6539 indoc! {"
6540 «/*
6541 * Lorem ipsum dolor sit amet,
6542 * consectetur adipiscing elit. Lorem
6543 * ipsum dolor sit amet, consectetur
6544 * adipiscing elit.
6545 */ˇ»
6546 "},
6547 rust_lang.clone(),
6548 &mut cx,
6549 );
6550
6551 // TODO: same as above, but with delimiters on separate line
6552 // assert_rewrap(
6553 // indoc! {"
6554 // «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6555 // */
6556 // /*
6557 // * Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6558 // "},
6559 // // desired:
6560 // // indoc! {"
6561 // // «/*
6562 // // * Lorem ipsum dolor sit amet,
6563 // // * consectetur adipiscing elit.
6564 // // */
6565 // // /*
6566 // // * Lorem ipsum dolor sit amet,
6567 // // * consectetur adipiscing elit.
6568 // // */ˇ»
6569 // // "},
6570 // // actual: (but with trailing w/s on the empty lines)
6571 // indoc! {"
6572 // «/*
6573 // * Lorem ipsum dolor sit amet,
6574 // * consectetur adipiscing elit.
6575 // *
6576 // */
6577 // /*
6578 // *
6579 // * Lorem ipsum dolor sit amet,
6580 // * consectetur adipiscing elit.
6581 // */ˇ»
6582 // "},
6583 // rust_lang.clone(),
6584 // &mut cx,
6585 // );
6586
6587 // TODO these are unhandled edge cases; not correct, just documenting known issues
6588 assert_rewrap(
6589 indoc! {"
6590 /*
6591 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6592 */
6593 /*
6594 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6595 /*ˇ Lorem ipsum dolor sit amet */ /* consectetur adipiscing elit. */
6596 "},
6597 // desired:
6598 // indoc! {"
6599 // /*
6600 // *ˇ Lorem ipsum dolor sit amet,
6601 // * consectetur adipiscing elit.
6602 // */
6603 // /*
6604 // *ˇ Lorem ipsum dolor sit amet,
6605 // * consectetur adipiscing elit.
6606 // */
6607 // /*
6608 // *ˇ Lorem ipsum dolor sit amet
6609 // */ /* consectetur adipiscing elit. */
6610 // "},
6611 // actual:
6612 indoc! {"
6613 /*
6614 //ˇ Lorem ipsum dolor sit amet,
6615 // consectetur adipiscing elit.
6616 */
6617 /*
6618 * //ˇ Lorem ipsum dolor sit amet,
6619 * consectetur adipiscing elit.
6620 */
6621 /*
6622 *ˇ Lorem ipsum dolor sit amet */ /*
6623 * consectetur adipiscing elit.
6624 */
6625 "},
6626 rust_lang,
6627 &mut cx,
6628 );
6629
6630 #[track_caller]
6631 fn assert_rewrap(
6632 unwrapped_text: &str,
6633 wrapped_text: &str,
6634 language: Arc<Language>,
6635 cx: &mut EditorTestContext,
6636 ) {
6637 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6638 cx.set_state(unwrapped_text);
6639 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6640 cx.assert_editor_state(wrapped_text);
6641 }
6642}
6643
6644#[gpui::test]
6645async fn test_hard_wrap(cx: &mut TestAppContext) {
6646 init_test(cx, |_| {});
6647 let mut cx = EditorTestContext::new(cx).await;
6648
6649 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
6650 cx.update_editor(|editor, _, cx| {
6651 editor.set_hard_wrap(Some(14), cx);
6652 });
6653
6654 cx.set_state(indoc!(
6655 "
6656 one two three ˇ
6657 "
6658 ));
6659 cx.simulate_input("four");
6660 cx.run_until_parked();
6661
6662 cx.assert_editor_state(indoc!(
6663 "
6664 one two three
6665 fourˇ
6666 "
6667 ));
6668
6669 cx.update_editor(|editor, window, cx| {
6670 editor.newline(&Default::default(), window, cx);
6671 });
6672 cx.run_until_parked();
6673 cx.assert_editor_state(indoc!(
6674 "
6675 one two three
6676 four
6677 ˇ
6678 "
6679 ));
6680
6681 cx.simulate_input("five");
6682 cx.run_until_parked();
6683 cx.assert_editor_state(indoc!(
6684 "
6685 one two three
6686 four
6687 fiveˇ
6688 "
6689 ));
6690
6691 cx.update_editor(|editor, window, cx| {
6692 editor.newline(&Default::default(), window, cx);
6693 });
6694 cx.run_until_parked();
6695 cx.simulate_input("# ");
6696 cx.run_until_parked();
6697 cx.assert_editor_state(indoc!(
6698 "
6699 one two three
6700 four
6701 five
6702 # ˇ
6703 "
6704 ));
6705
6706 cx.update_editor(|editor, window, cx| {
6707 editor.newline(&Default::default(), window, cx);
6708 });
6709 cx.run_until_parked();
6710 cx.assert_editor_state(indoc!(
6711 "
6712 one two three
6713 four
6714 five
6715 #\x20
6716 #ˇ
6717 "
6718 ));
6719
6720 cx.simulate_input(" 6");
6721 cx.run_until_parked();
6722 cx.assert_editor_state(indoc!(
6723 "
6724 one two three
6725 four
6726 five
6727 #
6728 # 6ˇ
6729 "
6730 ));
6731}
6732
6733#[gpui::test]
6734async fn test_clipboard(cx: &mut TestAppContext) {
6735 init_test(cx, |_| {});
6736
6737 let mut cx = EditorTestContext::new(cx).await;
6738
6739 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
6740 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6741 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
6742
6743 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
6744 cx.set_state("two ˇfour ˇsix ˇ");
6745 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6746 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
6747
6748 // Paste again but with only two cursors. Since the number of cursors doesn't
6749 // match the number of slices in the clipboard, the entire clipboard text
6750 // is pasted at each cursor.
6751 cx.set_state("ˇtwo one✅ four three six five ˇ");
6752 cx.update_editor(|e, window, cx| {
6753 e.handle_input("( ", window, cx);
6754 e.paste(&Paste, window, cx);
6755 e.handle_input(") ", window, cx);
6756 });
6757 cx.assert_editor_state(
6758 &([
6759 "( one✅ ",
6760 "three ",
6761 "five ) ˇtwo one✅ four three six five ( one✅ ",
6762 "three ",
6763 "five ) ˇ",
6764 ]
6765 .join("\n")),
6766 );
6767
6768 // Cut with three selections, one of which is full-line.
6769 cx.set_state(indoc! {"
6770 1«2ˇ»3
6771 4ˇ567
6772 «8ˇ»9"});
6773 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6774 cx.assert_editor_state(indoc! {"
6775 1ˇ3
6776 ˇ9"});
6777
6778 // Paste with three selections, noticing how the copied selection that was full-line
6779 // gets inserted before the second cursor.
6780 cx.set_state(indoc! {"
6781 1ˇ3
6782 9ˇ
6783 «oˇ»ne"});
6784 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6785 cx.assert_editor_state(indoc! {"
6786 12ˇ3
6787 4567
6788 9ˇ
6789 8ˇne"});
6790
6791 // Copy with a single cursor only, which writes the whole line into the clipboard.
6792 cx.set_state(indoc! {"
6793 The quick brown
6794 fox juˇmps over
6795 the lazy dog"});
6796 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6797 assert_eq!(
6798 cx.read_from_clipboard()
6799 .and_then(|item| item.text().as_deref().map(str::to_string)),
6800 Some("fox jumps over\n".to_string())
6801 );
6802
6803 // Paste with three selections, noticing how the copied full-line selection is inserted
6804 // before the empty selections but replaces the selection that is non-empty.
6805 cx.set_state(indoc! {"
6806 Tˇhe quick brown
6807 «foˇ»x jumps over
6808 tˇhe lazy dog"});
6809 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6810 cx.assert_editor_state(indoc! {"
6811 fox jumps over
6812 Tˇhe quick brown
6813 fox jumps over
6814 ˇx jumps over
6815 fox jumps over
6816 tˇhe lazy dog"});
6817}
6818
6819#[gpui::test]
6820async fn test_copy_trim(cx: &mut TestAppContext) {
6821 init_test(cx, |_| {});
6822
6823 let mut cx = EditorTestContext::new(cx).await;
6824 cx.set_state(
6825 r#" «for selection in selections.iter() {
6826 let mut start = selection.start;
6827 let mut end = selection.end;
6828 let is_entire_line = selection.is_empty();
6829 if is_entire_line {
6830 start = Point::new(start.row, 0);ˇ»
6831 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6832 }
6833 "#,
6834 );
6835 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6836 assert_eq!(
6837 cx.read_from_clipboard()
6838 .and_then(|item| item.text().as_deref().map(str::to_string)),
6839 Some(
6840 "for selection in selections.iter() {
6841 let mut start = selection.start;
6842 let mut end = selection.end;
6843 let is_entire_line = selection.is_empty();
6844 if is_entire_line {
6845 start = Point::new(start.row, 0);"
6846 .to_string()
6847 ),
6848 "Regular copying preserves all indentation selected",
6849 );
6850 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6851 assert_eq!(
6852 cx.read_from_clipboard()
6853 .and_then(|item| item.text().as_deref().map(str::to_string)),
6854 Some(
6855 "for selection in selections.iter() {
6856let mut start = selection.start;
6857let mut end = selection.end;
6858let is_entire_line = selection.is_empty();
6859if is_entire_line {
6860 start = Point::new(start.row, 0);"
6861 .to_string()
6862 ),
6863 "Copying with stripping should strip all leading whitespaces"
6864 );
6865
6866 cx.set_state(
6867 r#" « for selection in selections.iter() {
6868 let mut start = selection.start;
6869 let mut end = selection.end;
6870 let is_entire_line = selection.is_empty();
6871 if is_entire_line {
6872 start = Point::new(start.row, 0);ˇ»
6873 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6874 }
6875 "#,
6876 );
6877 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6878 assert_eq!(
6879 cx.read_from_clipboard()
6880 .and_then(|item| item.text().as_deref().map(str::to_string)),
6881 Some(
6882 " for selection in selections.iter() {
6883 let mut start = selection.start;
6884 let mut end = selection.end;
6885 let is_entire_line = selection.is_empty();
6886 if is_entire_line {
6887 start = Point::new(start.row, 0);"
6888 .to_string()
6889 ),
6890 "Regular copying preserves all indentation selected",
6891 );
6892 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6893 assert_eq!(
6894 cx.read_from_clipboard()
6895 .and_then(|item| item.text().as_deref().map(str::to_string)),
6896 Some(
6897 "for selection in selections.iter() {
6898let mut start = selection.start;
6899let mut end = selection.end;
6900let is_entire_line = selection.is_empty();
6901if is_entire_line {
6902 start = Point::new(start.row, 0);"
6903 .to_string()
6904 ),
6905 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
6906 );
6907
6908 cx.set_state(
6909 r#" «ˇ for selection in selections.iter() {
6910 let mut start = selection.start;
6911 let mut end = selection.end;
6912 let is_entire_line = selection.is_empty();
6913 if is_entire_line {
6914 start = Point::new(start.row, 0);»
6915 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6916 }
6917 "#,
6918 );
6919 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6920 assert_eq!(
6921 cx.read_from_clipboard()
6922 .and_then(|item| item.text().as_deref().map(str::to_string)),
6923 Some(
6924 " for selection in selections.iter() {
6925 let mut start = selection.start;
6926 let mut end = selection.end;
6927 let is_entire_line = selection.is_empty();
6928 if is_entire_line {
6929 start = Point::new(start.row, 0);"
6930 .to_string()
6931 ),
6932 "Regular copying for reverse selection works the same",
6933 );
6934 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6935 assert_eq!(
6936 cx.read_from_clipboard()
6937 .and_then(|item| item.text().as_deref().map(str::to_string)),
6938 Some(
6939 "for selection in selections.iter() {
6940let mut start = selection.start;
6941let mut end = selection.end;
6942let is_entire_line = selection.is_empty();
6943if is_entire_line {
6944 start = Point::new(start.row, 0);"
6945 .to_string()
6946 ),
6947 "Copying with stripping for reverse selection works the same"
6948 );
6949
6950 cx.set_state(
6951 r#" for selection «in selections.iter() {
6952 let mut start = selection.start;
6953 let mut end = selection.end;
6954 let is_entire_line = selection.is_empty();
6955 if is_entire_line {
6956 start = Point::new(start.row, 0);ˇ»
6957 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6958 }
6959 "#,
6960 );
6961 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6962 assert_eq!(
6963 cx.read_from_clipboard()
6964 .and_then(|item| item.text().as_deref().map(str::to_string)),
6965 Some(
6966 "in selections.iter() {
6967 let mut start = selection.start;
6968 let mut end = selection.end;
6969 let is_entire_line = selection.is_empty();
6970 if is_entire_line {
6971 start = Point::new(start.row, 0);"
6972 .to_string()
6973 ),
6974 "When selecting past the indent, the copying works as usual",
6975 );
6976 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6977 assert_eq!(
6978 cx.read_from_clipboard()
6979 .and_then(|item| item.text().as_deref().map(str::to_string)),
6980 Some(
6981 "in selections.iter() {
6982 let mut start = selection.start;
6983 let mut end = selection.end;
6984 let is_entire_line = selection.is_empty();
6985 if is_entire_line {
6986 start = Point::new(start.row, 0);"
6987 .to_string()
6988 ),
6989 "When selecting past the indent, nothing is trimmed"
6990 );
6991
6992 cx.set_state(
6993 r#" «for selection in selections.iter() {
6994 let mut start = selection.start;
6995
6996 let mut end = selection.end;
6997 let is_entire_line = selection.is_empty();
6998 if is_entire_line {
6999 start = Point::new(start.row, 0);
7000ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
7001 }
7002 "#,
7003 );
7004 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7005 assert_eq!(
7006 cx.read_from_clipboard()
7007 .and_then(|item| item.text().as_deref().map(str::to_string)),
7008 Some(
7009 "for selection in selections.iter() {
7010let mut start = selection.start;
7011
7012let mut end = selection.end;
7013let is_entire_line = selection.is_empty();
7014if is_entire_line {
7015 start = Point::new(start.row, 0);
7016"
7017 .to_string()
7018 ),
7019 "Copying with stripping should ignore empty lines"
7020 );
7021}
7022
7023#[gpui::test]
7024async fn test_paste_multiline(cx: &mut TestAppContext) {
7025 init_test(cx, |_| {});
7026
7027 let mut cx = EditorTestContext::new(cx).await;
7028 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7029
7030 // Cut an indented block, without the leading whitespace.
7031 cx.set_state(indoc! {"
7032 const a: B = (
7033 c(),
7034 «d(
7035 e,
7036 f
7037 )ˇ»
7038 );
7039 "});
7040 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7041 cx.assert_editor_state(indoc! {"
7042 const a: B = (
7043 c(),
7044 ˇ
7045 );
7046 "});
7047
7048 // Paste it at the same position.
7049 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7050 cx.assert_editor_state(indoc! {"
7051 const a: B = (
7052 c(),
7053 d(
7054 e,
7055 f
7056 )ˇ
7057 );
7058 "});
7059
7060 // Paste it at a line with a lower indent level.
7061 cx.set_state(indoc! {"
7062 ˇ
7063 const a: B = (
7064 c(),
7065 );
7066 "});
7067 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7068 cx.assert_editor_state(indoc! {"
7069 d(
7070 e,
7071 f
7072 )ˇ
7073 const a: B = (
7074 c(),
7075 );
7076 "});
7077
7078 // Cut an indented block, with the leading whitespace.
7079 cx.set_state(indoc! {"
7080 const a: B = (
7081 c(),
7082 « d(
7083 e,
7084 f
7085 )
7086 ˇ»);
7087 "});
7088 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7089 cx.assert_editor_state(indoc! {"
7090 const a: B = (
7091 c(),
7092 ˇ);
7093 "});
7094
7095 // Paste it at the same position.
7096 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7097 cx.assert_editor_state(indoc! {"
7098 const a: B = (
7099 c(),
7100 d(
7101 e,
7102 f
7103 )
7104 ˇ);
7105 "});
7106
7107 // Paste it at a line with a higher indent level.
7108 cx.set_state(indoc! {"
7109 const a: B = (
7110 c(),
7111 d(
7112 e,
7113 fˇ
7114 )
7115 );
7116 "});
7117 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7118 cx.assert_editor_state(indoc! {"
7119 const a: B = (
7120 c(),
7121 d(
7122 e,
7123 f d(
7124 e,
7125 f
7126 )
7127 ˇ
7128 )
7129 );
7130 "});
7131
7132 // Copy an indented block, starting mid-line
7133 cx.set_state(indoc! {"
7134 const a: B = (
7135 c(),
7136 somethin«g(
7137 e,
7138 f
7139 )ˇ»
7140 );
7141 "});
7142 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7143
7144 // Paste it on a line with a lower indent level
7145 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
7146 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7147 cx.assert_editor_state(indoc! {"
7148 const a: B = (
7149 c(),
7150 something(
7151 e,
7152 f
7153 )
7154 );
7155 g(
7156 e,
7157 f
7158 )ˇ"});
7159}
7160
7161#[gpui::test]
7162async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
7163 init_test(cx, |_| {});
7164
7165 cx.write_to_clipboard(ClipboardItem::new_string(
7166 " d(\n e\n );\n".into(),
7167 ));
7168
7169 let mut cx = EditorTestContext::new(cx).await;
7170 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7171
7172 cx.set_state(indoc! {"
7173 fn a() {
7174 b();
7175 if c() {
7176 ˇ
7177 }
7178 }
7179 "});
7180
7181 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7182 cx.assert_editor_state(indoc! {"
7183 fn a() {
7184 b();
7185 if c() {
7186 d(
7187 e
7188 );
7189 ˇ
7190 }
7191 }
7192 "});
7193
7194 cx.set_state(indoc! {"
7195 fn a() {
7196 b();
7197 ˇ
7198 }
7199 "});
7200
7201 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7202 cx.assert_editor_state(indoc! {"
7203 fn a() {
7204 b();
7205 d(
7206 e
7207 );
7208 ˇ
7209 }
7210 "});
7211}
7212
7213#[gpui::test]
7214fn test_select_all(cx: &mut TestAppContext) {
7215 init_test(cx, |_| {});
7216
7217 let editor = cx.add_window(|window, cx| {
7218 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
7219 build_editor(buffer, window, cx)
7220 });
7221 _ = editor.update(cx, |editor, window, cx| {
7222 editor.select_all(&SelectAll, window, cx);
7223 assert_eq!(
7224 editor.selections.display_ranges(cx),
7225 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
7226 );
7227 });
7228}
7229
7230#[gpui::test]
7231fn test_select_line(cx: &mut TestAppContext) {
7232 init_test(cx, |_| {});
7233
7234 let editor = cx.add_window(|window, cx| {
7235 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
7236 build_editor(buffer, window, cx)
7237 });
7238 _ = editor.update(cx, |editor, window, cx| {
7239 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7240 s.select_display_ranges([
7241 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7242 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7243 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7244 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
7245 ])
7246 });
7247 editor.select_line(&SelectLine, window, cx);
7248 assert_eq!(
7249 editor.selections.display_ranges(cx),
7250 vec![
7251 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
7252 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
7253 ]
7254 );
7255 });
7256
7257 _ = editor.update(cx, |editor, window, cx| {
7258 editor.select_line(&SelectLine, window, cx);
7259 assert_eq!(
7260 editor.selections.display_ranges(cx),
7261 vec![
7262 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
7263 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
7264 ]
7265 );
7266 });
7267
7268 _ = editor.update(cx, |editor, window, cx| {
7269 editor.select_line(&SelectLine, window, cx);
7270 assert_eq!(
7271 editor.selections.display_ranges(cx),
7272 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
7273 );
7274 });
7275}
7276
7277#[gpui::test]
7278async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
7279 init_test(cx, |_| {});
7280 let mut cx = EditorTestContext::new(cx).await;
7281
7282 #[track_caller]
7283 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
7284 cx.set_state(initial_state);
7285 cx.update_editor(|e, window, cx| {
7286 e.split_selection_into_lines(&Default::default(), window, cx)
7287 });
7288 cx.assert_editor_state(expected_state);
7289 }
7290
7291 // Selection starts and ends at the middle of lines, left-to-right
7292 test(
7293 &mut cx,
7294 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
7295 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7296 );
7297 // Same thing, right-to-left
7298 test(
7299 &mut cx,
7300 "aa\nb«b\ncc\ndd\neˇ»e\nff",
7301 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7302 );
7303
7304 // Whole buffer, left-to-right, last line *doesn't* end with newline
7305 test(
7306 &mut cx,
7307 "«ˇaa\nbb\ncc\ndd\nee\nff»",
7308 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7309 );
7310 // Same thing, right-to-left
7311 test(
7312 &mut cx,
7313 "«aa\nbb\ncc\ndd\nee\nffˇ»",
7314 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7315 );
7316
7317 // Whole buffer, left-to-right, last line ends with newline
7318 test(
7319 &mut cx,
7320 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
7321 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7322 );
7323 // Same thing, right-to-left
7324 test(
7325 &mut cx,
7326 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
7327 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7328 );
7329
7330 // Starts at the end of a line, ends at the start of another
7331 test(
7332 &mut cx,
7333 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
7334 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
7335 );
7336}
7337
7338#[gpui::test]
7339async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
7340 init_test(cx, |_| {});
7341
7342 let editor = cx.add_window(|window, cx| {
7343 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
7344 build_editor(buffer, window, cx)
7345 });
7346
7347 // setup
7348 _ = editor.update(cx, |editor, window, cx| {
7349 editor.fold_creases(
7350 vec![
7351 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
7352 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
7353 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
7354 ],
7355 true,
7356 window,
7357 cx,
7358 );
7359 assert_eq!(
7360 editor.display_text(cx),
7361 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7362 );
7363 });
7364
7365 _ = editor.update(cx, |editor, window, cx| {
7366 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7367 s.select_display_ranges([
7368 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7369 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7370 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7371 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
7372 ])
7373 });
7374 editor.split_selection_into_lines(&Default::default(), window, cx);
7375 assert_eq!(
7376 editor.display_text(cx),
7377 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7378 );
7379 });
7380 EditorTestContext::for_editor(editor, cx)
7381 .await
7382 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
7383
7384 _ = editor.update(cx, |editor, window, cx| {
7385 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7386 s.select_display_ranges([
7387 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
7388 ])
7389 });
7390 editor.split_selection_into_lines(&Default::default(), window, cx);
7391 assert_eq!(
7392 editor.display_text(cx),
7393 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
7394 );
7395 assert_eq!(
7396 editor.selections.display_ranges(cx),
7397 [
7398 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
7399 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
7400 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
7401 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
7402 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
7403 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
7404 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
7405 ]
7406 );
7407 });
7408 EditorTestContext::for_editor(editor, cx)
7409 .await
7410 .assert_editor_state(
7411 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
7412 );
7413}
7414
7415#[gpui::test]
7416async fn test_add_selection_above_below(cx: &mut TestAppContext) {
7417 init_test(cx, |_| {});
7418
7419 let mut cx = EditorTestContext::new(cx).await;
7420
7421 cx.set_state(indoc!(
7422 r#"abc
7423 defˇghi
7424
7425 jk
7426 nlmo
7427 "#
7428 ));
7429
7430 cx.update_editor(|editor, window, cx| {
7431 editor.add_selection_above(&Default::default(), window, cx);
7432 });
7433
7434 cx.assert_editor_state(indoc!(
7435 r#"abcˇ
7436 defˇghi
7437
7438 jk
7439 nlmo
7440 "#
7441 ));
7442
7443 cx.update_editor(|editor, window, cx| {
7444 editor.add_selection_above(&Default::default(), window, cx);
7445 });
7446
7447 cx.assert_editor_state(indoc!(
7448 r#"abcˇ
7449 defˇghi
7450
7451 jk
7452 nlmo
7453 "#
7454 ));
7455
7456 cx.update_editor(|editor, window, cx| {
7457 editor.add_selection_below(&Default::default(), window, cx);
7458 });
7459
7460 cx.assert_editor_state(indoc!(
7461 r#"abc
7462 defˇghi
7463
7464 jk
7465 nlmo
7466 "#
7467 ));
7468
7469 cx.update_editor(|editor, window, cx| {
7470 editor.undo_selection(&Default::default(), window, cx);
7471 });
7472
7473 cx.assert_editor_state(indoc!(
7474 r#"abcˇ
7475 defˇghi
7476
7477 jk
7478 nlmo
7479 "#
7480 ));
7481
7482 cx.update_editor(|editor, window, cx| {
7483 editor.redo_selection(&Default::default(), window, cx);
7484 });
7485
7486 cx.assert_editor_state(indoc!(
7487 r#"abc
7488 defˇghi
7489
7490 jk
7491 nlmo
7492 "#
7493 ));
7494
7495 cx.update_editor(|editor, window, cx| {
7496 editor.add_selection_below(&Default::default(), window, cx);
7497 });
7498
7499 cx.assert_editor_state(indoc!(
7500 r#"abc
7501 defˇghi
7502 ˇ
7503 jk
7504 nlmo
7505 "#
7506 ));
7507
7508 cx.update_editor(|editor, window, cx| {
7509 editor.add_selection_below(&Default::default(), window, cx);
7510 });
7511
7512 cx.assert_editor_state(indoc!(
7513 r#"abc
7514 defˇghi
7515 ˇ
7516 jkˇ
7517 nlmo
7518 "#
7519 ));
7520
7521 cx.update_editor(|editor, window, cx| {
7522 editor.add_selection_below(&Default::default(), window, cx);
7523 });
7524
7525 cx.assert_editor_state(indoc!(
7526 r#"abc
7527 defˇghi
7528 ˇ
7529 jkˇ
7530 nlmˇo
7531 "#
7532 ));
7533
7534 cx.update_editor(|editor, window, cx| {
7535 editor.add_selection_below(&Default::default(), window, cx);
7536 });
7537
7538 cx.assert_editor_state(indoc!(
7539 r#"abc
7540 defˇghi
7541 ˇ
7542 jkˇ
7543 nlmˇo
7544 ˇ"#
7545 ));
7546
7547 // change selections
7548 cx.set_state(indoc!(
7549 r#"abc
7550 def«ˇg»hi
7551
7552 jk
7553 nlmo
7554 "#
7555 ));
7556
7557 cx.update_editor(|editor, window, cx| {
7558 editor.add_selection_below(&Default::default(), window, cx);
7559 });
7560
7561 cx.assert_editor_state(indoc!(
7562 r#"abc
7563 def«ˇg»hi
7564
7565 jk
7566 nlm«ˇo»
7567 "#
7568 ));
7569
7570 cx.update_editor(|editor, window, cx| {
7571 editor.add_selection_below(&Default::default(), window, cx);
7572 });
7573
7574 cx.assert_editor_state(indoc!(
7575 r#"abc
7576 def«ˇg»hi
7577
7578 jk
7579 nlm«ˇo»
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«ˇg»hi
7590
7591 jk
7592 nlmo
7593 "#
7594 ));
7595
7596 cx.update_editor(|editor, window, cx| {
7597 editor.add_selection_above(&Default::default(), window, cx);
7598 });
7599
7600 cx.assert_editor_state(indoc!(
7601 r#"abc
7602 def«ˇg»hi
7603
7604 jk
7605 nlmo
7606 "#
7607 ));
7608
7609 // Change selections again
7610 cx.set_state(indoc!(
7611 r#"a«bc
7612 defgˇ»hi
7613
7614 jk
7615 nlmo
7616 "#
7617 ));
7618
7619 cx.update_editor(|editor, window, cx| {
7620 editor.add_selection_below(&Default::default(), window, cx);
7621 });
7622
7623 cx.assert_editor_state(indoc!(
7624 r#"a«bcˇ»
7625 d«efgˇ»hi
7626
7627 j«kˇ»
7628 nlmo
7629 "#
7630 ));
7631
7632 cx.update_editor(|editor, window, cx| {
7633 editor.add_selection_below(&Default::default(), window, cx);
7634 });
7635 cx.assert_editor_state(indoc!(
7636 r#"a«bcˇ»
7637 d«efgˇ»hi
7638
7639 j«kˇ»
7640 n«lmoˇ»
7641 "#
7642 ));
7643 cx.update_editor(|editor, window, cx| {
7644 editor.add_selection_above(&Default::default(), window, cx);
7645 });
7646
7647 cx.assert_editor_state(indoc!(
7648 r#"a«bcˇ»
7649 d«efgˇ»hi
7650
7651 j«kˇ»
7652 nlmo
7653 "#
7654 ));
7655
7656 // Change selections again
7657 cx.set_state(indoc!(
7658 r#"abc
7659 d«ˇefghi
7660
7661 jk
7662 nlm»o
7663 "#
7664 ));
7665
7666 cx.update_editor(|editor, window, cx| {
7667 editor.add_selection_above(&Default::default(), window, cx);
7668 });
7669
7670 cx.assert_editor_state(indoc!(
7671 r#"a«ˇbc»
7672 d«ˇef»ghi
7673
7674 j«ˇk»
7675 n«ˇlm»o
7676 "#
7677 ));
7678
7679 cx.update_editor(|editor, window, cx| {
7680 editor.add_selection_below(&Default::default(), window, cx);
7681 });
7682
7683 cx.assert_editor_state(indoc!(
7684 r#"abc
7685 d«ˇef»ghi
7686
7687 j«ˇk»
7688 n«ˇlm»o
7689 "#
7690 ));
7691}
7692
7693#[gpui::test]
7694async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
7695 init_test(cx, |_| {});
7696 let mut cx = EditorTestContext::new(cx).await;
7697
7698 cx.set_state(indoc!(
7699 r#"line onˇe
7700 liˇne two
7701 line three
7702 line four"#
7703 ));
7704
7705 cx.update_editor(|editor, window, cx| {
7706 editor.add_selection_below(&Default::default(), window, cx);
7707 });
7708
7709 // test multiple cursors expand in the same direction
7710 cx.assert_editor_state(indoc!(
7711 r#"line onˇe
7712 liˇne twˇo
7713 liˇne three
7714 line four"#
7715 ));
7716
7717 cx.update_editor(|editor, window, cx| {
7718 editor.add_selection_below(&Default::default(), window, cx);
7719 });
7720
7721 cx.update_editor(|editor, window, cx| {
7722 editor.add_selection_below(&Default::default(), window, cx);
7723 });
7724
7725 // test multiple cursors expand below overflow
7726 cx.assert_editor_state(indoc!(
7727 r#"line onˇe
7728 liˇne twˇo
7729 liˇne thˇree
7730 liˇne foˇur"#
7731 ));
7732
7733 cx.update_editor(|editor, window, cx| {
7734 editor.add_selection_above(&Default::default(), window, cx);
7735 });
7736
7737 // test multiple cursors retrieves back correctly
7738 cx.assert_editor_state(indoc!(
7739 r#"line onˇe
7740 liˇne twˇo
7741 liˇne thˇree
7742 line four"#
7743 ));
7744
7745 cx.update_editor(|editor, window, cx| {
7746 editor.add_selection_above(&Default::default(), window, cx);
7747 });
7748
7749 cx.update_editor(|editor, window, cx| {
7750 editor.add_selection_above(&Default::default(), window, cx);
7751 });
7752
7753 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
7754 cx.assert_editor_state(indoc!(
7755 r#"liˇne onˇe
7756 liˇne two
7757 line three
7758 line four"#
7759 ));
7760
7761 cx.update_editor(|editor, window, cx| {
7762 editor.undo_selection(&Default::default(), window, cx);
7763 });
7764
7765 // test undo
7766 cx.assert_editor_state(indoc!(
7767 r#"line onˇe
7768 liˇne twˇo
7769 line three
7770 line four"#
7771 ));
7772
7773 cx.update_editor(|editor, window, cx| {
7774 editor.redo_selection(&Default::default(), window, cx);
7775 });
7776
7777 // test redo
7778 cx.assert_editor_state(indoc!(
7779 r#"liˇne onˇe
7780 liˇne two
7781 line three
7782 line four"#
7783 ));
7784
7785 cx.set_state(indoc!(
7786 r#"abcd
7787 ef«ghˇ»
7788 ijkl
7789 «mˇ»nop"#
7790 ));
7791
7792 cx.update_editor(|editor, window, cx| {
7793 editor.add_selection_above(&Default::default(), window, cx);
7794 });
7795
7796 // test multiple selections expand in the same direction
7797 cx.assert_editor_state(indoc!(
7798 r#"ab«cdˇ»
7799 ef«ghˇ»
7800 «iˇ»jkl
7801 «mˇ»nop"#
7802 ));
7803
7804 cx.update_editor(|editor, window, cx| {
7805 editor.add_selection_above(&Default::default(), window, cx);
7806 });
7807
7808 // test multiple selection upward overflow
7809 cx.assert_editor_state(indoc!(
7810 r#"ab«cdˇ»
7811 «eˇ»f«ghˇ»
7812 «iˇ»jkl
7813 «mˇ»nop"#
7814 ));
7815
7816 cx.update_editor(|editor, window, cx| {
7817 editor.add_selection_below(&Default::default(), window, cx);
7818 });
7819
7820 // test multiple selection retrieves back correctly
7821 cx.assert_editor_state(indoc!(
7822 r#"abcd
7823 ef«ghˇ»
7824 «iˇ»jkl
7825 «mˇ»nop"#
7826 ));
7827
7828 cx.update_editor(|editor, window, cx| {
7829 editor.add_selection_below(&Default::default(), window, cx);
7830 });
7831
7832 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
7833 cx.assert_editor_state(indoc!(
7834 r#"abcd
7835 ef«ghˇ»
7836 ij«klˇ»
7837 «mˇ»nop"#
7838 ));
7839
7840 cx.update_editor(|editor, window, cx| {
7841 editor.undo_selection(&Default::default(), window, cx);
7842 });
7843
7844 // test undo
7845 cx.assert_editor_state(indoc!(
7846 r#"abcd
7847 ef«ghˇ»
7848 «iˇ»jkl
7849 «mˇ»nop"#
7850 ));
7851
7852 cx.update_editor(|editor, window, cx| {
7853 editor.redo_selection(&Default::default(), window, cx);
7854 });
7855
7856 // test redo
7857 cx.assert_editor_state(indoc!(
7858 r#"abcd
7859 ef«ghˇ»
7860 ij«klˇ»
7861 «mˇ»nop"#
7862 ));
7863}
7864
7865#[gpui::test]
7866async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
7867 init_test(cx, |_| {});
7868 let mut cx = EditorTestContext::new(cx).await;
7869
7870 cx.set_state(indoc!(
7871 r#"line onˇe
7872 liˇne two
7873 line three
7874 line four"#
7875 ));
7876
7877 cx.update_editor(|editor, window, cx| {
7878 editor.add_selection_below(&Default::default(), window, cx);
7879 editor.add_selection_below(&Default::default(), window, cx);
7880 editor.add_selection_below(&Default::default(), window, cx);
7881 });
7882
7883 // initial state with two multi cursor groups
7884 cx.assert_editor_state(indoc!(
7885 r#"line onˇe
7886 liˇne twˇo
7887 liˇne thˇree
7888 liˇne foˇur"#
7889 ));
7890
7891 // add single cursor in middle - simulate opt click
7892 cx.update_editor(|editor, window, cx| {
7893 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
7894 editor.begin_selection(new_cursor_point, true, 1, window, cx);
7895 editor.end_selection(window, cx);
7896 });
7897
7898 cx.assert_editor_state(indoc!(
7899 r#"line onˇe
7900 liˇne twˇo
7901 liˇneˇ thˇree
7902 liˇne foˇur"#
7903 ));
7904
7905 cx.update_editor(|editor, window, cx| {
7906 editor.add_selection_above(&Default::default(), window, cx);
7907 });
7908
7909 // test new added selection expands above and existing selection shrinks
7910 cx.assert_editor_state(indoc!(
7911 r#"line onˇe
7912 liˇneˇ twˇo
7913 liˇneˇ thˇree
7914 line four"#
7915 ));
7916
7917 cx.update_editor(|editor, window, cx| {
7918 editor.add_selection_above(&Default::default(), window, cx);
7919 });
7920
7921 // test new added selection expands above and existing selection shrinks
7922 cx.assert_editor_state(indoc!(
7923 r#"lineˇ onˇe
7924 liˇneˇ twˇo
7925 lineˇ three
7926 line four"#
7927 ));
7928
7929 // intial state with two selection groups
7930 cx.set_state(indoc!(
7931 r#"abcd
7932 ef«ghˇ»
7933 ijkl
7934 «mˇ»nop"#
7935 ));
7936
7937 cx.update_editor(|editor, window, cx| {
7938 editor.add_selection_above(&Default::default(), window, cx);
7939 editor.add_selection_above(&Default::default(), window, cx);
7940 });
7941
7942 cx.assert_editor_state(indoc!(
7943 r#"ab«cdˇ»
7944 «eˇ»f«ghˇ»
7945 «iˇ»jkl
7946 «mˇ»nop"#
7947 ));
7948
7949 // add single selection in middle - simulate opt drag
7950 cx.update_editor(|editor, window, cx| {
7951 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
7952 editor.begin_selection(new_cursor_point, true, 1, window, cx);
7953 editor.update_selection(
7954 DisplayPoint::new(DisplayRow(2), 4),
7955 0,
7956 gpui::Point::<f32>::default(),
7957 window,
7958 cx,
7959 );
7960 editor.end_selection(window, cx);
7961 });
7962
7963 cx.assert_editor_state(indoc!(
7964 r#"ab«cdˇ»
7965 «eˇ»f«ghˇ»
7966 «iˇ»jk«lˇ»
7967 «mˇ»nop"#
7968 ));
7969
7970 cx.update_editor(|editor, window, cx| {
7971 editor.add_selection_below(&Default::default(), window, cx);
7972 });
7973
7974 // test new added selection expands below, others shrinks from above
7975 cx.assert_editor_state(indoc!(
7976 r#"abcd
7977 ef«ghˇ»
7978 «iˇ»jk«lˇ»
7979 «mˇ»no«pˇ»"#
7980 ));
7981}
7982
7983#[gpui::test]
7984async fn test_select_next(cx: &mut TestAppContext) {
7985 init_test(cx, |_| {});
7986
7987 let mut cx = EditorTestContext::new(cx).await;
7988 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7989
7990 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7991 .unwrap();
7992 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7993
7994 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7995 .unwrap();
7996 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
7997
7998 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7999 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8000
8001 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8002 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8003
8004 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8005 .unwrap();
8006 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8007
8008 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8009 .unwrap();
8010 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8011
8012 // Test selection direction should be preserved
8013 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8014
8015 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8016 .unwrap();
8017 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
8018}
8019
8020#[gpui::test]
8021async fn test_select_all_matches(cx: &mut TestAppContext) {
8022 init_test(cx, |_| {});
8023
8024 let mut cx = EditorTestContext::new(cx).await;
8025
8026 // Test caret-only selections
8027 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8028 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8029 .unwrap();
8030 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8031
8032 // Test left-to-right selections
8033 cx.set_state("abc\n«abcˇ»\nabc");
8034 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8035 .unwrap();
8036 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
8037
8038 // Test right-to-left selections
8039 cx.set_state("abc\n«ˇabc»\nabc");
8040 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8041 .unwrap();
8042 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
8043
8044 // Test selecting whitespace with caret selection
8045 cx.set_state("abc\nˇ abc\nabc");
8046 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8047 .unwrap();
8048 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8049
8050 // Test selecting whitespace with left-to-right selection
8051 cx.set_state("abc\n«ˇ »abc\nabc");
8052 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8053 .unwrap();
8054 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
8055
8056 // Test no matches with right-to-left selection
8057 cx.set_state("abc\n« ˇ»abc\nabc");
8058 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8059 .unwrap();
8060 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8061
8062 // Test with a single word and clip_at_line_ends=true (#29823)
8063 cx.set_state("aˇbc");
8064 cx.update_editor(|e, window, cx| {
8065 e.set_clip_at_line_ends(true, cx);
8066 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8067 e.set_clip_at_line_ends(false, cx);
8068 });
8069 cx.assert_editor_state("«abcˇ»");
8070}
8071
8072#[gpui::test]
8073async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
8074 init_test(cx, |_| {});
8075
8076 let mut cx = EditorTestContext::new(cx).await;
8077
8078 let large_body_1 = "\nd".repeat(200);
8079 let large_body_2 = "\ne".repeat(200);
8080
8081 cx.set_state(&format!(
8082 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
8083 ));
8084 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
8085 let scroll_position = editor.scroll_position(cx);
8086 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
8087 scroll_position
8088 });
8089
8090 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8091 .unwrap();
8092 cx.assert_editor_state(&format!(
8093 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
8094 ));
8095 let scroll_position_after_selection =
8096 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
8097 assert_eq!(
8098 initial_scroll_position, scroll_position_after_selection,
8099 "Scroll position should not change after selecting all matches"
8100 );
8101}
8102
8103#[gpui::test]
8104async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
8105 init_test(cx, |_| {});
8106
8107 let mut cx = EditorLspTestContext::new_rust(
8108 lsp::ServerCapabilities {
8109 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8110 ..Default::default()
8111 },
8112 cx,
8113 )
8114 .await;
8115
8116 cx.set_state(indoc! {"
8117 line 1
8118 line 2
8119 linˇe 3
8120 line 4
8121 line 5
8122 "});
8123
8124 // Make an edit
8125 cx.update_editor(|editor, window, cx| {
8126 editor.handle_input("X", window, cx);
8127 });
8128
8129 // Move cursor to a different position
8130 cx.update_editor(|editor, window, cx| {
8131 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8132 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
8133 });
8134 });
8135
8136 cx.assert_editor_state(indoc! {"
8137 line 1
8138 line 2
8139 linXe 3
8140 line 4
8141 liˇne 5
8142 "});
8143
8144 cx.lsp
8145 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8146 Ok(Some(vec![lsp::TextEdit::new(
8147 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8148 "PREFIX ".to_string(),
8149 )]))
8150 });
8151
8152 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
8153 .unwrap()
8154 .await
8155 .unwrap();
8156
8157 cx.assert_editor_state(indoc! {"
8158 PREFIX line 1
8159 line 2
8160 linXe 3
8161 line 4
8162 liˇne 5
8163 "});
8164
8165 // Undo formatting
8166 cx.update_editor(|editor, window, cx| {
8167 editor.undo(&Default::default(), window, cx);
8168 });
8169
8170 // Verify cursor moved back to position after edit
8171 cx.assert_editor_state(indoc! {"
8172 line 1
8173 line 2
8174 linXˇe 3
8175 line 4
8176 line 5
8177 "});
8178}
8179
8180#[gpui::test]
8181async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) {
8182 init_test(cx, |_| {});
8183
8184 let mut cx = EditorTestContext::new(cx).await;
8185
8186 let provider = cx.new(|_| FakeEditPredictionProvider::default());
8187 cx.update_editor(|editor, window, cx| {
8188 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
8189 });
8190
8191 cx.set_state(indoc! {"
8192 line 1
8193 line 2
8194 linˇe 3
8195 line 4
8196 line 5
8197 line 6
8198 line 7
8199 line 8
8200 line 9
8201 line 10
8202 "});
8203
8204 let snapshot = cx.buffer_snapshot();
8205 let edit_position = snapshot.anchor_after(Point::new(2, 4));
8206
8207 cx.update(|_, cx| {
8208 provider.update(cx, |provider, _| {
8209 provider.set_edit_prediction(Some(edit_prediction::EditPrediction {
8210 id: None,
8211 edits: vec![(edit_position..edit_position, "X".into())],
8212 edit_preview: None,
8213 }))
8214 })
8215 });
8216
8217 cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
8218 cx.update_editor(|editor, window, cx| {
8219 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
8220 });
8221
8222 cx.assert_editor_state(indoc! {"
8223 line 1
8224 line 2
8225 lineXˇ 3
8226 line 4
8227 line 5
8228 line 6
8229 line 7
8230 line 8
8231 line 9
8232 line 10
8233 "});
8234
8235 cx.update_editor(|editor, window, cx| {
8236 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8237 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
8238 });
8239 });
8240
8241 cx.assert_editor_state(indoc! {"
8242 line 1
8243 line 2
8244 lineX 3
8245 line 4
8246 line 5
8247 line 6
8248 line 7
8249 line 8
8250 line 9
8251 liˇne 10
8252 "});
8253
8254 cx.update_editor(|editor, window, cx| {
8255 editor.undo(&Default::default(), window, cx);
8256 });
8257
8258 cx.assert_editor_state(indoc! {"
8259 line 1
8260 line 2
8261 lineˇ 3
8262 line 4
8263 line 5
8264 line 6
8265 line 7
8266 line 8
8267 line 9
8268 line 10
8269 "});
8270}
8271
8272#[gpui::test]
8273async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
8274 init_test(cx, |_| {});
8275
8276 let mut cx = EditorTestContext::new(cx).await;
8277 cx.set_state(
8278 r#"let foo = 2;
8279lˇet foo = 2;
8280let fooˇ = 2;
8281let foo = 2;
8282let foo = ˇ2;"#,
8283 );
8284
8285 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8286 .unwrap();
8287 cx.assert_editor_state(
8288 r#"let foo = 2;
8289«letˇ» foo = 2;
8290let «fooˇ» = 2;
8291let foo = 2;
8292let foo = «2ˇ»;"#,
8293 );
8294
8295 // noop for multiple selections with different contents
8296 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8297 .unwrap();
8298 cx.assert_editor_state(
8299 r#"let foo = 2;
8300«letˇ» foo = 2;
8301let «fooˇ» = 2;
8302let foo = 2;
8303let foo = «2ˇ»;"#,
8304 );
8305
8306 // Test last selection direction should be preserved
8307 cx.set_state(
8308 r#"let foo = 2;
8309let foo = 2;
8310let «fooˇ» = 2;
8311let «ˇfoo» = 2;
8312let foo = 2;"#,
8313 );
8314
8315 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8316 .unwrap();
8317 cx.assert_editor_state(
8318 r#"let foo = 2;
8319let foo = 2;
8320let «fooˇ» = 2;
8321let «ˇfoo» = 2;
8322let «ˇfoo» = 2;"#,
8323 );
8324}
8325
8326#[gpui::test]
8327async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
8328 init_test(cx, |_| {});
8329
8330 let mut cx =
8331 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
8332
8333 cx.assert_editor_state(indoc! {"
8334 ˇbbb
8335 ccc
8336
8337 bbb
8338 ccc
8339 "});
8340 cx.dispatch_action(SelectPrevious::default());
8341 cx.assert_editor_state(indoc! {"
8342 «bbbˇ»
8343 ccc
8344
8345 bbb
8346 ccc
8347 "});
8348 cx.dispatch_action(SelectPrevious::default());
8349 cx.assert_editor_state(indoc! {"
8350 «bbbˇ»
8351 ccc
8352
8353 «bbbˇ»
8354 ccc
8355 "});
8356}
8357
8358#[gpui::test]
8359async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
8360 init_test(cx, |_| {});
8361
8362 let mut cx = EditorTestContext::new(cx).await;
8363 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8364
8365 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8366 .unwrap();
8367 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8368
8369 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8370 .unwrap();
8371 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8372
8373 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8374 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8375
8376 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8377 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8378
8379 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8380 .unwrap();
8381 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
8382
8383 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8384 .unwrap();
8385 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8386}
8387
8388#[gpui::test]
8389async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
8390 init_test(cx, |_| {});
8391
8392 let mut cx = EditorTestContext::new(cx).await;
8393 cx.set_state("aˇ");
8394
8395 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8396 .unwrap();
8397 cx.assert_editor_state("«aˇ»");
8398 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8399 .unwrap();
8400 cx.assert_editor_state("«aˇ»");
8401}
8402
8403#[gpui::test]
8404async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
8405 init_test(cx, |_| {});
8406
8407 let mut cx = EditorTestContext::new(cx).await;
8408 cx.set_state(
8409 r#"let foo = 2;
8410lˇet foo = 2;
8411let fooˇ = 2;
8412let foo = 2;
8413let foo = ˇ2;"#,
8414 );
8415
8416 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8417 .unwrap();
8418 cx.assert_editor_state(
8419 r#"let foo = 2;
8420«letˇ» foo = 2;
8421let «fooˇ» = 2;
8422let foo = 2;
8423let foo = «2ˇ»;"#,
8424 );
8425
8426 // noop for multiple selections with different contents
8427 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8428 .unwrap();
8429 cx.assert_editor_state(
8430 r#"let foo = 2;
8431«letˇ» foo = 2;
8432let «fooˇ» = 2;
8433let foo = 2;
8434let foo = «2ˇ»;"#,
8435 );
8436}
8437
8438#[gpui::test]
8439async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
8440 init_test(cx, |_| {});
8441
8442 let mut cx = EditorTestContext::new(cx).await;
8443 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8444
8445 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8446 .unwrap();
8447 // selection direction is preserved
8448 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8449
8450 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8451 .unwrap();
8452 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8453
8454 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8455 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8456
8457 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8458 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8459
8460 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8461 .unwrap();
8462 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
8463
8464 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8465 .unwrap();
8466 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
8467}
8468
8469#[gpui::test]
8470async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
8471 init_test(cx, |_| {});
8472
8473 let language = Arc::new(Language::new(
8474 LanguageConfig::default(),
8475 Some(tree_sitter_rust::LANGUAGE.into()),
8476 ));
8477
8478 let text = r#"
8479 use mod1::mod2::{mod3, mod4};
8480
8481 fn fn_1(param1: bool, param2: &str) {
8482 let var1 = "text";
8483 }
8484 "#
8485 .unindent();
8486
8487 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8488 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8489 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8490
8491 editor
8492 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8493 .await;
8494
8495 editor.update_in(cx, |editor, window, cx| {
8496 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8497 s.select_display_ranges([
8498 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
8499 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
8500 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
8501 ]);
8502 });
8503 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8504 });
8505 editor.update(cx, |editor, cx| {
8506 assert_text_with_selections(
8507 editor,
8508 indoc! {r#"
8509 use mod1::mod2::{mod3, «mod4ˇ»};
8510
8511 fn fn_1«ˇ(param1: bool, param2: &str)» {
8512 let var1 = "«ˇtext»";
8513 }
8514 "#},
8515 cx,
8516 );
8517 });
8518
8519 editor.update_in(cx, |editor, window, cx| {
8520 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8521 });
8522 editor.update(cx, |editor, cx| {
8523 assert_text_with_selections(
8524 editor,
8525 indoc! {r#"
8526 use mod1::mod2::«{mod3, mod4}ˇ»;
8527
8528 «ˇfn fn_1(param1: bool, param2: &str) {
8529 let var1 = "text";
8530 }»
8531 "#},
8532 cx,
8533 );
8534 });
8535
8536 editor.update_in(cx, |editor, window, cx| {
8537 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8538 });
8539 assert_eq!(
8540 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
8541 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8542 );
8543
8544 // Trying to expand the selected syntax node one more time has no effect.
8545 editor.update_in(cx, |editor, window, cx| {
8546 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8547 });
8548 assert_eq!(
8549 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
8550 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8551 );
8552
8553 editor.update_in(cx, |editor, window, cx| {
8554 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8555 });
8556 editor.update(cx, |editor, cx| {
8557 assert_text_with_selections(
8558 editor,
8559 indoc! {r#"
8560 use mod1::mod2::«{mod3, mod4}ˇ»;
8561
8562 «ˇfn fn_1(param1: bool, param2: &str) {
8563 let var1 = "text";
8564 }»
8565 "#},
8566 cx,
8567 );
8568 });
8569
8570 editor.update_in(cx, |editor, window, cx| {
8571 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8572 });
8573 editor.update(cx, |editor, cx| {
8574 assert_text_with_selections(
8575 editor,
8576 indoc! {r#"
8577 use mod1::mod2::{mod3, «mod4ˇ»};
8578
8579 fn fn_1«ˇ(param1: bool, param2: &str)» {
8580 let var1 = "«ˇtext»";
8581 }
8582 "#},
8583 cx,
8584 );
8585 });
8586
8587 editor.update_in(cx, |editor, window, cx| {
8588 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8589 });
8590 editor.update(cx, |editor, cx| {
8591 assert_text_with_selections(
8592 editor,
8593 indoc! {r#"
8594 use mod1::mod2::{mod3, moˇd4};
8595
8596 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
8597 let var1 = "teˇxt";
8598 }
8599 "#},
8600 cx,
8601 );
8602 });
8603
8604 // Trying to shrink the selected syntax node one more time has no effect.
8605 editor.update_in(cx, |editor, window, cx| {
8606 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8607 });
8608 editor.update_in(cx, |editor, _, cx| {
8609 assert_text_with_selections(
8610 editor,
8611 indoc! {r#"
8612 use mod1::mod2::{mod3, moˇd4};
8613
8614 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
8615 let var1 = "teˇxt";
8616 }
8617 "#},
8618 cx,
8619 );
8620 });
8621
8622 // Ensure that we keep expanding the selection if the larger selection starts or ends within
8623 // a fold.
8624 editor.update_in(cx, |editor, window, cx| {
8625 editor.fold_creases(
8626 vec![
8627 Crease::simple(
8628 Point::new(0, 21)..Point::new(0, 24),
8629 FoldPlaceholder::test(),
8630 ),
8631 Crease::simple(
8632 Point::new(3, 20)..Point::new(3, 22),
8633 FoldPlaceholder::test(),
8634 ),
8635 ],
8636 true,
8637 window,
8638 cx,
8639 );
8640 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8641 });
8642 editor.update(cx, |editor, cx| {
8643 assert_text_with_selections(
8644 editor,
8645 indoc! {r#"
8646 use mod1::mod2::«{mod3, mod4}ˇ»;
8647
8648 fn fn_1«ˇ(param1: bool, param2: &str)» {
8649 let var1 = "«ˇtext»";
8650 }
8651 "#},
8652 cx,
8653 );
8654 });
8655}
8656
8657#[gpui::test]
8658async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
8659 init_test(cx, |_| {});
8660
8661 let language = Arc::new(Language::new(
8662 LanguageConfig::default(),
8663 Some(tree_sitter_rust::LANGUAGE.into()),
8664 ));
8665
8666 let text = "let a = 2;";
8667
8668 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8669 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8670 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8671
8672 editor
8673 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8674 .await;
8675
8676 // Test case 1: Cursor at end of word
8677 editor.update_in(cx, |editor, window, cx| {
8678 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8679 s.select_display_ranges([
8680 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
8681 ]);
8682 });
8683 });
8684 editor.update(cx, |editor, cx| {
8685 assert_text_with_selections(editor, "let aˇ = 2;", cx);
8686 });
8687 editor.update_in(cx, |editor, window, cx| {
8688 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8689 });
8690 editor.update(cx, |editor, cx| {
8691 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
8692 });
8693 editor.update_in(cx, |editor, window, cx| {
8694 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8695 });
8696 editor.update(cx, |editor, cx| {
8697 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
8698 });
8699
8700 // Test case 2: Cursor at end of statement
8701 editor.update_in(cx, |editor, window, cx| {
8702 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8703 s.select_display_ranges([
8704 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
8705 ]);
8706 });
8707 });
8708 editor.update(cx, |editor, cx| {
8709 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
8710 });
8711 editor.update_in(cx, |editor, window, cx| {
8712 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8713 });
8714 editor.update(cx, |editor, cx| {
8715 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
8716 });
8717}
8718
8719#[gpui::test]
8720async fn test_select_larger_syntax_node_for_cursor_at_symbol(cx: &mut TestAppContext) {
8721 init_test(cx, |_| {});
8722
8723 let language = Arc::new(Language::new(
8724 LanguageConfig {
8725 name: "JavaScript".into(),
8726 ..Default::default()
8727 },
8728 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
8729 ));
8730
8731 let text = r#"
8732 let a = {
8733 key: "value",
8734 };
8735 "#
8736 .unindent();
8737
8738 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8739 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8740 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8741
8742 editor
8743 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8744 .await;
8745
8746 // Test case 1: Cursor after '{'
8747 editor.update_in(cx, |editor, window, cx| {
8748 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8749 s.select_display_ranges([
8750 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 9)
8751 ]);
8752 });
8753 });
8754 editor.update(cx, |editor, cx| {
8755 assert_text_with_selections(
8756 editor,
8757 indoc! {r#"
8758 let a = {ˇ
8759 key: "value",
8760 };
8761 "#},
8762 cx,
8763 );
8764 });
8765 editor.update_in(cx, |editor, window, cx| {
8766 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8767 });
8768 editor.update(cx, |editor, cx| {
8769 assert_text_with_selections(
8770 editor,
8771 indoc! {r#"
8772 let a = «ˇ{
8773 key: "value",
8774 }»;
8775 "#},
8776 cx,
8777 );
8778 });
8779
8780 // Test case 2: Cursor after ':'
8781 editor.update_in(cx, |editor, window, cx| {
8782 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8783 s.select_display_ranges([
8784 DisplayPoint::new(DisplayRow(1), 8)..DisplayPoint::new(DisplayRow(1), 8)
8785 ]);
8786 });
8787 });
8788 editor.update(cx, |editor, cx| {
8789 assert_text_with_selections(
8790 editor,
8791 indoc! {r#"
8792 let a = {
8793 key:ˇ "value",
8794 };
8795 "#},
8796 cx,
8797 );
8798 });
8799 editor.update_in(cx, |editor, window, cx| {
8800 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8801 });
8802 editor.update(cx, |editor, cx| {
8803 assert_text_with_selections(
8804 editor,
8805 indoc! {r#"
8806 let a = {
8807 «ˇkey: "value"»,
8808 };
8809 "#},
8810 cx,
8811 );
8812 });
8813 editor.update_in(cx, |editor, window, cx| {
8814 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8815 });
8816 editor.update(cx, |editor, cx| {
8817 assert_text_with_selections(
8818 editor,
8819 indoc! {r#"
8820 let a = «ˇ{
8821 key: "value",
8822 }»;
8823 "#},
8824 cx,
8825 );
8826 });
8827
8828 // Test case 3: Cursor after ','
8829 editor.update_in(cx, |editor, window, cx| {
8830 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8831 s.select_display_ranges([
8832 DisplayPoint::new(DisplayRow(1), 17)..DisplayPoint::new(DisplayRow(1), 17)
8833 ]);
8834 });
8835 });
8836 editor.update(cx, |editor, cx| {
8837 assert_text_with_selections(
8838 editor,
8839 indoc! {r#"
8840 let a = {
8841 key: "value",ˇ
8842 };
8843 "#},
8844 cx,
8845 );
8846 });
8847 editor.update_in(cx, |editor, window, cx| {
8848 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8849 });
8850 editor.update(cx, |editor, cx| {
8851 assert_text_with_selections(
8852 editor,
8853 indoc! {r#"
8854 let a = «ˇ{
8855 key: "value",
8856 }»;
8857 "#},
8858 cx,
8859 );
8860 });
8861
8862 // Test case 4: Cursor after ';'
8863 editor.update_in(cx, |editor, window, cx| {
8864 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8865 s.select_display_ranges([
8866 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)
8867 ]);
8868 });
8869 });
8870 editor.update(cx, |editor, cx| {
8871 assert_text_with_selections(
8872 editor,
8873 indoc! {r#"
8874 let a = {
8875 key: "value",
8876 };ˇ
8877 "#},
8878 cx,
8879 );
8880 });
8881 editor.update_in(cx, |editor, window, cx| {
8882 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8883 });
8884 editor.update(cx, |editor, cx| {
8885 assert_text_with_selections(
8886 editor,
8887 indoc! {r#"
8888 «ˇlet a = {
8889 key: "value",
8890 };
8891 »"#},
8892 cx,
8893 );
8894 });
8895}
8896
8897#[gpui::test]
8898async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
8899 init_test(cx, |_| {});
8900
8901 let language = Arc::new(Language::new(
8902 LanguageConfig::default(),
8903 Some(tree_sitter_rust::LANGUAGE.into()),
8904 ));
8905
8906 let text = r#"
8907 use mod1::mod2::{mod3, mod4};
8908
8909 fn fn_1(param1: bool, param2: &str) {
8910 let var1 = "hello world";
8911 }
8912 "#
8913 .unindent();
8914
8915 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8916 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8917 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8918
8919 editor
8920 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8921 .await;
8922
8923 // Test 1: Cursor on a letter of a string word
8924 editor.update_in(cx, |editor, window, cx| {
8925 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8926 s.select_display_ranges([
8927 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
8928 ]);
8929 });
8930 });
8931 editor.update_in(cx, |editor, window, cx| {
8932 assert_text_with_selections(
8933 editor,
8934 indoc! {r#"
8935 use mod1::mod2::{mod3, mod4};
8936
8937 fn fn_1(param1: bool, param2: &str) {
8938 let var1 = "hˇello world";
8939 }
8940 "#},
8941 cx,
8942 );
8943 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8944 assert_text_with_selections(
8945 editor,
8946 indoc! {r#"
8947 use mod1::mod2::{mod3, mod4};
8948
8949 fn fn_1(param1: bool, param2: &str) {
8950 let var1 = "«ˇhello» world";
8951 }
8952 "#},
8953 cx,
8954 );
8955 });
8956
8957 // Test 2: Partial selection within a word
8958 editor.update_in(cx, |editor, window, cx| {
8959 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8960 s.select_display_ranges([
8961 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
8962 ]);
8963 });
8964 });
8965 editor.update_in(cx, |editor, window, cx| {
8966 assert_text_with_selections(
8967 editor,
8968 indoc! {r#"
8969 use mod1::mod2::{mod3, mod4};
8970
8971 fn fn_1(param1: bool, param2: &str) {
8972 let var1 = "h«elˇ»lo world";
8973 }
8974 "#},
8975 cx,
8976 );
8977 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8978 assert_text_with_selections(
8979 editor,
8980 indoc! {r#"
8981 use mod1::mod2::{mod3, mod4};
8982
8983 fn fn_1(param1: bool, param2: &str) {
8984 let var1 = "«ˇhello» world";
8985 }
8986 "#},
8987 cx,
8988 );
8989 });
8990
8991 // Test 3: Complete word already selected
8992 editor.update_in(cx, |editor, window, cx| {
8993 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8994 s.select_display_ranges([
8995 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
8996 ]);
8997 });
8998 });
8999 editor.update_in(cx, |editor, window, cx| {
9000 assert_text_with_selections(
9001 editor,
9002 indoc! {r#"
9003 use mod1::mod2::{mod3, mod4};
9004
9005 fn fn_1(param1: bool, param2: &str) {
9006 let var1 = "«helloˇ» world";
9007 }
9008 "#},
9009 cx,
9010 );
9011 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9012 assert_text_with_selections(
9013 editor,
9014 indoc! {r#"
9015 use mod1::mod2::{mod3, mod4};
9016
9017 fn fn_1(param1: bool, param2: &str) {
9018 let var1 = "«hello worldˇ»";
9019 }
9020 "#},
9021 cx,
9022 );
9023 });
9024
9025 // Test 4: Selection spanning across words
9026 editor.update_in(cx, |editor, window, cx| {
9027 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9028 s.select_display_ranges([
9029 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
9030 ]);
9031 });
9032 });
9033 editor.update_in(cx, |editor, window, cx| {
9034 assert_text_with_selections(
9035 editor,
9036 indoc! {r#"
9037 use mod1::mod2::{mod3, mod4};
9038
9039 fn fn_1(param1: bool, param2: &str) {
9040 let var1 = "hel«lo woˇ»rld";
9041 }
9042 "#},
9043 cx,
9044 );
9045 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9046 assert_text_with_selections(
9047 editor,
9048 indoc! {r#"
9049 use mod1::mod2::{mod3, mod4};
9050
9051 fn fn_1(param1: bool, param2: &str) {
9052 let var1 = "«ˇhello world»";
9053 }
9054 "#},
9055 cx,
9056 );
9057 });
9058
9059 // Test 5: Expansion beyond string
9060 editor.update_in(cx, |editor, window, cx| {
9061 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9062 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9063 assert_text_with_selections(
9064 editor,
9065 indoc! {r#"
9066 use mod1::mod2::{mod3, mod4};
9067
9068 fn fn_1(param1: bool, param2: &str) {
9069 «ˇlet var1 = "hello world";»
9070 }
9071 "#},
9072 cx,
9073 );
9074 });
9075}
9076
9077#[gpui::test]
9078async fn test_unwrap_syntax_nodes(cx: &mut gpui::TestAppContext) {
9079 init_test(cx, |_| {});
9080
9081 let mut cx = EditorTestContext::new(cx).await;
9082
9083 let language = Arc::new(Language::new(
9084 LanguageConfig::default(),
9085 Some(tree_sitter_rust::LANGUAGE.into()),
9086 ));
9087
9088 cx.update_buffer(|buffer, cx| {
9089 buffer.set_language(Some(language), cx);
9090 });
9091
9092 cx.set_state(indoc! { r#"use mod1::{mod2::{«mod3ˇ», mod4}, mod5::{mod6, «mod7ˇ»}};"# });
9093 cx.update_editor(|editor, window, cx| {
9094 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9095 });
9096
9097 cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# });
9098}
9099
9100#[gpui::test]
9101async fn test_fold_function_bodies(cx: &mut TestAppContext) {
9102 init_test(cx, |_| {});
9103
9104 let base_text = r#"
9105 impl A {
9106 // this is an uncommitted comment
9107
9108 fn b() {
9109 c();
9110 }
9111
9112 // this is another uncommitted comment
9113
9114 fn d() {
9115 // e
9116 // f
9117 }
9118 }
9119
9120 fn g() {
9121 // h
9122 }
9123 "#
9124 .unindent();
9125
9126 let text = r#"
9127 ˇimpl A {
9128
9129 fn b() {
9130 c();
9131 }
9132
9133 fn d() {
9134 // e
9135 // f
9136 }
9137 }
9138
9139 fn g() {
9140 // h
9141 }
9142 "#
9143 .unindent();
9144
9145 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9146 cx.set_state(&text);
9147 cx.set_head_text(&base_text);
9148 cx.update_editor(|editor, window, cx| {
9149 editor.expand_all_diff_hunks(&Default::default(), window, cx);
9150 });
9151
9152 cx.assert_state_with_diff(
9153 "
9154 ˇimpl A {
9155 - // this is an uncommitted comment
9156
9157 fn b() {
9158 c();
9159 }
9160
9161 - // this is another uncommitted comment
9162 -
9163 fn d() {
9164 // e
9165 // f
9166 }
9167 }
9168
9169 fn g() {
9170 // h
9171 }
9172 "
9173 .unindent(),
9174 );
9175
9176 let expected_display_text = "
9177 impl A {
9178 // this is an uncommitted comment
9179
9180 fn b() {
9181 ⋯
9182 }
9183
9184 // this is another uncommitted comment
9185
9186 fn d() {
9187 ⋯
9188 }
9189 }
9190
9191 fn g() {
9192 ⋯
9193 }
9194 "
9195 .unindent();
9196
9197 cx.update_editor(|editor, window, cx| {
9198 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
9199 assert_eq!(editor.display_text(cx), expected_display_text);
9200 });
9201}
9202
9203#[gpui::test]
9204async fn test_autoindent(cx: &mut TestAppContext) {
9205 init_test(cx, |_| {});
9206
9207 let language = Arc::new(
9208 Language::new(
9209 LanguageConfig {
9210 brackets: BracketPairConfig {
9211 pairs: vec![
9212 BracketPair {
9213 start: "{".to_string(),
9214 end: "}".to_string(),
9215 close: false,
9216 surround: false,
9217 newline: true,
9218 },
9219 BracketPair {
9220 start: "(".to_string(),
9221 end: ")".to_string(),
9222 close: false,
9223 surround: false,
9224 newline: true,
9225 },
9226 ],
9227 ..Default::default()
9228 },
9229 ..Default::default()
9230 },
9231 Some(tree_sitter_rust::LANGUAGE.into()),
9232 )
9233 .with_indents_query(
9234 r#"
9235 (_ "(" ")" @end) @indent
9236 (_ "{" "}" @end) @indent
9237 "#,
9238 )
9239 .unwrap(),
9240 );
9241
9242 let text = "fn a() {}";
9243
9244 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9245 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9246 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9247 editor
9248 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9249 .await;
9250
9251 editor.update_in(cx, |editor, window, cx| {
9252 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9253 s.select_ranges([5..5, 8..8, 9..9])
9254 });
9255 editor.newline(&Newline, window, cx);
9256 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
9257 assert_eq!(
9258 editor.selections.ranges(cx),
9259 &[
9260 Point::new(1, 4)..Point::new(1, 4),
9261 Point::new(3, 4)..Point::new(3, 4),
9262 Point::new(5, 0)..Point::new(5, 0)
9263 ]
9264 );
9265 });
9266}
9267
9268#[gpui::test]
9269async fn test_autoindent_disabled(cx: &mut TestAppContext) {
9270 init_test(cx, |settings| settings.defaults.auto_indent = Some(false));
9271
9272 let language = Arc::new(
9273 Language::new(
9274 LanguageConfig {
9275 brackets: BracketPairConfig {
9276 pairs: vec![
9277 BracketPair {
9278 start: "{".to_string(),
9279 end: "}".to_string(),
9280 close: false,
9281 surround: false,
9282 newline: true,
9283 },
9284 BracketPair {
9285 start: "(".to_string(),
9286 end: ")".to_string(),
9287 close: false,
9288 surround: false,
9289 newline: true,
9290 },
9291 ],
9292 ..Default::default()
9293 },
9294 ..Default::default()
9295 },
9296 Some(tree_sitter_rust::LANGUAGE.into()),
9297 )
9298 .with_indents_query(
9299 r#"
9300 (_ "(" ")" @end) @indent
9301 (_ "{" "}" @end) @indent
9302 "#,
9303 )
9304 .unwrap(),
9305 );
9306
9307 let text = "fn a() {}";
9308
9309 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9310 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9311 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9312 editor
9313 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9314 .await;
9315
9316 editor.update_in(cx, |editor, window, cx| {
9317 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9318 s.select_ranges([5..5, 8..8, 9..9])
9319 });
9320 editor.newline(&Newline, window, cx);
9321 assert_eq!(
9322 editor.text(cx),
9323 indoc!(
9324 "
9325 fn a(
9326
9327 ) {
9328
9329 }
9330 "
9331 )
9332 );
9333 assert_eq!(
9334 editor.selections.ranges(cx),
9335 &[
9336 Point::new(1, 0)..Point::new(1, 0),
9337 Point::new(3, 0)..Point::new(3, 0),
9338 Point::new(5, 0)..Point::new(5, 0)
9339 ]
9340 );
9341 });
9342}
9343
9344#[gpui::test]
9345async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) {
9346 init_test(cx, |settings| {
9347 settings.defaults.auto_indent = Some(true);
9348 settings.languages.0.insert(
9349 "python".into(),
9350 LanguageSettingsContent {
9351 auto_indent: Some(false),
9352 ..Default::default()
9353 },
9354 );
9355 });
9356
9357 let mut cx = EditorTestContext::new(cx).await;
9358
9359 let injected_language = Arc::new(
9360 Language::new(
9361 LanguageConfig {
9362 brackets: BracketPairConfig {
9363 pairs: vec![
9364 BracketPair {
9365 start: "{".to_string(),
9366 end: "}".to_string(),
9367 close: false,
9368 surround: false,
9369 newline: true,
9370 },
9371 BracketPair {
9372 start: "(".to_string(),
9373 end: ")".to_string(),
9374 close: true,
9375 surround: false,
9376 newline: true,
9377 },
9378 ],
9379 ..Default::default()
9380 },
9381 name: "python".into(),
9382 ..Default::default()
9383 },
9384 Some(tree_sitter_python::LANGUAGE.into()),
9385 )
9386 .with_indents_query(
9387 r#"
9388 (_ "(" ")" @end) @indent
9389 (_ "{" "}" @end) @indent
9390 "#,
9391 )
9392 .unwrap(),
9393 );
9394
9395 let language = Arc::new(
9396 Language::new(
9397 LanguageConfig {
9398 brackets: BracketPairConfig {
9399 pairs: vec![
9400 BracketPair {
9401 start: "{".to_string(),
9402 end: "}".to_string(),
9403 close: false,
9404 surround: false,
9405 newline: true,
9406 },
9407 BracketPair {
9408 start: "(".to_string(),
9409 end: ")".to_string(),
9410 close: true,
9411 surround: false,
9412 newline: true,
9413 },
9414 ],
9415 ..Default::default()
9416 },
9417 name: LanguageName::new("rust"),
9418 ..Default::default()
9419 },
9420 Some(tree_sitter_rust::LANGUAGE.into()),
9421 )
9422 .with_indents_query(
9423 r#"
9424 (_ "(" ")" @end) @indent
9425 (_ "{" "}" @end) @indent
9426 "#,
9427 )
9428 .unwrap()
9429 .with_injection_query(
9430 r#"
9431 (macro_invocation
9432 macro: (identifier) @_macro_name
9433 (token_tree) @injection.content
9434 (#set! injection.language "python"))
9435 "#,
9436 )
9437 .unwrap(),
9438 );
9439
9440 cx.language_registry().add(injected_language);
9441 cx.language_registry().add(language.clone());
9442
9443 cx.update_buffer(|buffer, cx| {
9444 buffer.set_language(Some(language), cx);
9445 });
9446
9447 cx.set_state(r#"struct A {ˇ}"#);
9448
9449 cx.update_editor(|editor, window, cx| {
9450 editor.newline(&Default::default(), window, cx);
9451 });
9452
9453 cx.assert_editor_state(indoc!(
9454 "struct A {
9455 ˇ
9456 }"
9457 ));
9458
9459 cx.set_state(r#"select_biased!(ˇ)"#);
9460
9461 cx.update_editor(|editor, window, cx| {
9462 editor.newline(&Default::default(), window, cx);
9463 editor.handle_input("def ", window, cx);
9464 editor.handle_input("(", window, cx);
9465 editor.newline(&Default::default(), window, cx);
9466 editor.handle_input("a", window, cx);
9467 });
9468
9469 cx.assert_editor_state(indoc!(
9470 "select_biased!(
9471 def (
9472 aˇ
9473 )
9474 )"
9475 ));
9476}
9477
9478#[gpui::test]
9479async fn test_autoindent_selections(cx: &mut TestAppContext) {
9480 init_test(cx, |_| {});
9481
9482 {
9483 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9484 cx.set_state(indoc! {"
9485 impl A {
9486
9487 fn b() {}
9488
9489 «fn c() {
9490
9491 }ˇ»
9492 }
9493 "});
9494
9495 cx.update_editor(|editor, window, cx| {
9496 editor.autoindent(&Default::default(), window, cx);
9497 });
9498
9499 cx.assert_editor_state(indoc! {"
9500 impl A {
9501
9502 fn b() {}
9503
9504 «fn c() {
9505
9506 }ˇ»
9507 }
9508 "});
9509 }
9510
9511 {
9512 let mut cx = EditorTestContext::new_multibuffer(
9513 cx,
9514 [indoc! { "
9515 impl A {
9516 «
9517 // a
9518 fn b(){}
9519 »
9520 «
9521 }
9522 fn c(){}
9523 »
9524 "}],
9525 );
9526
9527 let buffer = cx.update_editor(|editor, _, cx| {
9528 let buffer = editor.buffer().update(cx, |buffer, _| {
9529 buffer.all_buffers().iter().next().unwrap().clone()
9530 });
9531 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
9532 buffer
9533 });
9534
9535 cx.run_until_parked();
9536 cx.update_editor(|editor, window, cx| {
9537 editor.select_all(&Default::default(), window, cx);
9538 editor.autoindent(&Default::default(), window, cx)
9539 });
9540 cx.run_until_parked();
9541
9542 cx.update(|_, cx| {
9543 assert_eq!(
9544 buffer.read(cx).text(),
9545 indoc! { "
9546 impl A {
9547
9548 // a
9549 fn b(){}
9550
9551
9552 }
9553 fn c(){}
9554
9555 " }
9556 )
9557 });
9558 }
9559}
9560
9561#[gpui::test]
9562async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
9563 init_test(cx, |_| {});
9564
9565 let mut cx = EditorTestContext::new(cx).await;
9566
9567 let language = Arc::new(Language::new(
9568 LanguageConfig {
9569 brackets: BracketPairConfig {
9570 pairs: vec![
9571 BracketPair {
9572 start: "{".to_string(),
9573 end: "}".to_string(),
9574 close: true,
9575 surround: true,
9576 newline: true,
9577 },
9578 BracketPair {
9579 start: "(".to_string(),
9580 end: ")".to_string(),
9581 close: true,
9582 surround: true,
9583 newline: true,
9584 },
9585 BracketPair {
9586 start: "/*".to_string(),
9587 end: " */".to_string(),
9588 close: true,
9589 surround: true,
9590 newline: true,
9591 },
9592 BracketPair {
9593 start: "[".to_string(),
9594 end: "]".to_string(),
9595 close: false,
9596 surround: false,
9597 newline: true,
9598 },
9599 BracketPair {
9600 start: "\"".to_string(),
9601 end: "\"".to_string(),
9602 close: true,
9603 surround: true,
9604 newline: false,
9605 },
9606 BracketPair {
9607 start: "<".to_string(),
9608 end: ">".to_string(),
9609 close: false,
9610 surround: true,
9611 newline: true,
9612 },
9613 ],
9614 ..Default::default()
9615 },
9616 autoclose_before: "})]".to_string(),
9617 ..Default::default()
9618 },
9619 Some(tree_sitter_rust::LANGUAGE.into()),
9620 ));
9621
9622 cx.language_registry().add(language.clone());
9623 cx.update_buffer(|buffer, cx| {
9624 buffer.set_language(Some(language), cx);
9625 });
9626
9627 cx.set_state(
9628 &r#"
9629 🏀ˇ
9630 εˇ
9631 ❤️ˇ
9632 "#
9633 .unindent(),
9634 );
9635
9636 // autoclose multiple nested brackets at multiple cursors
9637 cx.update_editor(|editor, window, cx| {
9638 editor.handle_input("{", window, cx);
9639 editor.handle_input("{", window, cx);
9640 editor.handle_input("{", window, cx);
9641 });
9642 cx.assert_editor_state(
9643 &"
9644 🏀{{{ˇ}}}
9645 ε{{{ˇ}}}
9646 ❤️{{{ˇ}}}
9647 "
9648 .unindent(),
9649 );
9650
9651 // insert a different closing bracket
9652 cx.update_editor(|editor, window, cx| {
9653 editor.handle_input(")", window, cx);
9654 });
9655 cx.assert_editor_state(
9656 &"
9657 🏀{{{)ˇ}}}
9658 ε{{{)ˇ}}}
9659 ❤️{{{)ˇ}}}
9660 "
9661 .unindent(),
9662 );
9663
9664 // skip over the auto-closed brackets when typing a closing bracket
9665 cx.update_editor(|editor, window, cx| {
9666 editor.move_right(&MoveRight, window, cx);
9667 editor.handle_input("}", window, cx);
9668 editor.handle_input("}", window, cx);
9669 editor.handle_input("}", window, cx);
9670 });
9671 cx.assert_editor_state(
9672 &"
9673 🏀{{{)}}}}ˇ
9674 ε{{{)}}}}ˇ
9675 ❤️{{{)}}}}ˇ
9676 "
9677 .unindent(),
9678 );
9679
9680 // autoclose multi-character pairs
9681 cx.set_state(
9682 &"
9683 ˇ
9684 ˇ
9685 "
9686 .unindent(),
9687 );
9688 cx.update_editor(|editor, window, cx| {
9689 editor.handle_input("/", window, cx);
9690 editor.handle_input("*", window, cx);
9691 });
9692 cx.assert_editor_state(
9693 &"
9694 /*ˇ */
9695 /*ˇ */
9696 "
9697 .unindent(),
9698 );
9699
9700 // one cursor autocloses a multi-character pair, one cursor
9701 // does not autoclose.
9702 cx.set_state(
9703 &"
9704 /ˇ
9705 ˇ
9706 "
9707 .unindent(),
9708 );
9709 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
9710 cx.assert_editor_state(
9711 &"
9712 /*ˇ */
9713 *ˇ
9714 "
9715 .unindent(),
9716 );
9717
9718 // Don't autoclose if the next character isn't whitespace and isn't
9719 // listed in the language's "autoclose_before" section.
9720 cx.set_state("ˇa b");
9721 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
9722 cx.assert_editor_state("{ˇa b");
9723
9724 // Don't autoclose if `close` is false for the bracket pair
9725 cx.set_state("ˇ");
9726 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
9727 cx.assert_editor_state("[ˇ");
9728
9729 // Surround with brackets if text is selected
9730 cx.set_state("«aˇ» b");
9731 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
9732 cx.assert_editor_state("{«aˇ»} b");
9733
9734 // Autoclose when not immediately after a word character
9735 cx.set_state("a ˇ");
9736 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9737 cx.assert_editor_state("a \"ˇ\"");
9738
9739 // Autoclose pair where the start and end characters are the same
9740 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9741 cx.assert_editor_state("a \"\"ˇ");
9742
9743 // Don't autoclose when immediately after a word character
9744 cx.set_state("aˇ");
9745 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9746 cx.assert_editor_state("a\"ˇ");
9747
9748 // Do autoclose when after a non-word character
9749 cx.set_state("{ˇ");
9750 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9751 cx.assert_editor_state("{\"ˇ\"");
9752
9753 // Non identical pairs autoclose regardless of preceding character
9754 cx.set_state("aˇ");
9755 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
9756 cx.assert_editor_state("a{ˇ}");
9757
9758 // Don't autoclose pair if autoclose is disabled
9759 cx.set_state("ˇ");
9760 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
9761 cx.assert_editor_state("<ˇ");
9762
9763 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
9764 cx.set_state("«aˇ» b");
9765 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
9766 cx.assert_editor_state("<«aˇ»> b");
9767}
9768
9769#[gpui::test]
9770async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
9771 init_test(cx, |settings| {
9772 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
9773 });
9774
9775 let mut cx = EditorTestContext::new(cx).await;
9776
9777 let language = Arc::new(Language::new(
9778 LanguageConfig {
9779 brackets: BracketPairConfig {
9780 pairs: vec![
9781 BracketPair {
9782 start: "{".to_string(),
9783 end: "}".to_string(),
9784 close: true,
9785 surround: true,
9786 newline: true,
9787 },
9788 BracketPair {
9789 start: "(".to_string(),
9790 end: ")".to_string(),
9791 close: true,
9792 surround: true,
9793 newline: true,
9794 },
9795 BracketPair {
9796 start: "[".to_string(),
9797 end: "]".to_string(),
9798 close: false,
9799 surround: false,
9800 newline: true,
9801 },
9802 ],
9803 ..Default::default()
9804 },
9805 autoclose_before: "})]".to_string(),
9806 ..Default::default()
9807 },
9808 Some(tree_sitter_rust::LANGUAGE.into()),
9809 ));
9810
9811 cx.language_registry().add(language.clone());
9812 cx.update_buffer(|buffer, cx| {
9813 buffer.set_language(Some(language), cx);
9814 });
9815
9816 cx.set_state(
9817 &"
9818 ˇ
9819 ˇ
9820 ˇ
9821 "
9822 .unindent(),
9823 );
9824
9825 // ensure only matching closing brackets are skipped over
9826 cx.update_editor(|editor, window, cx| {
9827 editor.handle_input("}", window, cx);
9828 editor.move_left(&MoveLeft, window, cx);
9829 editor.handle_input(")", window, cx);
9830 editor.move_left(&MoveLeft, window, cx);
9831 });
9832 cx.assert_editor_state(
9833 &"
9834 ˇ)}
9835 ˇ)}
9836 ˇ)}
9837 "
9838 .unindent(),
9839 );
9840
9841 // skip-over closing brackets at multiple cursors
9842 cx.update_editor(|editor, window, cx| {
9843 editor.handle_input(")", window, cx);
9844 editor.handle_input("}", window, cx);
9845 });
9846 cx.assert_editor_state(
9847 &"
9848 )}ˇ
9849 )}ˇ
9850 )}ˇ
9851 "
9852 .unindent(),
9853 );
9854
9855 // ignore non-close brackets
9856 cx.update_editor(|editor, window, cx| {
9857 editor.handle_input("]", window, cx);
9858 editor.move_left(&MoveLeft, window, cx);
9859 editor.handle_input("]", window, cx);
9860 });
9861 cx.assert_editor_state(
9862 &"
9863 )}]ˇ]
9864 )}]ˇ]
9865 )}]ˇ]
9866 "
9867 .unindent(),
9868 );
9869}
9870
9871#[gpui::test]
9872async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
9873 init_test(cx, |_| {});
9874
9875 let mut cx = EditorTestContext::new(cx).await;
9876
9877 let html_language = Arc::new(
9878 Language::new(
9879 LanguageConfig {
9880 name: "HTML".into(),
9881 brackets: BracketPairConfig {
9882 pairs: vec![
9883 BracketPair {
9884 start: "<".into(),
9885 end: ">".into(),
9886 close: true,
9887 ..Default::default()
9888 },
9889 BracketPair {
9890 start: "{".into(),
9891 end: "}".into(),
9892 close: true,
9893 ..Default::default()
9894 },
9895 BracketPair {
9896 start: "(".into(),
9897 end: ")".into(),
9898 close: true,
9899 ..Default::default()
9900 },
9901 ],
9902 ..Default::default()
9903 },
9904 autoclose_before: "})]>".into(),
9905 ..Default::default()
9906 },
9907 Some(tree_sitter_html::LANGUAGE.into()),
9908 )
9909 .with_injection_query(
9910 r#"
9911 (script_element
9912 (raw_text) @injection.content
9913 (#set! injection.language "javascript"))
9914 "#,
9915 )
9916 .unwrap(),
9917 );
9918
9919 let javascript_language = Arc::new(Language::new(
9920 LanguageConfig {
9921 name: "JavaScript".into(),
9922 brackets: BracketPairConfig {
9923 pairs: vec![
9924 BracketPair {
9925 start: "/*".into(),
9926 end: " */".into(),
9927 close: true,
9928 ..Default::default()
9929 },
9930 BracketPair {
9931 start: "{".into(),
9932 end: "}".into(),
9933 close: true,
9934 ..Default::default()
9935 },
9936 BracketPair {
9937 start: "(".into(),
9938 end: ")".into(),
9939 close: true,
9940 ..Default::default()
9941 },
9942 ],
9943 ..Default::default()
9944 },
9945 autoclose_before: "})]>".into(),
9946 ..Default::default()
9947 },
9948 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
9949 ));
9950
9951 cx.language_registry().add(html_language.clone());
9952 cx.language_registry().add(javascript_language);
9953 cx.executor().run_until_parked();
9954
9955 cx.update_buffer(|buffer, cx| {
9956 buffer.set_language(Some(html_language), cx);
9957 });
9958
9959 cx.set_state(
9960 &r#"
9961 <body>ˇ
9962 <script>
9963 var x = 1;ˇ
9964 </script>
9965 </body>ˇ
9966 "#
9967 .unindent(),
9968 );
9969
9970 // Precondition: different languages are active at different locations.
9971 cx.update_editor(|editor, window, cx| {
9972 let snapshot = editor.snapshot(window, cx);
9973 let cursors = editor.selections.ranges::<usize>(cx);
9974 let languages = cursors
9975 .iter()
9976 .map(|c| snapshot.language_at(c.start).unwrap().name())
9977 .collect::<Vec<_>>();
9978 assert_eq!(
9979 languages,
9980 &["HTML".into(), "JavaScript".into(), "HTML".into()]
9981 );
9982 });
9983
9984 // Angle brackets autoclose in HTML, but not JavaScript.
9985 cx.update_editor(|editor, window, cx| {
9986 editor.handle_input("<", window, cx);
9987 editor.handle_input("a", window, cx);
9988 });
9989 cx.assert_editor_state(
9990 &r#"
9991 <body><aˇ>
9992 <script>
9993 var x = 1;<aˇ
9994 </script>
9995 </body><aˇ>
9996 "#
9997 .unindent(),
9998 );
9999
10000 // Curly braces and parens autoclose in both HTML and JavaScript.
10001 cx.update_editor(|editor, window, cx| {
10002 editor.handle_input(" b=", window, cx);
10003 editor.handle_input("{", window, cx);
10004 editor.handle_input("c", window, cx);
10005 editor.handle_input("(", window, cx);
10006 });
10007 cx.assert_editor_state(
10008 &r#"
10009 <body><a b={c(ˇ)}>
10010 <script>
10011 var x = 1;<a b={c(ˇ)}
10012 </script>
10013 </body><a b={c(ˇ)}>
10014 "#
10015 .unindent(),
10016 );
10017
10018 // Brackets that were already autoclosed are skipped.
10019 cx.update_editor(|editor, window, cx| {
10020 editor.handle_input(")", window, cx);
10021 editor.handle_input("d", window, cx);
10022 editor.handle_input("}", window, cx);
10023 });
10024 cx.assert_editor_state(
10025 &r#"
10026 <body><a b={c()d}ˇ>
10027 <script>
10028 var x = 1;<a b={c()d}ˇ
10029 </script>
10030 </body><a b={c()d}ˇ>
10031 "#
10032 .unindent(),
10033 );
10034 cx.update_editor(|editor, window, cx| {
10035 editor.handle_input(">", window, cx);
10036 });
10037 cx.assert_editor_state(
10038 &r#"
10039 <body><a b={c()d}>ˇ
10040 <script>
10041 var x = 1;<a b={c()d}>ˇ
10042 </script>
10043 </body><a b={c()d}>ˇ
10044 "#
10045 .unindent(),
10046 );
10047
10048 // Reset
10049 cx.set_state(
10050 &r#"
10051 <body>ˇ
10052 <script>
10053 var x = 1;ˇ
10054 </script>
10055 </body>ˇ
10056 "#
10057 .unindent(),
10058 );
10059
10060 cx.update_editor(|editor, window, cx| {
10061 editor.handle_input("<", window, cx);
10062 });
10063 cx.assert_editor_state(
10064 &r#"
10065 <body><ˇ>
10066 <script>
10067 var x = 1;<ˇ
10068 </script>
10069 </body><ˇ>
10070 "#
10071 .unindent(),
10072 );
10073
10074 // When backspacing, the closing angle brackets are removed.
10075 cx.update_editor(|editor, window, cx| {
10076 editor.backspace(&Backspace, window, cx);
10077 });
10078 cx.assert_editor_state(
10079 &r#"
10080 <body>ˇ
10081 <script>
10082 var x = 1;ˇ
10083 </script>
10084 </body>ˇ
10085 "#
10086 .unindent(),
10087 );
10088
10089 // Block comments autoclose in JavaScript, but not HTML.
10090 cx.update_editor(|editor, window, cx| {
10091 editor.handle_input("/", window, cx);
10092 editor.handle_input("*", window, cx);
10093 });
10094 cx.assert_editor_state(
10095 &r#"
10096 <body>/*ˇ
10097 <script>
10098 var x = 1;/*ˇ */
10099 </script>
10100 </body>/*ˇ
10101 "#
10102 .unindent(),
10103 );
10104}
10105
10106#[gpui::test]
10107async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
10108 init_test(cx, |_| {});
10109
10110 let mut cx = EditorTestContext::new(cx).await;
10111
10112 let rust_language = Arc::new(
10113 Language::new(
10114 LanguageConfig {
10115 name: "Rust".into(),
10116 brackets: serde_json::from_value(json!([
10117 { "start": "{", "end": "}", "close": true, "newline": true },
10118 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
10119 ]))
10120 .unwrap(),
10121 autoclose_before: "})]>".into(),
10122 ..Default::default()
10123 },
10124 Some(tree_sitter_rust::LANGUAGE.into()),
10125 )
10126 .with_override_query("(string_literal) @string")
10127 .unwrap(),
10128 );
10129
10130 cx.language_registry().add(rust_language.clone());
10131 cx.update_buffer(|buffer, cx| {
10132 buffer.set_language(Some(rust_language), cx);
10133 });
10134
10135 cx.set_state(
10136 &r#"
10137 let x = ˇ
10138 "#
10139 .unindent(),
10140 );
10141
10142 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
10143 cx.update_editor(|editor, window, cx| {
10144 editor.handle_input("\"", window, cx);
10145 });
10146 cx.assert_editor_state(
10147 &r#"
10148 let x = "ˇ"
10149 "#
10150 .unindent(),
10151 );
10152
10153 // Inserting another quotation mark. The cursor moves across the existing
10154 // automatically-inserted quotation mark.
10155 cx.update_editor(|editor, window, cx| {
10156 editor.handle_input("\"", window, cx);
10157 });
10158 cx.assert_editor_state(
10159 &r#"
10160 let x = ""ˇ
10161 "#
10162 .unindent(),
10163 );
10164
10165 // Reset
10166 cx.set_state(
10167 &r#"
10168 let x = ˇ
10169 "#
10170 .unindent(),
10171 );
10172
10173 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
10174 cx.update_editor(|editor, window, cx| {
10175 editor.handle_input("\"", window, cx);
10176 editor.handle_input(" ", window, cx);
10177 editor.move_left(&Default::default(), window, cx);
10178 editor.handle_input("\\", window, cx);
10179 editor.handle_input("\"", window, cx);
10180 });
10181 cx.assert_editor_state(
10182 &r#"
10183 let x = "\"ˇ "
10184 "#
10185 .unindent(),
10186 );
10187
10188 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
10189 // mark. Nothing is inserted.
10190 cx.update_editor(|editor, window, cx| {
10191 editor.move_right(&Default::default(), window, cx);
10192 editor.handle_input("\"", window, cx);
10193 });
10194 cx.assert_editor_state(
10195 &r#"
10196 let x = "\" "ˇ
10197 "#
10198 .unindent(),
10199 );
10200}
10201
10202#[gpui::test]
10203async fn test_surround_with_pair(cx: &mut TestAppContext) {
10204 init_test(cx, |_| {});
10205
10206 let language = Arc::new(Language::new(
10207 LanguageConfig {
10208 brackets: BracketPairConfig {
10209 pairs: vec![
10210 BracketPair {
10211 start: "{".to_string(),
10212 end: "}".to_string(),
10213 close: true,
10214 surround: true,
10215 newline: true,
10216 },
10217 BracketPair {
10218 start: "/* ".to_string(),
10219 end: "*/".to_string(),
10220 close: true,
10221 surround: true,
10222 ..Default::default()
10223 },
10224 ],
10225 ..Default::default()
10226 },
10227 ..Default::default()
10228 },
10229 Some(tree_sitter_rust::LANGUAGE.into()),
10230 ));
10231
10232 let text = r#"
10233 a
10234 b
10235 c
10236 "#
10237 .unindent();
10238
10239 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10240 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10241 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10242 editor
10243 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10244 .await;
10245
10246 editor.update_in(cx, |editor, window, cx| {
10247 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10248 s.select_display_ranges([
10249 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10250 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10251 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
10252 ])
10253 });
10254
10255 editor.handle_input("{", window, cx);
10256 editor.handle_input("{", window, cx);
10257 editor.handle_input("{", window, cx);
10258 assert_eq!(
10259 editor.text(cx),
10260 "
10261 {{{a}}}
10262 {{{b}}}
10263 {{{c}}}
10264 "
10265 .unindent()
10266 );
10267 assert_eq!(
10268 editor.selections.display_ranges(cx),
10269 [
10270 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
10271 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
10272 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
10273 ]
10274 );
10275
10276 editor.undo(&Undo, window, cx);
10277 editor.undo(&Undo, window, cx);
10278 editor.undo(&Undo, window, cx);
10279 assert_eq!(
10280 editor.text(cx),
10281 "
10282 a
10283 b
10284 c
10285 "
10286 .unindent()
10287 );
10288 assert_eq!(
10289 editor.selections.display_ranges(cx),
10290 [
10291 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10292 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10293 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10294 ]
10295 );
10296
10297 // Ensure inserting the first character of a multi-byte bracket pair
10298 // doesn't surround the selections with the bracket.
10299 editor.handle_input("/", window, cx);
10300 assert_eq!(
10301 editor.text(cx),
10302 "
10303 /
10304 /
10305 /
10306 "
10307 .unindent()
10308 );
10309 assert_eq!(
10310 editor.selections.display_ranges(cx),
10311 [
10312 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10313 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10314 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10315 ]
10316 );
10317
10318 editor.undo(&Undo, window, cx);
10319 assert_eq!(
10320 editor.text(cx),
10321 "
10322 a
10323 b
10324 c
10325 "
10326 .unindent()
10327 );
10328 assert_eq!(
10329 editor.selections.display_ranges(cx),
10330 [
10331 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10332 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10333 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10334 ]
10335 );
10336
10337 // Ensure inserting the last character of a multi-byte bracket pair
10338 // doesn't surround the selections with the bracket.
10339 editor.handle_input("*", window, cx);
10340 assert_eq!(
10341 editor.text(cx),
10342 "
10343 *
10344 *
10345 *
10346 "
10347 .unindent()
10348 );
10349 assert_eq!(
10350 editor.selections.display_ranges(cx),
10351 [
10352 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10353 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10354 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10355 ]
10356 );
10357 });
10358}
10359
10360#[gpui::test]
10361async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
10362 init_test(cx, |_| {});
10363
10364 let language = Arc::new(Language::new(
10365 LanguageConfig {
10366 brackets: BracketPairConfig {
10367 pairs: vec![BracketPair {
10368 start: "{".to_string(),
10369 end: "}".to_string(),
10370 close: true,
10371 surround: true,
10372 newline: true,
10373 }],
10374 ..Default::default()
10375 },
10376 autoclose_before: "}".to_string(),
10377 ..Default::default()
10378 },
10379 Some(tree_sitter_rust::LANGUAGE.into()),
10380 ));
10381
10382 let text = r#"
10383 a
10384 b
10385 c
10386 "#
10387 .unindent();
10388
10389 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10390 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10391 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10392 editor
10393 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10394 .await;
10395
10396 editor.update_in(cx, |editor, window, cx| {
10397 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10398 s.select_ranges([
10399 Point::new(0, 1)..Point::new(0, 1),
10400 Point::new(1, 1)..Point::new(1, 1),
10401 Point::new(2, 1)..Point::new(2, 1),
10402 ])
10403 });
10404
10405 editor.handle_input("{", window, cx);
10406 editor.handle_input("{", window, cx);
10407 editor.handle_input("_", window, cx);
10408 assert_eq!(
10409 editor.text(cx),
10410 "
10411 a{{_}}
10412 b{{_}}
10413 c{{_}}
10414 "
10415 .unindent()
10416 );
10417 assert_eq!(
10418 editor.selections.ranges::<Point>(cx),
10419 [
10420 Point::new(0, 4)..Point::new(0, 4),
10421 Point::new(1, 4)..Point::new(1, 4),
10422 Point::new(2, 4)..Point::new(2, 4)
10423 ]
10424 );
10425
10426 editor.backspace(&Default::default(), window, cx);
10427 editor.backspace(&Default::default(), window, cx);
10428 assert_eq!(
10429 editor.text(cx),
10430 "
10431 a{}
10432 b{}
10433 c{}
10434 "
10435 .unindent()
10436 );
10437 assert_eq!(
10438 editor.selections.ranges::<Point>(cx),
10439 [
10440 Point::new(0, 2)..Point::new(0, 2),
10441 Point::new(1, 2)..Point::new(1, 2),
10442 Point::new(2, 2)..Point::new(2, 2)
10443 ]
10444 );
10445
10446 editor.delete_to_previous_word_start(&Default::default(), window, cx);
10447 assert_eq!(
10448 editor.text(cx),
10449 "
10450 a
10451 b
10452 c
10453 "
10454 .unindent()
10455 );
10456 assert_eq!(
10457 editor.selections.ranges::<Point>(cx),
10458 [
10459 Point::new(0, 1)..Point::new(0, 1),
10460 Point::new(1, 1)..Point::new(1, 1),
10461 Point::new(2, 1)..Point::new(2, 1)
10462 ]
10463 );
10464 });
10465}
10466
10467#[gpui::test]
10468async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
10469 init_test(cx, |settings| {
10470 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10471 });
10472
10473 let mut cx = EditorTestContext::new(cx).await;
10474
10475 let language = Arc::new(Language::new(
10476 LanguageConfig {
10477 brackets: BracketPairConfig {
10478 pairs: vec![
10479 BracketPair {
10480 start: "{".to_string(),
10481 end: "}".to_string(),
10482 close: true,
10483 surround: true,
10484 newline: true,
10485 },
10486 BracketPair {
10487 start: "(".to_string(),
10488 end: ")".to_string(),
10489 close: true,
10490 surround: true,
10491 newline: true,
10492 },
10493 BracketPair {
10494 start: "[".to_string(),
10495 end: "]".to_string(),
10496 close: false,
10497 surround: true,
10498 newline: true,
10499 },
10500 ],
10501 ..Default::default()
10502 },
10503 autoclose_before: "})]".to_string(),
10504 ..Default::default()
10505 },
10506 Some(tree_sitter_rust::LANGUAGE.into()),
10507 ));
10508
10509 cx.language_registry().add(language.clone());
10510 cx.update_buffer(|buffer, cx| {
10511 buffer.set_language(Some(language), cx);
10512 });
10513
10514 cx.set_state(
10515 &"
10516 {(ˇ)}
10517 [[ˇ]]
10518 {(ˇ)}
10519 "
10520 .unindent(),
10521 );
10522
10523 cx.update_editor(|editor, window, cx| {
10524 editor.backspace(&Default::default(), window, cx);
10525 editor.backspace(&Default::default(), window, cx);
10526 });
10527
10528 cx.assert_editor_state(
10529 &"
10530 ˇ
10531 ˇ]]
10532 ˇ
10533 "
10534 .unindent(),
10535 );
10536
10537 cx.update_editor(|editor, window, cx| {
10538 editor.handle_input("{", window, cx);
10539 editor.handle_input("{", window, cx);
10540 editor.move_right(&MoveRight, window, cx);
10541 editor.move_right(&MoveRight, window, cx);
10542 editor.move_left(&MoveLeft, window, cx);
10543 editor.move_left(&MoveLeft, window, cx);
10544 editor.backspace(&Default::default(), window, cx);
10545 });
10546
10547 cx.assert_editor_state(
10548 &"
10549 {ˇ}
10550 {ˇ}]]
10551 {ˇ}
10552 "
10553 .unindent(),
10554 );
10555
10556 cx.update_editor(|editor, window, cx| {
10557 editor.backspace(&Default::default(), window, cx);
10558 });
10559
10560 cx.assert_editor_state(
10561 &"
10562 ˇ
10563 ˇ]]
10564 ˇ
10565 "
10566 .unindent(),
10567 );
10568}
10569
10570#[gpui::test]
10571async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
10572 init_test(cx, |_| {});
10573
10574 let language = Arc::new(Language::new(
10575 LanguageConfig::default(),
10576 Some(tree_sitter_rust::LANGUAGE.into()),
10577 ));
10578
10579 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
10580 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10581 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10582 editor
10583 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10584 .await;
10585
10586 editor.update_in(cx, |editor, window, cx| {
10587 editor.set_auto_replace_emoji_shortcode(true);
10588
10589 editor.handle_input("Hello ", window, cx);
10590 editor.handle_input(":wave", window, cx);
10591 assert_eq!(editor.text(cx), "Hello :wave".unindent());
10592
10593 editor.handle_input(":", window, cx);
10594 assert_eq!(editor.text(cx), "Hello 👋".unindent());
10595
10596 editor.handle_input(" :smile", window, cx);
10597 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
10598
10599 editor.handle_input(":", window, cx);
10600 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
10601
10602 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
10603 editor.handle_input(":wave", window, cx);
10604 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
10605
10606 editor.handle_input(":", window, cx);
10607 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
10608
10609 editor.handle_input(":1", window, cx);
10610 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
10611
10612 editor.handle_input(":", window, cx);
10613 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
10614
10615 // Ensure shortcode does not get replaced when it is part of a word
10616 editor.handle_input(" Test:wave", window, cx);
10617 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
10618
10619 editor.handle_input(":", window, cx);
10620 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
10621
10622 editor.set_auto_replace_emoji_shortcode(false);
10623
10624 // Ensure shortcode does not get replaced when auto replace is off
10625 editor.handle_input(" :wave", window, cx);
10626 assert_eq!(
10627 editor.text(cx),
10628 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
10629 );
10630
10631 editor.handle_input(":", window, cx);
10632 assert_eq!(
10633 editor.text(cx),
10634 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
10635 );
10636 });
10637}
10638
10639#[gpui::test]
10640async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
10641 init_test(cx, |_| {});
10642
10643 let (text, insertion_ranges) = marked_text_ranges(
10644 indoc! {"
10645 ˇ
10646 "},
10647 false,
10648 );
10649
10650 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
10651 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10652
10653 _ = editor.update_in(cx, |editor, window, cx| {
10654 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
10655
10656 editor
10657 .insert_snippet(&insertion_ranges, snippet, window, cx)
10658 .unwrap();
10659
10660 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
10661 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
10662 assert_eq!(editor.text(cx), expected_text);
10663 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
10664 }
10665
10666 assert(
10667 editor,
10668 cx,
10669 indoc! {"
10670 type «» =•
10671 "},
10672 );
10673
10674 assert!(editor.context_menu_visible(), "There should be a matches");
10675 });
10676}
10677
10678#[gpui::test]
10679async fn test_snippets(cx: &mut TestAppContext) {
10680 init_test(cx, |_| {});
10681
10682 let mut cx = EditorTestContext::new(cx).await;
10683
10684 cx.set_state(indoc! {"
10685 a.ˇ b
10686 a.ˇ b
10687 a.ˇ b
10688 "});
10689
10690 cx.update_editor(|editor, window, cx| {
10691 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
10692 let insertion_ranges = editor
10693 .selections
10694 .all(cx)
10695 .iter()
10696 .map(|s| s.range())
10697 .collect::<Vec<_>>();
10698 editor
10699 .insert_snippet(&insertion_ranges, snippet, window, cx)
10700 .unwrap();
10701 });
10702
10703 cx.assert_editor_state(indoc! {"
10704 a.f(«oneˇ», two, «threeˇ») b
10705 a.f(«oneˇ», two, «threeˇ») b
10706 a.f(«oneˇ», two, «threeˇ») b
10707 "});
10708
10709 // Can't move earlier than the first tab stop
10710 cx.update_editor(|editor, window, cx| {
10711 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
10712 });
10713 cx.assert_editor_state(indoc! {"
10714 a.f(«oneˇ», two, «threeˇ») b
10715 a.f(«oneˇ», two, «threeˇ») b
10716 a.f(«oneˇ», two, «threeˇ») b
10717 "});
10718
10719 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10720 cx.assert_editor_state(indoc! {"
10721 a.f(one, «twoˇ», three) b
10722 a.f(one, «twoˇ», three) b
10723 a.f(one, «twoˇ», three) b
10724 "});
10725
10726 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
10727 cx.assert_editor_state(indoc! {"
10728 a.f(«oneˇ», two, «threeˇ») b
10729 a.f(«oneˇ», two, «threeˇ») b
10730 a.f(«oneˇ», two, «threeˇ») b
10731 "});
10732
10733 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10734 cx.assert_editor_state(indoc! {"
10735 a.f(one, «twoˇ», three) b
10736 a.f(one, «twoˇ», three) b
10737 a.f(one, «twoˇ», three) b
10738 "});
10739 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10740 cx.assert_editor_state(indoc! {"
10741 a.f(one, two, three)ˇ b
10742 a.f(one, two, three)ˇ b
10743 a.f(one, two, three)ˇ b
10744 "});
10745
10746 // As soon as the last tab stop is reached, snippet state is gone
10747 cx.update_editor(|editor, window, cx| {
10748 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
10749 });
10750 cx.assert_editor_state(indoc! {"
10751 a.f(one, two, three)ˇ b
10752 a.f(one, two, three)ˇ b
10753 a.f(one, two, three)ˇ b
10754 "});
10755}
10756
10757#[gpui::test]
10758async fn test_snippet_indentation(cx: &mut TestAppContext) {
10759 init_test(cx, |_| {});
10760
10761 let mut cx = EditorTestContext::new(cx).await;
10762
10763 cx.update_editor(|editor, window, cx| {
10764 let snippet = Snippet::parse(indoc! {"
10765 /*
10766 * Multiline comment with leading indentation
10767 *
10768 * $1
10769 */
10770 $0"})
10771 .unwrap();
10772 let insertion_ranges = editor
10773 .selections
10774 .all(cx)
10775 .iter()
10776 .map(|s| s.range())
10777 .collect::<Vec<_>>();
10778 editor
10779 .insert_snippet(&insertion_ranges, snippet, window, cx)
10780 .unwrap();
10781 });
10782
10783 cx.assert_editor_state(indoc! {"
10784 /*
10785 * Multiline comment with leading indentation
10786 *
10787 * ˇ
10788 */
10789 "});
10790
10791 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10792 cx.assert_editor_state(indoc! {"
10793 /*
10794 * Multiline comment with leading indentation
10795 *
10796 *•
10797 */
10798 ˇ"});
10799}
10800
10801#[gpui::test]
10802async fn test_document_format_during_save(cx: &mut TestAppContext) {
10803 init_test(cx, |_| {});
10804
10805 let fs = FakeFs::new(cx.executor());
10806 fs.insert_file(path!("/file.rs"), Default::default()).await;
10807
10808 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
10809
10810 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10811 language_registry.add(rust_lang());
10812 let mut fake_servers = language_registry.register_fake_lsp(
10813 "Rust",
10814 FakeLspAdapter {
10815 capabilities: lsp::ServerCapabilities {
10816 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10817 ..Default::default()
10818 },
10819 ..Default::default()
10820 },
10821 );
10822
10823 let buffer = project
10824 .update(cx, |project, cx| {
10825 project.open_local_buffer(path!("/file.rs"), cx)
10826 })
10827 .await
10828 .unwrap();
10829
10830 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10831 let (editor, cx) = cx.add_window_view(|window, cx| {
10832 build_editor_with_project(project.clone(), buffer, window, cx)
10833 });
10834 editor.update_in(cx, |editor, window, cx| {
10835 editor.set_text("one\ntwo\nthree\n", window, cx)
10836 });
10837 assert!(cx.read(|cx| editor.is_dirty(cx)));
10838
10839 cx.executor().start_waiting();
10840 let fake_server = fake_servers.next().await.unwrap();
10841
10842 {
10843 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10844 move |params, _| async move {
10845 assert_eq!(
10846 params.text_document.uri,
10847 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
10848 );
10849 assert_eq!(params.options.tab_size, 4);
10850 Ok(Some(vec![lsp::TextEdit::new(
10851 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10852 ", ".to_string(),
10853 )]))
10854 },
10855 );
10856 let save = editor
10857 .update_in(cx, |editor, window, cx| {
10858 editor.save(
10859 SaveOptions {
10860 format: true,
10861 autosave: false,
10862 },
10863 project.clone(),
10864 window,
10865 cx,
10866 )
10867 })
10868 .unwrap();
10869 cx.executor().start_waiting();
10870 save.await;
10871
10872 assert_eq!(
10873 editor.update(cx, |editor, cx| editor.text(cx)),
10874 "one, two\nthree\n"
10875 );
10876 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10877 }
10878
10879 {
10880 editor.update_in(cx, |editor, window, cx| {
10881 editor.set_text("one\ntwo\nthree\n", window, cx)
10882 });
10883 assert!(cx.read(|cx| editor.is_dirty(cx)));
10884
10885 // Ensure we can still save even if formatting hangs.
10886 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10887 move |params, _| async move {
10888 assert_eq!(
10889 params.text_document.uri,
10890 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
10891 );
10892 futures::future::pending::<()>().await;
10893 unreachable!()
10894 },
10895 );
10896 let save = editor
10897 .update_in(cx, |editor, window, cx| {
10898 editor.save(
10899 SaveOptions {
10900 format: true,
10901 autosave: false,
10902 },
10903 project.clone(),
10904 window,
10905 cx,
10906 )
10907 })
10908 .unwrap();
10909 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
10910 cx.executor().start_waiting();
10911 save.await;
10912 assert_eq!(
10913 editor.update(cx, |editor, cx| editor.text(cx)),
10914 "one\ntwo\nthree\n"
10915 );
10916 }
10917
10918 // Set rust language override and assert overridden tabsize is sent to language server
10919 update_test_language_settings(cx, |settings| {
10920 settings.languages.0.insert(
10921 "Rust".into(),
10922 LanguageSettingsContent {
10923 tab_size: NonZeroU32::new(8),
10924 ..Default::default()
10925 },
10926 );
10927 });
10928
10929 {
10930 editor.update_in(cx, |editor, window, cx| {
10931 editor.set_text("somehting_new\n", window, cx)
10932 });
10933 assert!(cx.read(|cx| editor.is_dirty(cx)));
10934 let _formatting_request_signal = fake_server
10935 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
10936 assert_eq!(
10937 params.text_document.uri,
10938 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
10939 );
10940 assert_eq!(params.options.tab_size, 8);
10941 Ok(Some(vec![]))
10942 });
10943 let save = editor
10944 .update_in(cx, |editor, window, cx| {
10945 editor.save(
10946 SaveOptions {
10947 format: true,
10948 autosave: false,
10949 },
10950 project.clone(),
10951 window,
10952 cx,
10953 )
10954 })
10955 .unwrap();
10956 cx.executor().start_waiting();
10957 save.await;
10958 }
10959}
10960
10961#[gpui::test]
10962async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
10963 init_test(cx, |settings| {
10964 settings.defaults.ensure_final_newline_on_save = Some(false);
10965 });
10966
10967 let fs = FakeFs::new(cx.executor());
10968 fs.insert_file(path!("/file.txt"), "foo".into()).await;
10969
10970 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
10971
10972 let buffer = project
10973 .update(cx, |project, cx| {
10974 project.open_local_buffer(path!("/file.txt"), cx)
10975 })
10976 .await
10977 .unwrap();
10978
10979 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10980 let (editor, cx) = cx.add_window_view(|window, cx| {
10981 build_editor_with_project(project.clone(), buffer, window, cx)
10982 });
10983 editor.update_in(cx, |editor, window, cx| {
10984 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
10985 s.select_ranges([0..0])
10986 });
10987 });
10988 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10989
10990 editor.update_in(cx, |editor, window, cx| {
10991 editor.handle_input("\n", window, cx)
10992 });
10993 cx.run_until_parked();
10994 save(&editor, &project, cx).await;
10995 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
10996
10997 editor.update_in(cx, |editor, window, cx| {
10998 editor.undo(&Default::default(), window, cx);
10999 });
11000 save(&editor, &project, cx).await;
11001 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11002
11003 editor.update_in(cx, |editor, window, cx| {
11004 editor.redo(&Default::default(), window, cx);
11005 });
11006 cx.run_until_parked();
11007 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11008
11009 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
11010 let save = editor
11011 .update_in(cx, |editor, window, cx| {
11012 editor.save(
11013 SaveOptions {
11014 format: true,
11015 autosave: false,
11016 },
11017 project.clone(),
11018 window,
11019 cx,
11020 )
11021 })
11022 .unwrap();
11023 cx.executor().start_waiting();
11024 save.await;
11025 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11026 }
11027}
11028
11029#[gpui::test]
11030async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
11031 init_test(cx, |_| {});
11032
11033 let cols = 4;
11034 let rows = 10;
11035 let sample_text_1 = sample_text(rows, cols, 'a');
11036 assert_eq!(
11037 sample_text_1,
11038 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
11039 );
11040 let sample_text_2 = sample_text(rows, cols, 'l');
11041 assert_eq!(
11042 sample_text_2,
11043 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
11044 );
11045 let sample_text_3 = sample_text(rows, cols, 'v');
11046 assert_eq!(
11047 sample_text_3,
11048 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
11049 );
11050
11051 let fs = FakeFs::new(cx.executor());
11052 fs.insert_tree(
11053 path!("/a"),
11054 json!({
11055 "main.rs": sample_text_1,
11056 "other.rs": sample_text_2,
11057 "lib.rs": sample_text_3,
11058 }),
11059 )
11060 .await;
11061
11062 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11063 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11064 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11065
11066 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11067 language_registry.add(rust_lang());
11068 let mut fake_servers = language_registry.register_fake_lsp(
11069 "Rust",
11070 FakeLspAdapter {
11071 capabilities: lsp::ServerCapabilities {
11072 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11073 ..Default::default()
11074 },
11075 ..Default::default()
11076 },
11077 );
11078
11079 let worktree = project.update(cx, |project, cx| {
11080 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
11081 assert_eq!(worktrees.len(), 1);
11082 worktrees.pop().unwrap()
11083 });
11084 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11085
11086 let buffer_1 = project
11087 .update(cx, |project, cx| {
11088 project.open_buffer((worktree_id, "main.rs"), cx)
11089 })
11090 .await
11091 .unwrap();
11092 let buffer_2 = project
11093 .update(cx, |project, cx| {
11094 project.open_buffer((worktree_id, "other.rs"), cx)
11095 })
11096 .await
11097 .unwrap();
11098 let buffer_3 = project
11099 .update(cx, |project, cx| {
11100 project.open_buffer((worktree_id, "lib.rs"), cx)
11101 })
11102 .await
11103 .unwrap();
11104
11105 let multi_buffer = cx.new(|cx| {
11106 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11107 multi_buffer.push_excerpts(
11108 buffer_1.clone(),
11109 [
11110 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11111 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11112 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11113 ],
11114 cx,
11115 );
11116 multi_buffer.push_excerpts(
11117 buffer_2.clone(),
11118 [
11119 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11120 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11121 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11122 ],
11123 cx,
11124 );
11125 multi_buffer.push_excerpts(
11126 buffer_3.clone(),
11127 [
11128 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11129 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11130 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11131 ],
11132 cx,
11133 );
11134 multi_buffer
11135 });
11136 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
11137 Editor::new(
11138 EditorMode::full(),
11139 multi_buffer,
11140 Some(project.clone()),
11141 window,
11142 cx,
11143 )
11144 });
11145
11146 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11147 editor.change_selections(
11148 SelectionEffects::scroll(Autoscroll::Next),
11149 window,
11150 cx,
11151 |s| s.select_ranges(Some(1..2)),
11152 );
11153 editor.insert("|one|two|three|", window, cx);
11154 });
11155 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11156 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11157 editor.change_selections(
11158 SelectionEffects::scroll(Autoscroll::Next),
11159 window,
11160 cx,
11161 |s| s.select_ranges(Some(60..70)),
11162 );
11163 editor.insert("|four|five|six|", window, cx);
11164 });
11165 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11166
11167 // First two buffers should be edited, but not the third one.
11168 assert_eq!(
11169 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11170 "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}",
11171 );
11172 buffer_1.update(cx, |buffer, _| {
11173 assert!(buffer.is_dirty());
11174 assert_eq!(
11175 buffer.text(),
11176 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
11177 )
11178 });
11179 buffer_2.update(cx, |buffer, _| {
11180 assert!(buffer.is_dirty());
11181 assert_eq!(
11182 buffer.text(),
11183 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
11184 )
11185 });
11186 buffer_3.update(cx, |buffer, _| {
11187 assert!(!buffer.is_dirty());
11188 assert_eq!(buffer.text(), sample_text_3,)
11189 });
11190 cx.executor().run_until_parked();
11191
11192 cx.executor().start_waiting();
11193 let save = multi_buffer_editor
11194 .update_in(cx, |editor, window, cx| {
11195 editor.save(
11196 SaveOptions {
11197 format: true,
11198 autosave: false,
11199 },
11200 project.clone(),
11201 window,
11202 cx,
11203 )
11204 })
11205 .unwrap();
11206
11207 let fake_server = fake_servers.next().await.unwrap();
11208 fake_server
11209 .server
11210 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
11211 Ok(Some(vec![lsp::TextEdit::new(
11212 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11213 format!("[{} formatted]", params.text_document.uri),
11214 )]))
11215 })
11216 .detach();
11217 save.await;
11218
11219 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
11220 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
11221 assert_eq!(
11222 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11223 uri!(
11224 "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}"
11225 ),
11226 );
11227 buffer_1.update(cx, |buffer, _| {
11228 assert!(!buffer.is_dirty());
11229 assert_eq!(
11230 buffer.text(),
11231 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
11232 )
11233 });
11234 buffer_2.update(cx, |buffer, _| {
11235 assert!(!buffer.is_dirty());
11236 assert_eq!(
11237 buffer.text(),
11238 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
11239 )
11240 });
11241 buffer_3.update(cx, |buffer, _| {
11242 assert!(!buffer.is_dirty());
11243 assert_eq!(buffer.text(), sample_text_3,)
11244 });
11245}
11246
11247#[gpui::test]
11248async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
11249 init_test(cx, |_| {});
11250
11251 let fs = FakeFs::new(cx.executor());
11252 fs.insert_tree(
11253 path!("/dir"),
11254 json!({
11255 "file1.rs": "fn main() { println!(\"hello\"); }",
11256 "file2.rs": "fn test() { println!(\"test\"); }",
11257 "file3.rs": "fn other() { println!(\"other\"); }\n",
11258 }),
11259 )
11260 .await;
11261
11262 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
11263 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11264 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11265
11266 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11267 language_registry.add(rust_lang());
11268
11269 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
11270 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11271
11272 // Open three buffers
11273 let buffer_1 = project
11274 .update(cx, |project, cx| {
11275 project.open_buffer((worktree_id, "file1.rs"), cx)
11276 })
11277 .await
11278 .unwrap();
11279 let buffer_2 = project
11280 .update(cx, |project, cx| {
11281 project.open_buffer((worktree_id, "file2.rs"), cx)
11282 })
11283 .await
11284 .unwrap();
11285 let buffer_3 = project
11286 .update(cx, |project, cx| {
11287 project.open_buffer((worktree_id, "file3.rs"), cx)
11288 })
11289 .await
11290 .unwrap();
11291
11292 // Create a multi-buffer with all three buffers
11293 let multi_buffer = cx.new(|cx| {
11294 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11295 multi_buffer.push_excerpts(
11296 buffer_1.clone(),
11297 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11298 cx,
11299 );
11300 multi_buffer.push_excerpts(
11301 buffer_2.clone(),
11302 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11303 cx,
11304 );
11305 multi_buffer.push_excerpts(
11306 buffer_3.clone(),
11307 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11308 cx,
11309 );
11310 multi_buffer
11311 });
11312
11313 let editor = cx.new_window_entity(|window, cx| {
11314 Editor::new(
11315 EditorMode::full(),
11316 multi_buffer,
11317 Some(project.clone()),
11318 window,
11319 cx,
11320 )
11321 });
11322
11323 // Edit only the first buffer
11324 editor.update_in(cx, |editor, window, cx| {
11325 editor.change_selections(
11326 SelectionEffects::scroll(Autoscroll::Next),
11327 window,
11328 cx,
11329 |s| s.select_ranges(Some(10..10)),
11330 );
11331 editor.insert("// edited", window, cx);
11332 });
11333
11334 // Verify that only buffer 1 is dirty
11335 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
11336 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11337 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11338
11339 // Get write counts after file creation (files were created with initial content)
11340 // We expect each file to have been written once during creation
11341 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
11342 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
11343 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
11344
11345 // Perform autosave
11346 let save_task = editor.update_in(cx, |editor, window, cx| {
11347 editor.save(
11348 SaveOptions {
11349 format: true,
11350 autosave: true,
11351 },
11352 project.clone(),
11353 window,
11354 cx,
11355 )
11356 });
11357 save_task.await.unwrap();
11358
11359 // Only the dirty buffer should have been saved
11360 assert_eq!(
11361 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11362 1,
11363 "Buffer 1 was dirty, so it should have been written once during autosave"
11364 );
11365 assert_eq!(
11366 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11367 0,
11368 "Buffer 2 was clean, so it should not have been written during autosave"
11369 );
11370 assert_eq!(
11371 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11372 0,
11373 "Buffer 3 was clean, so it should not have been written during autosave"
11374 );
11375
11376 // Verify buffer states after autosave
11377 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11378 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11379 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11380
11381 // Now perform a manual save (format = true)
11382 let save_task = editor.update_in(cx, |editor, window, cx| {
11383 editor.save(
11384 SaveOptions {
11385 format: true,
11386 autosave: false,
11387 },
11388 project.clone(),
11389 window,
11390 cx,
11391 )
11392 });
11393 save_task.await.unwrap();
11394
11395 // During manual save, clean buffers don't get written to disk
11396 // They just get did_save called for language server notifications
11397 assert_eq!(
11398 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11399 1,
11400 "Buffer 1 should only have been written once total (during autosave, not manual save)"
11401 );
11402 assert_eq!(
11403 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11404 0,
11405 "Buffer 2 should not have been written at all"
11406 );
11407 assert_eq!(
11408 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11409 0,
11410 "Buffer 3 should not have been written at all"
11411 );
11412}
11413
11414async fn setup_range_format_test(
11415 cx: &mut TestAppContext,
11416) -> (
11417 Entity<Project>,
11418 Entity<Editor>,
11419 &mut gpui::VisualTestContext,
11420 lsp::FakeLanguageServer,
11421) {
11422 init_test(cx, |_| {});
11423
11424 let fs = FakeFs::new(cx.executor());
11425 fs.insert_file(path!("/file.rs"), Default::default()).await;
11426
11427 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11428
11429 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11430 language_registry.add(rust_lang());
11431 let mut fake_servers = language_registry.register_fake_lsp(
11432 "Rust",
11433 FakeLspAdapter {
11434 capabilities: lsp::ServerCapabilities {
11435 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
11436 ..lsp::ServerCapabilities::default()
11437 },
11438 ..FakeLspAdapter::default()
11439 },
11440 );
11441
11442 let buffer = project
11443 .update(cx, |project, cx| {
11444 project.open_local_buffer(path!("/file.rs"), cx)
11445 })
11446 .await
11447 .unwrap();
11448
11449 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11450 let (editor, cx) = cx.add_window_view(|window, cx| {
11451 build_editor_with_project(project.clone(), buffer, window, cx)
11452 });
11453
11454 cx.executor().start_waiting();
11455 let fake_server = fake_servers.next().await.unwrap();
11456
11457 (project, editor, cx, fake_server)
11458}
11459
11460#[gpui::test]
11461async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
11462 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11463
11464 editor.update_in(cx, |editor, window, cx| {
11465 editor.set_text("one\ntwo\nthree\n", window, cx)
11466 });
11467 assert!(cx.read(|cx| editor.is_dirty(cx)));
11468
11469 let save = editor
11470 .update_in(cx, |editor, window, cx| {
11471 editor.save(
11472 SaveOptions {
11473 format: true,
11474 autosave: false,
11475 },
11476 project.clone(),
11477 window,
11478 cx,
11479 )
11480 })
11481 .unwrap();
11482 fake_server
11483 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
11484 assert_eq!(
11485 params.text_document.uri,
11486 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11487 );
11488 assert_eq!(params.options.tab_size, 4);
11489 Ok(Some(vec![lsp::TextEdit::new(
11490 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11491 ", ".to_string(),
11492 )]))
11493 })
11494 .next()
11495 .await;
11496 cx.executor().start_waiting();
11497 save.await;
11498 assert_eq!(
11499 editor.update(cx, |editor, cx| editor.text(cx)),
11500 "one, two\nthree\n"
11501 );
11502 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11503}
11504
11505#[gpui::test]
11506async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
11507 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11508
11509 editor.update_in(cx, |editor, window, cx| {
11510 editor.set_text("one\ntwo\nthree\n", window, cx)
11511 });
11512 assert!(cx.read(|cx| editor.is_dirty(cx)));
11513
11514 // Test that save still works when formatting hangs
11515 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
11516 move |params, _| async move {
11517 assert_eq!(
11518 params.text_document.uri,
11519 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11520 );
11521 futures::future::pending::<()>().await;
11522 unreachable!()
11523 },
11524 );
11525 let save = editor
11526 .update_in(cx, |editor, window, cx| {
11527 editor.save(
11528 SaveOptions {
11529 format: true,
11530 autosave: false,
11531 },
11532 project.clone(),
11533 window,
11534 cx,
11535 )
11536 })
11537 .unwrap();
11538 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11539 cx.executor().start_waiting();
11540 save.await;
11541 assert_eq!(
11542 editor.update(cx, |editor, cx| editor.text(cx)),
11543 "one\ntwo\nthree\n"
11544 );
11545 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11546}
11547
11548#[gpui::test]
11549async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
11550 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11551
11552 // Buffer starts clean, no formatting should be requested
11553 let save = editor
11554 .update_in(cx, |editor, window, cx| {
11555 editor.save(
11556 SaveOptions {
11557 format: false,
11558 autosave: false,
11559 },
11560 project.clone(),
11561 window,
11562 cx,
11563 )
11564 })
11565 .unwrap();
11566 let _pending_format_request = fake_server
11567 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
11568 panic!("Should not be invoked");
11569 })
11570 .next();
11571 cx.executor().start_waiting();
11572 save.await;
11573 cx.run_until_parked();
11574}
11575
11576#[gpui::test]
11577async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
11578 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11579
11580 // Set Rust language override and assert overridden tabsize is sent to language server
11581 update_test_language_settings(cx, |settings| {
11582 settings.languages.0.insert(
11583 "Rust".into(),
11584 LanguageSettingsContent {
11585 tab_size: NonZeroU32::new(8),
11586 ..Default::default()
11587 },
11588 );
11589 });
11590
11591 editor.update_in(cx, |editor, window, cx| {
11592 editor.set_text("something_new\n", window, cx)
11593 });
11594 assert!(cx.read(|cx| editor.is_dirty(cx)));
11595 let save = editor
11596 .update_in(cx, |editor, window, cx| {
11597 editor.save(
11598 SaveOptions {
11599 format: true,
11600 autosave: false,
11601 },
11602 project.clone(),
11603 window,
11604 cx,
11605 )
11606 })
11607 .unwrap();
11608 fake_server
11609 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
11610 assert_eq!(
11611 params.text_document.uri,
11612 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11613 );
11614 assert_eq!(params.options.tab_size, 8);
11615 Ok(Some(Vec::new()))
11616 })
11617 .next()
11618 .await;
11619 save.await;
11620}
11621
11622#[gpui::test]
11623async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
11624 init_test(cx, |settings| {
11625 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
11626 Formatter::LanguageServer { name: None },
11627 )))
11628 });
11629
11630 let fs = FakeFs::new(cx.executor());
11631 fs.insert_file(path!("/file.rs"), Default::default()).await;
11632
11633 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11634
11635 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11636 language_registry.add(Arc::new(Language::new(
11637 LanguageConfig {
11638 name: "Rust".into(),
11639 matcher: LanguageMatcher {
11640 path_suffixes: vec!["rs".to_string()],
11641 ..Default::default()
11642 },
11643 ..LanguageConfig::default()
11644 },
11645 Some(tree_sitter_rust::LANGUAGE.into()),
11646 )));
11647 update_test_language_settings(cx, |settings| {
11648 // Enable Prettier formatting for the same buffer, and ensure
11649 // LSP is called instead of Prettier.
11650 settings.defaults.prettier = Some(PrettierSettings {
11651 allowed: true,
11652 ..PrettierSettings::default()
11653 });
11654 });
11655 let mut fake_servers = language_registry.register_fake_lsp(
11656 "Rust",
11657 FakeLspAdapter {
11658 capabilities: lsp::ServerCapabilities {
11659 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11660 ..Default::default()
11661 },
11662 ..Default::default()
11663 },
11664 );
11665
11666 let buffer = project
11667 .update(cx, |project, cx| {
11668 project.open_local_buffer(path!("/file.rs"), cx)
11669 })
11670 .await
11671 .unwrap();
11672
11673 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11674 let (editor, cx) = cx.add_window_view(|window, cx| {
11675 build_editor_with_project(project.clone(), buffer, window, cx)
11676 });
11677 editor.update_in(cx, |editor, window, cx| {
11678 editor.set_text("one\ntwo\nthree\n", window, cx)
11679 });
11680
11681 cx.executor().start_waiting();
11682 let fake_server = fake_servers.next().await.unwrap();
11683
11684 let format = editor
11685 .update_in(cx, |editor, window, cx| {
11686 editor.perform_format(
11687 project.clone(),
11688 FormatTrigger::Manual,
11689 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
11690 window,
11691 cx,
11692 )
11693 })
11694 .unwrap();
11695 fake_server
11696 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
11697 assert_eq!(
11698 params.text_document.uri,
11699 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11700 );
11701 assert_eq!(params.options.tab_size, 4);
11702 Ok(Some(vec![lsp::TextEdit::new(
11703 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11704 ", ".to_string(),
11705 )]))
11706 })
11707 .next()
11708 .await;
11709 cx.executor().start_waiting();
11710 format.await;
11711 assert_eq!(
11712 editor.update(cx, |editor, cx| editor.text(cx)),
11713 "one, two\nthree\n"
11714 );
11715
11716 editor.update_in(cx, |editor, window, cx| {
11717 editor.set_text("one\ntwo\nthree\n", window, cx)
11718 });
11719 // Ensure we don't lock if formatting hangs.
11720 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11721 move |params, _| async move {
11722 assert_eq!(
11723 params.text_document.uri,
11724 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11725 );
11726 futures::future::pending::<()>().await;
11727 unreachable!()
11728 },
11729 );
11730 let format = editor
11731 .update_in(cx, |editor, window, cx| {
11732 editor.perform_format(
11733 project,
11734 FormatTrigger::Manual,
11735 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
11736 window,
11737 cx,
11738 )
11739 })
11740 .unwrap();
11741 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11742 cx.executor().start_waiting();
11743 format.await;
11744 assert_eq!(
11745 editor.update(cx, |editor, cx| editor.text(cx)),
11746 "one\ntwo\nthree\n"
11747 );
11748}
11749
11750#[gpui::test]
11751async fn test_multiple_formatters(cx: &mut TestAppContext) {
11752 init_test(cx, |settings| {
11753 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
11754 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
11755 Formatter::LanguageServer { name: None },
11756 Formatter::CodeActions(
11757 [
11758 ("code-action-1".into(), true),
11759 ("code-action-2".into(), true),
11760 ]
11761 .into_iter()
11762 .collect(),
11763 ),
11764 ])))
11765 });
11766
11767 let fs = FakeFs::new(cx.executor());
11768 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
11769 .await;
11770
11771 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11772 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11773 language_registry.add(rust_lang());
11774
11775 let mut fake_servers = language_registry.register_fake_lsp(
11776 "Rust",
11777 FakeLspAdapter {
11778 capabilities: lsp::ServerCapabilities {
11779 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11780 execute_command_provider: Some(lsp::ExecuteCommandOptions {
11781 commands: vec!["the-command-for-code-action-1".into()],
11782 ..Default::default()
11783 }),
11784 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
11785 ..Default::default()
11786 },
11787 ..Default::default()
11788 },
11789 );
11790
11791 let buffer = project
11792 .update(cx, |project, cx| {
11793 project.open_local_buffer(path!("/file.rs"), cx)
11794 })
11795 .await
11796 .unwrap();
11797
11798 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11799 let (editor, cx) = cx.add_window_view(|window, cx| {
11800 build_editor_with_project(project.clone(), buffer, window, cx)
11801 });
11802
11803 cx.executor().start_waiting();
11804
11805 let fake_server = fake_servers.next().await.unwrap();
11806 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11807 move |_params, _| async move {
11808 Ok(Some(vec![lsp::TextEdit::new(
11809 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
11810 "applied-formatting\n".to_string(),
11811 )]))
11812 },
11813 );
11814 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
11815 move |params, _| async move {
11816 assert_eq!(
11817 params.context.only,
11818 Some(vec!["code-action-1".into(), "code-action-2".into()])
11819 );
11820 let uri = lsp::Uri::from_file_path(path!("/file.rs")).unwrap();
11821 Ok(Some(vec![
11822 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
11823 kind: Some("code-action-1".into()),
11824 edit: Some(lsp::WorkspaceEdit::new(
11825 [(
11826 uri.clone(),
11827 vec![lsp::TextEdit::new(
11828 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
11829 "applied-code-action-1-edit\n".to_string(),
11830 )],
11831 )]
11832 .into_iter()
11833 .collect(),
11834 )),
11835 command: Some(lsp::Command {
11836 command: "the-command-for-code-action-1".into(),
11837 ..Default::default()
11838 }),
11839 ..Default::default()
11840 }),
11841 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
11842 kind: Some("code-action-2".into()),
11843 edit: Some(lsp::WorkspaceEdit::new(
11844 [(
11845 uri,
11846 vec![lsp::TextEdit::new(
11847 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
11848 "applied-code-action-2-edit\n".to_string(),
11849 )],
11850 )]
11851 .into_iter()
11852 .collect(),
11853 )),
11854 ..Default::default()
11855 }),
11856 ]))
11857 },
11858 );
11859
11860 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
11861 move |params, _| async move { Ok(params) }
11862 });
11863
11864 let command_lock = Arc::new(futures::lock::Mutex::new(()));
11865 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
11866 let fake = fake_server.clone();
11867 let lock = command_lock.clone();
11868 move |params, _| {
11869 assert_eq!(params.command, "the-command-for-code-action-1");
11870 let fake = fake.clone();
11871 let lock = lock.clone();
11872 async move {
11873 lock.lock().await;
11874 fake.server
11875 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
11876 label: None,
11877 edit: lsp::WorkspaceEdit {
11878 changes: Some(
11879 [(
11880 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
11881 vec![lsp::TextEdit {
11882 range: lsp::Range::new(
11883 lsp::Position::new(0, 0),
11884 lsp::Position::new(0, 0),
11885 ),
11886 new_text: "applied-code-action-1-command\n".into(),
11887 }],
11888 )]
11889 .into_iter()
11890 .collect(),
11891 ),
11892 ..Default::default()
11893 },
11894 })
11895 .await
11896 .into_response()
11897 .unwrap();
11898 Ok(Some(json!(null)))
11899 }
11900 }
11901 });
11902
11903 cx.executor().start_waiting();
11904 editor
11905 .update_in(cx, |editor, window, cx| {
11906 editor.perform_format(
11907 project.clone(),
11908 FormatTrigger::Manual,
11909 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
11910 window,
11911 cx,
11912 )
11913 })
11914 .unwrap()
11915 .await;
11916 editor.update(cx, |editor, cx| {
11917 assert_eq!(
11918 editor.text(cx),
11919 r#"
11920 applied-code-action-2-edit
11921 applied-code-action-1-command
11922 applied-code-action-1-edit
11923 applied-formatting
11924 one
11925 two
11926 three
11927 "#
11928 .unindent()
11929 );
11930 });
11931
11932 editor.update_in(cx, |editor, window, cx| {
11933 editor.undo(&Default::default(), window, cx);
11934 assert_eq!(editor.text(cx), "one \ntwo \nthree");
11935 });
11936
11937 // Perform a manual edit while waiting for an LSP command
11938 // that's being run as part of a formatting code action.
11939 let lock_guard = command_lock.lock().await;
11940 let format = editor
11941 .update_in(cx, |editor, window, cx| {
11942 editor.perform_format(
11943 project.clone(),
11944 FormatTrigger::Manual,
11945 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
11946 window,
11947 cx,
11948 )
11949 })
11950 .unwrap();
11951 cx.run_until_parked();
11952 editor.update(cx, |editor, cx| {
11953 assert_eq!(
11954 editor.text(cx),
11955 r#"
11956 applied-code-action-1-edit
11957 applied-formatting
11958 one
11959 two
11960 three
11961 "#
11962 .unindent()
11963 );
11964
11965 editor.buffer.update(cx, |buffer, cx| {
11966 let ix = buffer.len(cx);
11967 buffer.edit([(ix..ix, "edited\n")], None, cx);
11968 });
11969 });
11970
11971 // Allow the LSP command to proceed. Because the buffer was edited,
11972 // the second code action will not be run.
11973 drop(lock_guard);
11974 format.await;
11975 editor.update_in(cx, |editor, window, cx| {
11976 assert_eq!(
11977 editor.text(cx),
11978 r#"
11979 applied-code-action-1-command
11980 applied-code-action-1-edit
11981 applied-formatting
11982 one
11983 two
11984 three
11985 edited
11986 "#
11987 .unindent()
11988 );
11989
11990 // The manual edit is undone first, because it is the last thing the user did
11991 // (even though the command completed afterwards).
11992 editor.undo(&Default::default(), window, cx);
11993 assert_eq!(
11994 editor.text(cx),
11995 r#"
11996 applied-code-action-1-command
11997 applied-code-action-1-edit
11998 applied-formatting
11999 one
12000 two
12001 three
12002 "#
12003 .unindent()
12004 );
12005
12006 // All the formatting (including the command, which completed after the manual edit)
12007 // is undone together.
12008 editor.undo(&Default::default(), window, cx);
12009 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12010 });
12011}
12012
12013#[gpui::test]
12014async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
12015 init_test(cx, |settings| {
12016 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
12017 Formatter::LanguageServer { name: None },
12018 ])))
12019 });
12020
12021 let fs = FakeFs::new(cx.executor());
12022 fs.insert_file(path!("/file.ts"), Default::default()).await;
12023
12024 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12025
12026 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12027 language_registry.add(Arc::new(Language::new(
12028 LanguageConfig {
12029 name: "TypeScript".into(),
12030 matcher: LanguageMatcher {
12031 path_suffixes: vec!["ts".to_string()],
12032 ..Default::default()
12033 },
12034 ..LanguageConfig::default()
12035 },
12036 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12037 )));
12038 update_test_language_settings(cx, |settings| {
12039 settings.defaults.prettier = Some(PrettierSettings {
12040 allowed: true,
12041 ..PrettierSettings::default()
12042 });
12043 });
12044 let mut fake_servers = language_registry.register_fake_lsp(
12045 "TypeScript",
12046 FakeLspAdapter {
12047 capabilities: lsp::ServerCapabilities {
12048 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12049 ..Default::default()
12050 },
12051 ..Default::default()
12052 },
12053 );
12054
12055 let buffer = project
12056 .update(cx, |project, cx| {
12057 project.open_local_buffer(path!("/file.ts"), cx)
12058 })
12059 .await
12060 .unwrap();
12061
12062 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12063 let (editor, cx) = cx.add_window_view(|window, cx| {
12064 build_editor_with_project(project.clone(), buffer, window, cx)
12065 });
12066 editor.update_in(cx, |editor, window, cx| {
12067 editor.set_text(
12068 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12069 window,
12070 cx,
12071 )
12072 });
12073
12074 cx.executor().start_waiting();
12075 let fake_server = fake_servers.next().await.unwrap();
12076
12077 let format = editor
12078 .update_in(cx, |editor, window, cx| {
12079 editor.perform_code_action_kind(
12080 project.clone(),
12081 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12082 window,
12083 cx,
12084 )
12085 })
12086 .unwrap();
12087 fake_server
12088 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
12089 assert_eq!(
12090 params.text_document.uri,
12091 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12092 );
12093 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12094 lsp::CodeAction {
12095 title: "Organize Imports".to_string(),
12096 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
12097 edit: Some(lsp::WorkspaceEdit {
12098 changes: Some(
12099 [(
12100 params.text_document.uri.clone(),
12101 vec![lsp::TextEdit::new(
12102 lsp::Range::new(
12103 lsp::Position::new(1, 0),
12104 lsp::Position::new(2, 0),
12105 ),
12106 "".to_string(),
12107 )],
12108 )]
12109 .into_iter()
12110 .collect(),
12111 ),
12112 ..Default::default()
12113 }),
12114 ..Default::default()
12115 },
12116 )]))
12117 })
12118 .next()
12119 .await;
12120 cx.executor().start_waiting();
12121 format.await;
12122 assert_eq!(
12123 editor.update(cx, |editor, cx| editor.text(cx)),
12124 "import { a } from 'module';\n\nconst x = a;\n"
12125 );
12126
12127 editor.update_in(cx, |editor, window, cx| {
12128 editor.set_text(
12129 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12130 window,
12131 cx,
12132 )
12133 });
12134 // Ensure we don't lock if code action hangs.
12135 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12136 move |params, _| async move {
12137 assert_eq!(
12138 params.text_document.uri,
12139 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12140 );
12141 futures::future::pending::<()>().await;
12142 unreachable!()
12143 },
12144 );
12145 let format = editor
12146 .update_in(cx, |editor, window, cx| {
12147 editor.perform_code_action_kind(
12148 project,
12149 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12150 window,
12151 cx,
12152 )
12153 })
12154 .unwrap();
12155 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
12156 cx.executor().start_waiting();
12157 format.await;
12158 assert_eq!(
12159 editor.update(cx, |editor, cx| editor.text(cx)),
12160 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
12161 );
12162}
12163
12164#[gpui::test]
12165async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
12166 init_test(cx, |_| {});
12167
12168 let mut cx = EditorLspTestContext::new_rust(
12169 lsp::ServerCapabilities {
12170 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12171 ..Default::default()
12172 },
12173 cx,
12174 )
12175 .await;
12176
12177 cx.set_state(indoc! {"
12178 one.twoˇ
12179 "});
12180
12181 // The format request takes a long time. When it completes, it inserts
12182 // a newline and an indent before the `.`
12183 cx.lsp
12184 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
12185 let executor = cx.background_executor().clone();
12186 async move {
12187 executor.timer(Duration::from_millis(100)).await;
12188 Ok(Some(vec![lsp::TextEdit {
12189 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
12190 new_text: "\n ".into(),
12191 }]))
12192 }
12193 });
12194
12195 // Submit a format request.
12196 let format_1 = cx
12197 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12198 .unwrap();
12199 cx.executor().run_until_parked();
12200
12201 // Submit a second format request.
12202 let format_2 = cx
12203 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12204 .unwrap();
12205 cx.executor().run_until_parked();
12206
12207 // Wait for both format requests to complete
12208 cx.executor().advance_clock(Duration::from_millis(200));
12209 cx.executor().start_waiting();
12210 format_1.await.unwrap();
12211 cx.executor().start_waiting();
12212 format_2.await.unwrap();
12213
12214 // The formatting edits only happens once.
12215 cx.assert_editor_state(indoc! {"
12216 one
12217 .twoˇ
12218 "});
12219}
12220
12221#[gpui::test]
12222async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
12223 init_test(cx, |settings| {
12224 settings.defaults.formatter = Some(SelectedFormatter::Auto)
12225 });
12226
12227 let mut cx = EditorLspTestContext::new_rust(
12228 lsp::ServerCapabilities {
12229 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12230 ..Default::default()
12231 },
12232 cx,
12233 )
12234 .await;
12235
12236 // Set up a buffer white some trailing whitespace and no trailing newline.
12237 cx.set_state(
12238 &[
12239 "one ", //
12240 "twoˇ", //
12241 "three ", //
12242 "four", //
12243 ]
12244 .join("\n"),
12245 );
12246
12247 // Submit a format request.
12248 let format = cx
12249 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12250 .unwrap();
12251
12252 // Record which buffer changes have been sent to the language server
12253 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
12254 cx.lsp
12255 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
12256 let buffer_changes = buffer_changes.clone();
12257 move |params, _| {
12258 buffer_changes.lock().extend(
12259 params
12260 .content_changes
12261 .into_iter()
12262 .map(|e| (e.range.unwrap(), e.text)),
12263 );
12264 }
12265 });
12266
12267 // Handle formatting requests to the language server.
12268 cx.lsp
12269 .set_request_handler::<lsp::request::Formatting, _, _>({
12270 let buffer_changes = buffer_changes.clone();
12271 move |_, _| {
12272 // When formatting is requested, trailing whitespace has already been stripped,
12273 // and the trailing newline has already been added.
12274 assert_eq!(
12275 &buffer_changes.lock()[1..],
12276 &[
12277 (
12278 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
12279 "".into()
12280 ),
12281 (
12282 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
12283 "".into()
12284 ),
12285 (
12286 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
12287 "\n".into()
12288 ),
12289 ]
12290 );
12291
12292 // Insert blank lines between each line of the buffer.
12293 async move {
12294 Ok(Some(vec![
12295 lsp::TextEdit {
12296 range: lsp::Range::new(
12297 lsp::Position::new(1, 0),
12298 lsp::Position::new(1, 0),
12299 ),
12300 new_text: "\n".into(),
12301 },
12302 lsp::TextEdit {
12303 range: lsp::Range::new(
12304 lsp::Position::new(2, 0),
12305 lsp::Position::new(2, 0),
12306 ),
12307 new_text: "\n".into(),
12308 },
12309 ]))
12310 }
12311 }
12312 });
12313
12314 // After formatting the buffer, the trailing whitespace is stripped,
12315 // a newline is appended, and the edits provided by the language server
12316 // have been applied.
12317 format.await.unwrap();
12318 cx.assert_editor_state(
12319 &[
12320 "one", //
12321 "", //
12322 "twoˇ", //
12323 "", //
12324 "three", //
12325 "four", //
12326 "", //
12327 ]
12328 .join("\n"),
12329 );
12330
12331 // Undoing the formatting undoes the trailing whitespace removal, the
12332 // trailing newline, and the LSP edits.
12333 cx.update_buffer(|buffer, cx| buffer.undo(cx));
12334 cx.assert_editor_state(
12335 &[
12336 "one ", //
12337 "twoˇ", //
12338 "three ", //
12339 "four", //
12340 ]
12341 .join("\n"),
12342 );
12343}
12344
12345#[gpui::test]
12346async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
12347 cx: &mut TestAppContext,
12348) {
12349 init_test(cx, |_| {});
12350
12351 cx.update(|cx| {
12352 cx.update_global::<SettingsStore, _>(|settings, cx| {
12353 settings.update_user_settings::<EditorSettings>(cx, |settings| {
12354 settings.auto_signature_help = Some(true);
12355 });
12356 });
12357 });
12358
12359 let mut cx = EditorLspTestContext::new_rust(
12360 lsp::ServerCapabilities {
12361 signature_help_provider: Some(lsp::SignatureHelpOptions {
12362 ..Default::default()
12363 }),
12364 ..Default::default()
12365 },
12366 cx,
12367 )
12368 .await;
12369
12370 let language = Language::new(
12371 LanguageConfig {
12372 name: "Rust".into(),
12373 brackets: BracketPairConfig {
12374 pairs: vec![
12375 BracketPair {
12376 start: "{".to_string(),
12377 end: "}".to_string(),
12378 close: true,
12379 surround: true,
12380 newline: true,
12381 },
12382 BracketPair {
12383 start: "(".to_string(),
12384 end: ")".to_string(),
12385 close: true,
12386 surround: true,
12387 newline: true,
12388 },
12389 BracketPair {
12390 start: "/*".to_string(),
12391 end: " */".to_string(),
12392 close: true,
12393 surround: true,
12394 newline: true,
12395 },
12396 BracketPair {
12397 start: "[".to_string(),
12398 end: "]".to_string(),
12399 close: false,
12400 surround: false,
12401 newline: true,
12402 },
12403 BracketPair {
12404 start: "\"".to_string(),
12405 end: "\"".to_string(),
12406 close: true,
12407 surround: true,
12408 newline: false,
12409 },
12410 BracketPair {
12411 start: "<".to_string(),
12412 end: ">".to_string(),
12413 close: false,
12414 surround: true,
12415 newline: true,
12416 },
12417 ],
12418 ..Default::default()
12419 },
12420 autoclose_before: "})]".to_string(),
12421 ..Default::default()
12422 },
12423 Some(tree_sitter_rust::LANGUAGE.into()),
12424 );
12425 let language = Arc::new(language);
12426
12427 cx.language_registry().add(language.clone());
12428 cx.update_buffer(|buffer, cx| {
12429 buffer.set_language(Some(language), cx);
12430 });
12431
12432 cx.set_state(
12433 &r#"
12434 fn main() {
12435 sampleˇ
12436 }
12437 "#
12438 .unindent(),
12439 );
12440
12441 cx.update_editor(|editor, window, cx| {
12442 editor.handle_input("(", window, cx);
12443 });
12444 cx.assert_editor_state(
12445 &"
12446 fn main() {
12447 sample(ˇ)
12448 }
12449 "
12450 .unindent(),
12451 );
12452
12453 let mocked_response = lsp::SignatureHelp {
12454 signatures: vec![lsp::SignatureInformation {
12455 label: "fn sample(param1: u8, param2: u8)".to_string(),
12456 documentation: None,
12457 parameters: Some(vec![
12458 lsp::ParameterInformation {
12459 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12460 documentation: None,
12461 },
12462 lsp::ParameterInformation {
12463 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12464 documentation: None,
12465 },
12466 ]),
12467 active_parameter: None,
12468 }],
12469 active_signature: Some(0),
12470 active_parameter: Some(0),
12471 };
12472 handle_signature_help_request(&mut cx, mocked_response).await;
12473
12474 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12475 .await;
12476
12477 cx.editor(|editor, _, _| {
12478 let signature_help_state = editor.signature_help_state.popover().cloned();
12479 let signature = signature_help_state.unwrap();
12480 assert_eq!(
12481 signature.signatures[signature.current_signature].label,
12482 "fn sample(param1: u8, param2: u8)"
12483 );
12484 });
12485}
12486
12487#[gpui::test]
12488async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
12489 init_test(cx, |_| {});
12490
12491 cx.update(|cx| {
12492 cx.update_global::<SettingsStore, _>(|settings, cx| {
12493 settings.update_user_settings::<EditorSettings>(cx, |settings| {
12494 settings.auto_signature_help = Some(false);
12495 settings.show_signature_help_after_edits = Some(false);
12496 });
12497 });
12498 });
12499
12500 let mut cx = EditorLspTestContext::new_rust(
12501 lsp::ServerCapabilities {
12502 signature_help_provider: Some(lsp::SignatureHelpOptions {
12503 ..Default::default()
12504 }),
12505 ..Default::default()
12506 },
12507 cx,
12508 )
12509 .await;
12510
12511 let language = Language::new(
12512 LanguageConfig {
12513 name: "Rust".into(),
12514 brackets: BracketPairConfig {
12515 pairs: vec![
12516 BracketPair {
12517 start: "{".to_string(),
12518 end: "}".to_string(),
12519 close: true,
12520 surround: true,
12521 newline: true,
12522 },
12523 BracketPair {
12524 start: "(".to_string(),
12525 end: ")".to_string(),
12526 close: true,
12527 surround: true,
12528 newline: true,
12529 },
12530 BracketPair {
12531 start: "/*".to_string(),
12532 end: " */".to_string(),
12533 close: true,
12534 surround: true,
12535 newline: true,
12536 },
12537 BracketPair {
12538 start: "[".to_string(),
12539 end: "]".to_string(),
12540 close: false,
12541 surround: false,
12542 newline: true,
12543 },
12544 BracketPair {
12545 start: "\"".to_string(),
12546 end: "\"".to_string(),
12547 close: true,
12548 surround: true,
12549 newline: false,
12550 },
12551 BracketPair {
12552 start: "<".to_string(),
12553 end: ">".to_string(),
12554 close: false,
12555 surround: true,
12556 newline: true,
12557 },
12558 ],
12559 ..Default::default()
12560 },
12561 autoclose_before: "})]".to_string(),
12562 ..Default::default()
12563 },
12564 Some(tree_sitter_rust::LANGUAGE.into()),
12565 );
12566 let language = Arc::new(language);
12567
12568 cx.language_registry().add(language.clone());
12569 cx.update_buffer(|buffer, cx| {
12570 buffer.set_language(Some(language), cx);
12571 });
12572
12573 // Ensure that signature_help is not called when no signature help is enabled.
12574 cx.set_state(
12575 &r#"
12576 fn main() {
12577 sampleˇ
12578 }
12579 "#
12580 .unindent(),
12581 );
12582 cx.update_editor(|editor, window, cx| {
12583 editor.handle_input("(", window, cx);
12584 });
12585 cx.assert_editor_state(
12586 &"
12587 fn main() {
12588 sample(ˇ)
12589 }
12590 "
12591 .unindent(),
12592 );
12593 cx.editor(|editor, _, _| {
12594 assert!(editor.signature_help_state.task().is_none());
12595 });
12596
12597 let mocked_response = lsp::SignatureHelp {
12598 signatures: vec![lsp::SignatureInformation {
12599 label: "fn sample(param1: u8, param2: u8)".to_string(),
12600 documentation: None,
12601 parameters: Some(vec![
12602 lsp::ParameterInformation {
12603 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12604 documentation: None,
12605 },
12606 lsp::ParameterInformation {
12607 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12608 documentation: None,
12609 },
12610 ]),
12611 active_parameter: None,
12612 }],
12613 active_signature: Some(0),
12614 active_parameter: Some(0),
12615 };
12616
12617 // Ensure that signature_help is called when enabled afte edits
12618 cx.update(|_, cx| {
12619 cx.update_global::<SettingsStore, _>(|settings, cx| {
12620 settings.update_user_settings::<EditorSettings>(cx, |settings| {
12621 settings.auto_signature_help = Some(false);
12622 settings.show_signature_help_after_edits = Some(true);
12623 });
12624 });
12625 });
12626 cx.set_state(
12627 &r#"
12628 fn main() {
12629 sampleˇ
12630 }
12631 "#
12632 .unindent(),
12633 );
12634 cx.update_editor(|editor, window, cx| {
12635 editor.handle_input("(", window, cx);
12636 });
12637 cx.assert_editor_state(
12638 &"
12639 fn main() {
12640 sample(ˇ)
12641 }
12642 "
12643 .unindent(),
12644 );
12645 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12646 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12647 .await;
12648 cx.update_editor(|editor, _, _| {
12649 let signature_help_state = editor.signature_help_state.popover().cloned();
12650 assert!(signature_help_state.is_some());
12651 let signature = signature_help_state.unwrap();
12652 assert_eq!(
12653 signature.signatures[signature.current_signature].label,
12654 "fn sample(param1: u8, param2: u8)"
12655 );
12656 editor.signature_help_state = SignatureHelpState::default();
12657 });
12658
12659 // Ensure that signature_help is called when auto signature help override is enabled
12660 cx.update(|_, cx| {
12661 cx.update_global::<SettingsStore, _>(|settings, cx| {
12662 settings.update_user_settings::<EditorSettings>(cx, |settings| {
12663 settings.auto_signature_help = Some(true);
12664 settings.show_signature_help_after_edits = Some(false);
12665 });
12666 });
12667 });
12668 cx.set_state(
12669 &r#"
12670 fn main() {
12671 sampleˇ
12672 }
12673 "#
12674 .unindent(),
12675 );
12676 cx.update_editor(|editor, window, cx| {
12677 editor.handle_input("(", window, cx);
12678 });
12679 cx.assert_editor_state(
12680 &"
12681 fn main() {
12682 sample(ˇ)
12683 }
12684 "
12685 .unindent(),
12686 );
12687 handle_signature_help_request(&mut cx, mocked_response).await;
12688 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12689 .await;
12690 cx.editor(|editor, _, _| {
12691 let signature_help_state = editor.signature_help_state.popover().cloned();
12692 assert!(signature_help_state.is_some());
12693 let signature = signature_help_state.unwrap();
12694 assert_eq!(
12695 signature.signatures[signature.current_signature].label,
12696 "fn sample(param1: u8, param2: u8)"
12697 );
12698 });
12699}
12700
12701#[gpui::test]
12702async fn test_signature_help(cx: &mut TestAppContext) {
12703 init_test(cx, |_| {});
12704 cx.update(|cx| {
12705 cx.update_global::<SettingsStore, _>(|settings, cx| {
12706 settings.update_user_settings::<EditorSettings>(cx, |settings| {
12707 settings.auto_signature_help = Some(true);
12708 });
12709 });
12710 });
12711
12712 let mut cx = EditorLspTestContext::new_rust(
12713 lsp::ServerCapabilities {
12714 signature_help_provider: Some(lsp::SignatureHelpOptions {
12715 ..Default::default()
12716 }),
12717 ..Default::default()
12718 },
12719 cx,
12720 )
12721 .await;
12722
12723 // A test that directly calls `show_signature_help`
12724 cx.update_editor(|editor, window, cx| {
12725 editor.show_signature_help(&ShowSignatureHelp, window, cx);
12726 });
12727
12728 let mocked_response = lsp::SignatureHelp {
12729 signatures: vec![lsp::SignatureInformation {
12730 label: "fn sample(param1: u8, param2: u8)".to_string(),
12731 documentation: None,
12732 parameters: Some(vec![
12733 lsp::ParameterInformation {
12734 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12735 documentation: None,
12736 },
12737 lsp::ParameterInformation {
12738 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12739 documentation: None,
12740 },
12741 ]),
12742 active_parameter: None,
12743 }],
12744 active_signature: Some(0),
12745 active_parameter: Some(0),
12746 };
12747 handle_signature_help_request(&mut cx, mocked_response).await;
12748
12749 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12750 .await;
12751
12752 cx.editor(|editor, _, _| {
12753 let signature_help_state = editor.signature_help_state.popover().cloned();
12754 assert!(signature_help_state.is_some());
12755 let signature = signature_help_state.unwrap();
12756 assert_eq!(
12757 signature.signatures[signature.current_signature].label,
12758 "fn sample(param1: u8, param2: u8)"
12759 );
12760 });
12761
12762 // When exiting outside from inside the brackets, `signature_help` is closed.
12763 cx.set_state(indoc! {"
12764 fn main() {
12765 sample(ˇ);
12766 }
12767
12768 fn sample(param1: u8, param2: u8) {}
12769 "});
12770
12771 cx.update_editor(|editor, window, cx| {
12772 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12773 s.select_ranges([0..0])
12774 });
12775 });
12776
12777 let mocked_response = lsp::SignatureHelp {
12778 signatures: Vec::new(),
12779 active_signature: None,
12780 active_parameter: None,
12781 };
12782 handle_signature_help_request(&mut cx, mocked_response).await;
12783
12784 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
12785 .await;
12786
12787 cx.editor(|editor, _, _| {
12788 assert!(!editor.signature_help_state.is_shown());
12789 });
12790
12791 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
12792 cx.set_state(indoc! {"
12793 fn main() {
12794 sample(ˇ);
12795 }
12796
12797 fn sample(param1: u8, param2: u8) {}
12798 "});
12799
12800 let mocked_response = lsp::SignatureHelp {
12801 signatures: vec![lsp::SignatureInformation {
12802 label: "fn sample(param1: u8, param2: u8)".to_string(),
12803 documentation: None,
12804 parameters: Some(vec![
12805 lsp::ParameterInformation {
12806 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12807 documentation: None,
12808 },
12809 lsp::ParameterInformation {
12810 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12811 documentation: None,
12812 },
12813 ]),
12814 active_parameter: None,
12815 }],
12816 active_signature: Some(0),
12817 active_parameter: Some(0),
12818 };
12819 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12820 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12821 .await;
12822 cx.editor(|editor, _, _| {
12823 assert!(editor.signature_help_state.is_shown());
12824 });
12825
12826 // Restore the popover with more parameter input
12827 cx.set_state(indoc! {"
12828 fn main() {
12829 sample(param1, param2ˇ);
12830 }
12831
12832 fn sample(param1: u8, param2: u8) {}
12833 "});
12834
12835 let mocked_response = lsp::SignatureHelp {
12836 signatures: vec![lsp::SignatureInformation {
12837 label: "fn sample(param1: u8, param2: u8)".to_string(),
12838 documentation: None,
12839 parameters: Some(vec![
12840 lsp::ParameterInformation {
12841 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12842 documentation: None,
12843 },
12844 lsp::ParameterInformation {
12845 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12846 documentation: None,
12847 },
12848 ]),
12849 active_parameter: None,
12850 }],
12851 active_signature: Some(0),
12852 active_parameter: Some(1),
12853 };
12854 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12855 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12856 .await;
12857
12858 // When selecting a range, the popover is gone.
12859 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
12860 cx.update_editor(|editor, window, cx| {
12861 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12862 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
12863 })
12864 });
12865 cx.assert_editor_state(indoc! {"
12866 fn main() {
12867 sample(param1, «ˇparam2»);
12868 }
12869
12870 fn sample(param1: u8, param2: u8) {}
12871 "});
12872 cx.editor(|editor, _, _| {
12873 assert!(!editor.signature_help_state.is_shown());
12874 });
12875
12876 // When unselecting again, the popover is back if within the brackets.
12877 cx.update_editor(|editor, window, cx| {
12878 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12879 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
12880 })
12881 });
12882 cx.assert_editor_state(indoc! {"
12883 fn main() {
12884 sample(param1, ˇparam2);
12885 }
12886
12887 fn sample(param1: u8, param2: u8) {}
12888 "});
12889 handle_signature_help_request(&mut cx, mocked_response).await;
12890 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12891 .await;
12892 cx.editor(|editor, _, _| {
12893 assert!(editor.signature_help_state.is_shown());
12894 });
12895
12896 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
12897 cx.update_editor(|editor, window, cx| {
12898 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12899 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
12900 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
12901 })
12902 });
12903 cx.assert_editor_state(indoc! {"
12904 fn main() {
12905 sample(param1, ˇparam2);
12906 }
12907
12908 fn sample(param1: u8, param2: u8) {}
12909 "});
12910
12911 let mocked_response = lsp::SignatureHelp {
12912 signatures: vec![lsp::SignatureInformation {
12913 label: "fn sample(param1: u8, param2: u8)".to_string(),
12914 documentation: None,
12915 parameters: Some(vec![
12916 lsp::ParameterInformation {
12917 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12918 documentation: None,
12919 },
12920 lsp::ParameterInformation {
12921 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12922 documentation: None,
12923 },
12924 ]),
12925 active_parameter: None,
12926 }],
12927 active_signature: Some(0),
12928 active_parameter: Some(1),
12929 };
12930 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12931 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12932 .await;
12933 cx.update_editor(|editor, _, cx| {
12934 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
12935 });
12936 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
12937 .await;
12938 cx.update_editor(|editor, window, cx| {
12939 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12940 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
12941 })
12942 });
12943 cx.assert_editor_state(indoc! {"
12944 fn main() {
12945 sample(param1, «ˇparam2»);
12946 }
12947
12948 fn sample(param1: u8, param2: u8) {}
12949 "});
12950 cx.update_editor(|editor, window, cx| {
12951 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12952 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
12953 })
12954 });
12955 cx.assert_editor_state(indoc! {"
12956 fn main() {
12957 sample(param1, ˇparam2);
12958 }
12959
12960 fn sample(param1: u8, param2: u8) {}
12961 "});
12962 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
12963 .await;
12964}
12965
12966#[gpui::test]
12967async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
12968 init_test(cx, |_| {});
12969
12970 let mut cx = EditorLspTestContext::new_rust(
12971 lsp::ServerCapabilities {
12972 signature_help_provider: Some(lsp::SignatureHelpOptions {
12973 ..Default::default()
12974 }),
12975 ..Default::default()
12976 },
12977 cx,
12978 )
12979 .await;
12980
12981 cx.set_state(indoc! {"
12982 fn main() {
12983 overloadedˇ
12984 }
12985 "});
12986
12987 cx.update_editor(|editor, window, cx| {
12988 editor.handle_input("(", window, cx);
12989 editor.show_signature_help(&ShowSignatureHelp, window, cx);
12990 });
12991
12992 // Mock response with 3 signatures
12993 let mocked_response = lsp::SignatureHelp {
12994 signatures: vec![
12995 lsp::SignatureInformation {
12996 label: "fn overloaded(x: i32)".to_string(),
12997 documentation: None,
12998 parameters: Some(vec![lsp::ParameterInformation {
12999 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13000 documentation: None,
13001 }]),
13002 active_parameter: None,
13003 },
13004 lsp::SignatureInformation {
13005 label: "fn overloaded(x: i32, y: i32)".to_string(),
13006 documentation: None,
13007 parameters: Some(vec![
13008 lsp::ParameterInformation {
13009 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13010 documentation: None,
13011 },
13012 lsp::ParameterInformation {
13013 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13014 documentation: None,
13015 },
13016 ]),
13017 active_parameter: None,
13018 },
13019 lsp::SignatureInformation {
13020 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
13021 documentation: None,
13022 parameters: Some(vec![
13023 lsp::ParameterInformation {
13024 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13025 documentation: None,
13026 },
13027 lsp::ParameterInformation {
13028 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13029 documentation: None,
13030 },
13031 lsp::ParameterInformation {
13032 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
13033 documentation: None,
13034 },
13035 ]),
13036 active_parameter: None,
13037 },
13038 ],
13039 active_signature: Some(1),
13040 active_parameter: Some(0),
13041 };
13042 handle_signature_help_request(&mut cx, mocked_response).await;
13043
13044 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13045 .await;
13046
13047 // Verify we have multiple signatures and the right one is selected
13048 cx.editor(|editor, _, _| {
13049 let popover = editor.signature_help_state.popover().cloned().unwrap();
13050 assert_eq!(popover.signatures.len(), 3);
13051 // active_signature was 1, so that should be the current
13052 assert_eq!(popover.current_signature, 1);
13053 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
13054 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
13055 assert_eq!(
13056 popover.signatures[2].label,
13057 "fn overloaded(x: i32, y: i32, z: i32)"
13058 );
13059 });
13060
13061 // Test navigation functionality
13062 cx.update_editor(|editor, window, cx| {
13063 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13064 });
13065
13066 cx.editor(|editor, _, _| {
13067 let popover = editor.signature_help_state.popover().cloned().unwrap();
13068 assert_eq!(popover.current_signature, 2);
13069 });
13070
13071 // Test wrap around
13072 cx.update_editor(|editor, window, cx| {
13073 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13074 });
13075
13076 cx.editor(|editor, _, _| {
13077 let popover = editor.signature_help_state.popover().cloned().unwrap();
13078 assert_eq!(popover.current_signature, 0);
13079 });
13080
13081 // Test previous navigation
13082 cx.update_editor(|editor, window, cx| {
13083 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
13084 });
13085
13086 cx.editor(|editor, _, _| {
13087 let popover = editor.signature_help_state.popover().cloned().unwrap();
13088 assert_eq!(popover.current_signature, 2);
13089 });
13090}
13091
13092#[gpui::test]
13093async fn test_completion_mode(cx: &mut TestAppContext) {
13094 init_test(cx, |_| {});
13095 let mut cx = EditorLspTestContext::new_rust(
13096 lsp::ServerCapabilities {
13097 completion_provider: Some(lsp::CompletionOptions {
13098 resolve_provider: Some(true),
13099 ..Default::default()
13100 }),
13101 ..Default::default()
13102 },
13103 cx,
13104 )
13105 .await;
13106
13107 struct Run {
13108 run_description: &'static str,
13109 initial_state: String,
13110 buffer_marked_text: String,
13111 completion_label: &'static str,
13112 completion_text: &'static str,
13113 expected_with_insert_mode: String,
13114 expected_with_replace_mode: String,
13115 expected_with_replace_subsequence_mode: String,
13116 expected_with_replace_suffix_mode: String,
13117 }
13118
13119 let runs = [
13120 Run {
13121 run_description: "Start of word matches completion text",
13122 initial_state: "before ediˇ after".into(),
13123 buffer_marked_text: "before <edi|> after".into(),
13124 completion_label: "editor",
13125 completion_text: "editor",
13126 expected_with_insert_mode: "before editorˇ after".into(),
13127 expected_with_replace_mode: "before editorˇ after".into(),
13128 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13129 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13130 },
13131 Run {
13132 run_description: "Accept same text at the middle of the word",
13133 initial_state: "before ediˇtor after".into(),
13134 buffer_marked_text: "before <edi|tor> after".into(),
13135 completion_label: "editor",
13136 completion_text: "editor",
13137 expected_with_insert_mode: "before editorˇtor after".into(),
13138 expected_with_replace_mode: "before editorˇ after".into(),
13139 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13140 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13141 },
13142 Run {
13143 run_description: "End of word matches completion text -- cursor at end",
13144 initial_state: "before torˇ after".into(),
13145 buffer_marked_text: "before <tor|> after".into(),
13146 completion_label: "editor",
13147 completion_text: "editor",
13148 expected_with_insert_mode: "before editorˇ after".into(),
13149 expected_with_replace_mode: "before editorˇ after".into(),
13150 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13151 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13152 },
13153 Run {
13154 run_description: "End of word matches completion text -- cursor at start",
13155 initial_state: "before ˇtor after".into(),
13156 buffer_marked_text: "before <|tor> after".into(),
13157 completion_label: "editor",
13158 completion_text: "editor",
13159 expected_with_insert_mode: "before editorˇtor after".into(),
13160 expected_with_replace_mode: "before editorˇ after".into(),
13161 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13162 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13163 },
13164 Run {
13165 run_description: "Prepend text containing whitespace",
13166 initial_state: "pˇfield: bool".into(),
13167 buffer_marked_text: "<p|field>: bool".into(),
13168 completion_label: "pub ",
13169 completion_text: "pub ",
13170 expected_with_insert_mode: "pub ˇfield: bool".into(),
13171 expected_with_replace_mode: "pub ˇ: bool".into(),
13172 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
13173 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
13174 },
13175 Run {
13176 run_description: "Add element to start of list",
13177 initial_state: "[element_ˇelement_2]".into(),
13178 buffer_marked_text: "[<element_|element_2>]".into(),
13179 completion_label: "element_1",
13180 completion_text: "element_1",
13181 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
13182 expected_with_replace_mode: "[element_1ˇ]".into(),
13183 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
13184 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
13185 },
13186 Run {
13187 run_description: "Add element to start of list -- first and second elements are equal",
13188 initial_state: "[elˇelement]".into(),
13189 buffer_marked_text: "[<el|element>]".into(),
13190 completion_label: "element",
13191 completion_text: "element",
13192 expected_with_insert_mode: "[elementˇelement]".into(),
13193 expected_with_replace_mode: "[elementˇ]".into(),
13194 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
13195 expected_with_replace_suffix_mode: "[elementˇ]".into(),
13196 },
13197 Run {
13198 run_description: "Ends with matching suffix",
13199 initial_state: "SubˇError".into(),
13200 buffer_marked_text: "<Sub|Error>".into(),
13201 completion_label: "SubscriptionError",
13202 completion_text: "SubscriptionError",
13203 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
13204 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13205 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13206 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
13207 },
13208 Run {
13209 run_description: "Suffix is a subsequence -- contiguous",
13210 initial_state: "SubˇErr".into(),
13211 buffer_marked_text: "<Sub|Err>".into(),
13212 completion_label: "SubscriptionError",
13213 completion_text: "SubscriptionError",
13214 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
13215 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13216 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13217 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
13218 },
13219 Run {
13220 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
13221 initial_state: "Suˇscrirr".into(),
13222 buffer_marked_text: "<Su|scrirr>".into(),
13223 completion_label: "SubscriptionError",
13224 completion_text: "SubscriptionError",
13225 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
13226 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13227 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13228 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
13229 },
13230 Run {
13231 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
13232 initial_state: "foo(indˇix)".into(),
13233 buffer_marked_text: "foo(<ind|ix>)".into(),
13234 completion_label: "node_index",
13235 completion_text: "node_index",
13236 expected_with_insert_mode: "foo(node_indexˇix)".into(),
13237 expected_with_replace_mode: "foo(node_indexˇ)".into(),
13238 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
13239 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
13240 },
13241 Run {
13242 run_description: "Replace range ends before cursor - should extend to cursor",
13243 initial_state: "before editˇo after".into(),
13244 buffer_marked_text: "before <{ed}>it|o after".into(),
13245 completion_label: "editor",
13246 completion_text: "editor",
13247 expected_with_insert_mode: "before editorˇo after".into(),
13248 expected_with_replace_mode: "before editorˇo after".into(),
13249 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
13250 expected_with_replace_suffix_mode: "before editorˇo after".into(),
13251 },
13252 Run {
13253 run_description: "Uses label for suffix matching",
13254 initial_state: "before ediˇtor after".into(),
13255 buffer_marked_text: "before <edi|tor> after".into(),
13256 completion_label: "editor",
13257 completion_text: "editor()",
13258 expected_with_insert_mode: "before editor()ˇtor after".into(),
13259 expected_with_replace_mode: "before editor()ˇ after".into(),
13260 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
13261 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
13262 },
13263 Run {
13264 run_description: "Case insensitive subsequence and suffix matching",
13265 initial_state: "before EDiˇtoR after".into(),
13266 buffer_marked_text: "before <EDi|toR> after".into(),
13267 completion_label: "editor",
13268 completion_text: "editor",
13269 expected_with_insert_mode: "before editorˇtoR after".into(),
13270 expected_with_replace_mode: "before editorˇ after".into(),
13271 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13272 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13273 },
13274 ];
13275
13276 for run in runs {
13277 let run_variations = [
13278 (LspInsertMode::Insert, run.expected_with_insert_mode),
13279 (LspInsertMode::Replace, run.expected_with_replace_mode),
13280 (
13281 LspInsertMode::ReplaceSubsequence,
13282 run.expected_with_replace_subsequence_mode,
13283 ),
13284 (
13285 LspInsertMode::ReplaceSuffix,
13286 run.expected_with_replace_suffix_mode,
13287 ),
13288 ];
13289
13290 for (lsp_insert_mode, expected_text) in run_variations {
13291 eprintln!(
13292 "run = {:?}, mode = {lsp_insert_mode:.?}",
13293 run.run_description,
13294 );
13295
13296 update_test_language_settings(&mut cx, |settings| {
13297 settings.defaults.completions = Some(CompletionSettings {
13298 lsp_insert_mode,
13299 words: WordsCompletionMode::Disabled,
13300 words_min_length: 0,
13301 lsp: true,
13302 lsp_fetch_timeout_ms: 0,
13303 });
13304 });
13305
13306 cx.set_state(&run.initial_state);
13307 cx.update_editor(|editor, window, cx| {
13308 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13309 });
13310
13311 let counter = Arc::new(AtomicUsize::new(0));
13312 handle_completion_request_with_insert_and_replace(
13313 &mut cx,
13314 &run.buffer_marked_text,
13315 vec![(run.completion_label, run.completion_text)],
13316 counter.clone(),
13317 )
13318 .await;
13319 cx.condition(|editor, _| editor.context_menu_visible())
13320 .await;
13321 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13322
13323 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13324 editor
13325 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13326 .unwrap()
13327 });
13328 cx.assert_editor_state(&expected_text);
13329 handle_resolve_completion_request(&mut cx, None).await;
13330 apply_additional_edits.await.unwrap();
13331 }
13332 }
13333}
13334
13335#[gpui::test]
13336async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
13337 init_test(cx, |_| {});
13338 let mut cx = EditorLspTestContext::new_rust(
13339 lsp::ServerCapabilities {
13340 completion_provider: Some(lsp::CompletionOptions {
13341 resolve_provider: Some(true),
13342 ..Default::default()
13343 }),
13344 ..Default::default()
13345 },
13346 cx,
13347 )
13348 .await;
13349
13350 let initial_state = "SubˇError";
13351 let buffer_marked_text = "<Sub|Error>";
13352 let completion_text = "SubscriptionError";
13353 let expected_with_insert_mode = "SubscriptionErrorˇError";
13354 let expected_with_replace_mode = "SubscriptionErrorˇ";
13355
13356 update_test_language_settings(&mut cx, |settings| {
13357 settings.defaults.completions = Some(CompletionSettings {
13358 words: WordsCompletionMode::Disabled,
13359 words_min_length: 0,
13360 // set the opposite here to ensure that the action is overriding the default behavior
13361 lsp_insert_mode: LspInsertMode::Insert,
13362 lsp: true,
13363 lsp_fetch_timeout_ms: 0,
13364 });
13365 });
13366
13367 cx.set_state(initial_state);
13368 cx.update_editor(|editor, window, cx| {
13369 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13370 });
13371
13372 let counter = Arc::new(AtomicUsize::new(0));
13373 handle_completion_request_with_insert_and_replace(
13374 &mut cx,
13375 buffer_marked_text,
13376 vec![(completion_text, completion_text)],
13377 counter.clone(),
13378 )
13379 .await;
13380 cx.condition(|editor, _| editor.context_menu_visible())
13381 .await;
13382 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13383
13384 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13385 editor
13386 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13387 .unwrap()
13388 });
13389 cx.assert_editor_state(expected_with_replace_mode);
13390 handle_resolve_completion_request(&mut cx, None).await;
13391 apply_additional_edits.await.unwrap();
13392
13393 update_test_language_settings(&mut cx, |settings| {
13394 settings.defaults.completions = Some(CompletionSettings {
13395 words: WordsCompletionMode::Disabled,
13396 words_min_length: 0,
13397 // set the opposite here to ensure that the action is overriding the default behavior
13398 lsp_insert_mode: LspInsertMode::Replace,
13399 lsp: true,
13400 lsp_fetch_timeout_ms: 0,
13401 });
13402 });
13403
13404 cx.set_state(initial_state);
13405 cx.update_editor(|editor, window, cx| {
13406 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13407 });
13408 handle_completion_request_with_insert_and_replace(
13409 &mut cx,
13410 buffer_marked_text,
13411 vec![(completion_text, completion_text)],
13412 counter.clone(),
13413 )
13414 .await;
13415 cx.condition(|editor, _| editor.context_menu_visible())
13416 .await;
13417 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
13418
13419 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13420 editor
13421 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
13422 .unwrap()
13423 });
13424 cx.assert_editor_state(expected_with_insert_mode);
13425 handle_resolve_completion_request(&mut cx, None).await;
13426 apply_additional_edits.await.unwrap();
13427}
13428
13429#[gpui::test]
13430async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
13431 init_test(cx, |_| {});
13432 let mut cx = EditorLspTestContext::new_rust(
13433 lsp::ServerCapabilities {
13434 completion_provider: Some(lsp::CompletionOptions {
13435 resolve_provider: Some(true),
13436 ..Default::default()
13437 }),
13438 ..Default::default()
13439 },
13440 cx,
13441 )
13442 .await;
13443
13444 // scenario: surrounding text matches completion text
13445 let completion_text = "to_offset";
13446 let initial_state = indoc! {"
13447 1. buf.to_offˇsuffix
13448 2. buf.to_offˇsuf
13449 3. buf.to_offˇfix
13450 4. buf.to_offˇ
13451 5. into_offˇensive
13452 6. ˇsuffix
13453 7. let ˇ //
13454 8. aaˇzz
13455 9. buf.to_off«zzzzzˇ»suffix
13456 10. buf.«ˇzzzzz»suffix
13457 11. to_off«ˇzzzzz»
13458
13459 buf.to_offˇsuffix // newest cursor
13460 "};
13461 let completion_marked_buffer = indoc! {"
13462 1. buf.to_offsuffix
13463 2. buf.to_offsuf
13464 3. buf.to_offfix
13465 4. buf.to_off
13466 5. into_offensive
13467 6. suffix
13468 7. let //
13469 8. aazz
13470 9. buf.to_offzzzzzsuffix
13471 10. buf.zzzzzsuffix
13472 11. to_offzzzzz
13473
13474 buf.<to_off|suffix> // newest cursor
13475 "};
13476 let expected = indoc! {"
13477 1. buf.to_offsetˇ
13478 2. buf.to_offsetˇsuf
13479 3. buf.to_offsetˇfix
13480 4. buf.to_offsetˇ
13481 5. into_offsetˇensive
13482 6. to_offsetˇsuffix
13483 7. let to_offsetˇ //
13484 8. aato_offsetˇzz
13485 9. buf.to_offsetˇ
13486 10. buf.to_offsetˇsuffix
13487 11. to_offsetˇ
13488
13489 buf.to_offsetˇ // newest cursor
13490 "};
13491 cx.set_state(initial_state);
13492 cx.update_editor(|editor, window, cx| {
13493 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13494 });
13495 handle_completion_request_with_insert_and_replace(
13496 &mut cx,
13497 completion_marked_buffer,
13498 vec![(completion_text, completion_text)],
13499 Arc::new(AtomicUsize::new(0)),
13500 )
13501 .await;
13502 cx.condition(|editor, _| editor.context_menu_visible())
13503 .await;
13504 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13505 editor
13506 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13507 .unwrap()
13508 });
13509 cx.assert_editor_state(expected);
13510 handle_resolve_completion_request(&mut cx, None).await;
13511 apply_additional_edits.await.unwrap();
13512
13513 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
13514 let completion_text = "foo_and_bar";
13515 let initial_state = indoc! {"
13516 1. ooanbˇ
13517 2. zooanbˇ
13518 3. ooanbˇz
13519 4. zooanbˇz
13520 5. ooanˇ
13521 6. oanbˇ
13522
13523 ooanbˇ
13524 "};
13525 let completion_marked_buffer = indoc! {"
13526 1. ooanb
13527 2. zooanb
13528 3. ooanbz
13529 4. zooanbz
13530 5. ooan
13531 6. oanb
13532
13533 <ooanb|>
13534 "};
13535 let expected = indoc! {"
13536 1. foo_and_barˇ
13537 2. zfoo_and_barˇ
13538 3. foo_and_barˇz
13539 4. zfoo_and_barˇz
13540 5. ooanfoo_and_barˇ
13541 6. oanbfoo_and_barˇ
13542
13543 foo_and_barˇ
13544 "};
13545 cx.set_state(initial_state);
13546 cx.update_editor(|editor, window, cx| {
13547 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13548 });
13549 handle_completion_request_with_insert_and_replace(
13550 &mut cx,
13551 completion_marked_buffer,
13552 vec![(completion_text, completion_text)],
13553 Arc::new(AtomicUsize::new(0)),
13554 )
13555 .await;
13556 cx.condition(|editor, _| editor.context_menu_visible())
13557 .await;
13558 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13559 editor
13560 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13561 .unwrap()
13562 });
13563 cx.assert_editor_state(expected);
13564 handle_resolve_completion_request(&mut cx, None).await;
13565 apply_additional_edits.await.unwrap();
13566
13567 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
13568 // (expects the same as if it was inserted at the end)
13569 let completion_text = "foo_and_bar";
13570 let initial_state = indoc! {"
13571 1. ooˇanb
13572 2. zooˇanb
13573 3. ooˇanbz
13574 4. zooˇanbz
13575
13576 ooˇanb
13577 "};
13578 let completion_marked_buffer = indoc! {"
13579 1. ooanb
13580 2. zooanb
13581 3. ooanbz
13582 4. zooanbz
13583
13584 <oo|anb>
13585 "};
13586 let expected = indoc! {"
13587 1. foo_and_barˇ
13588 2. zfoo_and_barˇ
13589 3. foo_and_barˇz
13590 4. zfoo_and_barˇz
13591
13592 foo_and_barˇ
13593 "};
13594 cx.set_state(initial_state);
13595 cx.update_editor(|editor, window, cx| {
13596 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13597 });
13598 handle_completion_request_with_insert_and_replace(
13599 &mut cx,
13600 completion_marked_buffer,
13601 vec![(completion_text, completion_text)],
13602 Arc::new(AtomicUsize::new(0)),
13603 )
13604 .await;
13605 cx.condition(|editor, _| editor.context_menu_visible())
13606 .await;
13607 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13608 editor
13609 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13610 .unwrap()
13611 });
13612 cx.assert_editor_state(expected);
13613 handle_resolve_completion_request(&mut cx, None).await;
13614 apply_additional_edits.await.unwrap();
13615}
13616
13617// This used to crash
13618#[gpui::test]
13619async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
13620 init_test(cx, |_| {});
13621
13622 let buffer_text = indoc! {"
13623 fn main() {
13624 10.satu;
13625
13626 //
13627 // separate cursors so they open in different excerpts (manually reproducible)
13628 //
13629
13630 10.satu20;
13631 }
13632 "};
13633 let multibuffer_text_with_selections = indoc! {"
13634 fn main() {
13635 10.satuˇ;
13636
13637 //
13638
13639 //
13640
13641 10.satuˇ20;
13642 }
13643 "};
13644 let expected_multibuffer = indoc! {"
13645 fn main() {
13646 10.saturating_sub()ˇ;
13647
13648 //
13649
13650 //
13651
13652 10.saturating_sub()ˇ;
13653 }
13654 "};
13655
13656 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
13657 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
13658
13659 let fs = FakeFs::new(cx.executor());
13660 fs.insert_tree(
13661 path!("/a"),
13662 json!({
13663 "main.rs": buffer_text,
13664 }),
13665 )
13666 .await;
13667
13668 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13669 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13670 language_registry.add(rust_lang());
13671 let mut fake_servers = language_registry.register_fake_lsp(
13672 "Rust",
13673 FakeLspAdapter {
13674 capabilities: lsp::ServerCapabilities {
13675 completion_provider: Some(lsp::CompletionOptions {
13676 resolve_provider: None,
13677 ..lsp::CompletionOptions::default()
13678 }),
13679 ..lsp::ServerCapabilities::default()
13680 },
13681 ..FakeLspAdapter::default()
13682 },
13683 );
13684 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13685 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13686 let buffer = project
13687 .update(cx, |project, cx| {
13688 project.open_local_buffer(path!("/a/main.rs"), cx)
13689 })
13690 .await
13691 .unwrap();
13692
13693 let multi_buffer = cx.new(|cx| {
13694 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
13695 multi_buffer.push_excerpts(
13696 buffer.clone(),
13697 [ExcerptRange::new(0..first_excerpt_end)],
13698 cx,
13699 );
13700 multi_buffer.push_excerpts(
13701 buffer.clone(),
13702 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
13703 cx,
13704 );
13705 multi_buffer
13706 });
13707
13708 let editor = workspace
13709 .update(cx, |_, window, cx| {
13710 cx.new(|cx| {
13711 Editor::new(
13712 EditorMode::Full {
13713 scale_ui_elements_with_buffer_font_size: false,
13714 show_active_line_background: false,
13715 sized_by_content: false,
13716 },
13717 multi_buffer.clone(),
13718 Some(project.clone()),
13719 window,
13720 cx,
13721 )
13722 })
13723 })
13724 .unwrap();
13725
13726 let pane = workspace
13727 .update(cx, |workspace, _, _| workspace.active_pane().clone())
13728 .unwrap();
13729 pane.update_in(cx, |pane, window, cx| {
13730 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
13731 });
13732
13733 let fake_server = fake_servers.next().await.unwrap();
13734
13735 editor.update_in(cx, |editor, window, cx| {
13736 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13737 s.select_ranges([
13738 Point::new(1, 11)..Point::new(1, 11),
13739 Point::new(7, 11)..Point::new(7, 11),
13740 ])
13741 });
13742
13743 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
13744 });
13745
13746 editor.update_in(cx, |editor, window, cx| {
13747 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13748 });
13749
13750 fake_server
13751 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13752 let completion_item = lsp::CompletionItem {
13753 label: "saturating_sub()".into(),
13754 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
13755 lsp::InsertReplaceEdit {
13756 new_text: "saturating_sub()".to_owned(),
13757 insert: lsp::Range::new(
13758 lsp::Position::new(7, 7),
13759 lsp::Position::new(7, 11),
13760 ),
13761 replace: lsp::Range::new(
13762 lsp::Position::new(7, 7),
13763 lsp::Position::new(7, 13),
13764 ),
13765 },
13766 )),
13767 ..lsp::CompletionItem::default()
13768 };
13769
13770 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
13771 })
13772 .next()
13773 .await
13774 .unwrap();
13775
13776 cx.condition(&editor, |editor, _| editor.context_menu_visible())
13777 .await;
13778
13779 editor
13780 .update_in(cx, |editor, window, cx| {
13781 editor
13782 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13783 .unwrap()
13784 })
13785 .await
13786 .unwrap();
13787
13788 editor.update(cx, |editor, cx| {
13789 assert_text_with_selections(editor, expected_multibuffer, cx);
13790 })
13791}
13792
13793#[gpui::test]
13794async fn test_completion(cx: &mut TestAppContext) {
13795 init_test(cx, |_| {});
13796
13797 let mut cx = EditorLspTestContext::new_rust(
13798 lsp::ServerCapabilities {
13799 completion_provider: Some(lsp::CompletionOptions {
13800 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
13801 resolve_provider: Some(true),
13802 ..Default::default()
13803 }),
13804 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13805 ..Default::default()
13806 },
13807 cx,
13808 )
13809 .await;
13810 let counter = Arc::new(AtomicUsize::new(0));
13811
13812 cx.set_state(indoc! {"
13813 oneˇ
13814 two
13815 three
13816 "});
13817 cx.simulate_keystroke(".");
13818 handle_completion_request(
13819 indoc! {"
13820 one.|<>
13821 two
13822 three
13823 "},
13824 vec!["first_completion", "second_completion"],
13825 true,
13826 counter.clone(),
13827 &mut cx,
13828 )
13829 .await;
13830 cx.condition(|editor, _| editor.context_menu_visible())
13831 .await;
13832 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13833
13834 let _handler = handle_signature_help_request(
13835 &mut cx,
13836 lsp::SignatureHelp {
13837 signatures: vec![lsp::SignatureInformation {
13838 label: "test signature".to_string(),
13839 documentation: None,
13840 parameters: Some(vec![lsp::ParameterInformation {
13841 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
13842 documentation: None,
13843 }]),
13844 active_parameter: None,
13845 }],
13846 active_signature: None,
13847 active_parameter: None,
13848 },
13849 );
13850 cx.update_editor(|editor, window, cx| {
13851 assert!(
13852 !editor.signature_help_state.is_shown(),
13853 "No signature help was called for"
13854 );
13855 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13856 });
13857 cx.run_until_parked();
13858 cx.update_editor(|editor, _, _| {
13859 assert!(
13860 !editor.signature_help_state.is_shown(),
13861 "No signature help should be shown when completions menu is open"
13862 );
13863 });
13864
13865 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13866 editor.context_menu_next(&Default::default(), window, cx);
13867 editor
13868 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13869 .unwrap()
13870 });
13871 cx.assert_editor_state(indoc! {"
13872 one.second_completionˇ
13873 two
13874 three
13875 "});
13876
13877 handle_resolve_completion_request(
13878 &mut cx,
13879 Some(vec![
13880 (
13881 //This overlaps with the primary completion edit which is
13882 //misbehavior from the LSP spec, test that we filter it out
13883 indoc! {"
13884 one.second_ˇcompletion
13885 two
13886 threeˇ
13887 "},
13888 "overlapping additional edit",
13889 ),
13890 (
13891 indoc! {"
13892 one.second_completion
13893 two
13894 threeˇ
13895 "},
13896 "\nadditional edit",
13897 ),
13898 ]),
13899 )
13900 .await;
13901 apply_additional_edits.await.unwrap();
13902 cx.assert_editor_state(indoc! {"
13903 one.second_completionˇ
13904 two
13905 three
13906 additional edit
13907 "});
13908
13909 cx.set_state(indoc! {"
13910 one.second_completion
13911 twoˇ
13912 threeˇ
13913 additional edit
13914 "});
13915 cx.simulate_keystroke(" ");
13916 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
13917 cx.simulate_keystroke("s");
13918 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
13919
13920 cx.assert_editor_state(indoc! {"
13921 one.second_completion
13922 two sˇ
13923 three sˇ
13924 additional edit
13925 "});
13926 handle_completion_request(
13927 indoc! {"
13928 one.second_completion
13929 two s
13930 three <s|>
13931 additional edit
13932 "},
13933 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
13934 true,
13935 counter.clone(),
13936 &mut cx,
13937 )
13938 .await;
13939 cx.condition(|editor, _| editor.context_menu_visible())
13940 .await;
13941 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
13942
13943 cx.simulate_keystroke("i");
13944
13945 handle_completion_request(
13946 indoc! {"
13947 one.second_completion
13948 two si
13949 three <si|>
13950 additional edit
13951 "},
13952 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
13953 true,
13954 counter.clone(),
13955 &mut cx,
13956 )
13957 .await;
13958 cx.condition(|editor, _| editor.context_menu_visible())
13959 .await;
13960 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
13961
13962 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13963 editor
13964 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13965 .unwrap()
13966 });
13967 cx.assert_editor_state(indoc! {"
13968 one.second_completion
13969 two sixth_completionˇ
13970 three sixth_completionˇ
13971 additional edit
13972 "});
13973
13974 apply_additional_edits.await.unwrap();
13975
13976 update_test_language_settings(&mut cx, |settings| {
13977 settings.defaults.show_completions_on_input = Some(false);
13978 });
13979 cx.set_state("editorˇ");
13980 cx.simulate_keystroke(".");
13981 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
13982 cx.simulate_keystrokes("c l o");
13983 cx.assert_editor_state("editor.cloˇ");
13984 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
13985 cx.update_editor(|editor, window, cx| {
13986 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13987 });
13988 handle_completion_request(
13989 "editor.<clo|>",
13990 vec!["close", "clobber"],
13991 true,
13992 counter.clone(),
13993 &mut cx,
13994 )
13995 .await;
13996 cx.condition(|editor, _| editor.context_menu_visible())
13997 .await;
13998 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
13999
14000 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14001 editor
14002 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14003 .unwrap()
14004 });
14005 cx.assert_editor_state("editor.clobberˇ");
14006 handle_resolve_completion_request(&mut cx, None).await;
14007 apply_additional_edits.await.unwrap();
14008}
14009
14010#[gpui::test]
14011async fn test_completion_reuse(cx: &mut TestAppContext) {
14012 init_test(cx, |_| {});
14013
14014 let mut cx = EditorLspTestContext::new_rust(
14015 lsp::ServerCapabilities {
14016 completion_provider: Some(lsp::CompletionOptions {
14017 trigger_characters: Some(vec![".".to_string()]),
14018 ..Default::default()
14019 }),
14020 ..Default::default()
14021 },
14022 cx,
14023 )
14024 .await;
14025
14026 let counter = Arc::new(AtomicUsize::new(0));
14027 cx.set_state("objˇ");
14028 cx.simulate_keystroke(".");
14029
14030 // Initial completion request returns complete results
14031 let is_incomplete = false;
14032 handle_completion_request(
14033 "obj.|<>",
14034 vec!["a", "ab", "abc"],
14035 is_incomplete,
14036 counter.clone(),
14037 &mut cx,
14038 )
14039 .await;
14040 cx.run_until_parked();
14041 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14042 cx.assert_editor_state("obj.ˇ");
14043 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14044
14045 // Type "a" - filters existing completions
14046 cx.simulate_keystroke("a");
14047 cx.run_until_parked();
14048 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14049 cx.assert_editor_state("obj.aˇ");
14050 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14051
14052 // Type "b" - filters existing completions
14053 cx.simulate_keystroke("b");
14054 cx.run_until_parked();
14055 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14056 cx.assert_editor_state("obj.abˇ");
14057 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14058
14059 // Type "c" - filters existing completions
14060 cx.simulate_keystroke("c");
14061 cx.run_until_parked();
14062 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14063 cx.assert_editor_state("obj.abcˇ");
14064 check_displayed_completions(vec!["abc"], &mut cx);
14065
14066 // Backspace to delete "c" - filters existing completions
14067 cx.update_editor(|editor, window, cx| {
14068 editor.backspace(&Backspace, window, cx);
14069 });
14070 cx.run_until_parked();
14071 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14072 cx.assert_editor_state("obj.abˇ");
14073 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14074
14075 // Moving cursor to the left dismisses menu.
14076 cx.update_editor(|editor, window, cx| {
14077 editor.move_left(&MoveLeft, window, cx);
14078 });
14079 cx.run_until_parked();
14080 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14081 cx.assert_editor_state("obj.aˇb");
14082 cx.update_editor(|editor, _, _| {
14083 assert_eq!(editor.context_menu_visible(), false);
14084 });
14085
14086 // Type "b" - new request
14087 cx.simulate_keystroke("b");
14088 let is_incomplete = false;
14089 handle_completion_request(
14090 "obj.<ab|>a",
14091 vec!["ab", "abc"],
14092 is_incomplete,
14093 counter.clone(),
14094 &mut cx,
14095 )
14096 .await;
14097 cx.run_until_parked();
14098 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14099 cx.assert_editor_state("obj.abˇb");
14100 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14101
14102 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
14103 cx.update_editor(|editor, window, cx| {
14104 editor.backspace(&Backspace, window, cx);
14105 });
14106 let is_incomplete = false;
14107 handle_completion_request(
14108 "obj.<a|>b",
14109 vec!["a", "ab", "abc"],
14110 is_incomplete,
14111 counter.clone(),
14112 &mut cx,
14113 )
14114 .await;
14115 cx.run_until_parked();
14116 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14117 cx.assert_editor_state("obj.aˇb");
14118 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14119
14120 // Backspace to delete "a" - dismisses menu.
14121 cx.update_editor(|editor, window, cx| {
14122 editor.backspace(&Backspace, window, cx);
14123 });
14124 cx.run_until_parked();
14125 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14126 cx.assert_editor_state("obj.ˇb");
14127 cx.update_editor(|editor, _, _| {
14128 assert_eq!(editor.context_menu_visible(), false);
14129 });
14130}
14131
14132#[gpui::test]
14133async fn test_word_completion(cx: &mut TestAppContext) {
14134 let lsp_fetch_timeout_ms = 10;
14135 init_test(cx, |language_settings| {
14136 language_settings.defaults.completions = Some(CompletionSettings {
14137 words: WordsCompletionMode::Fallback,
14138 words_min_length: 0,
14139 lsp: true,
14140 lsp_fetch_timeout_ms: 10,
14141 lsp_insert_mode: LspInsertMode::Insert,
14142 });
14143 });
14144
14145 let mut cx = EditorLspTestContext::new_rust(
14146 lsp::ServerCapabilities {
14147 completion_provider: Some(lsp::CompletionOptions {
14148 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14149 ..lsp::CompletionOptions::default()
14150 }),
14151 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14152 ..lsp::ServerCapabilities::default()
14153 },
14154 cx,
14155 )
14156 .await;
14157
14158 let throttle_completions = Arc::new(AtomicBool::new(false));
14159
14160 let lsp_throttle_completions = throttle_completions.clone();
14161 let _completion_requests_handler =
14162 cx.lsp
14163 .server
14164 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
14165 let lsp_throttle_completions = lsp_throttle_completions.clone();
14166 let cx = cx.clone();
14167 async move {
14168 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
14169 cx.background_executor()
14170 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
14171 .await;
14172 }
14173 Ok(Some(lsp::CompletionResponse::Array(vec![
14174 lsp::CompletionItem {
14175 label: "first".into(),
14176 ..lsp::CompletionItem::default()
14177 },
14178 lsp::CompletionItem {
14179 label: "last".into(),
14180 ..lsp::CompletionItem::default()
14181 },
14182 ])))
14183 }
14184 });
14185
14186 cx.set_state(indoc! {"
14187 oneˇ
14188 two
14189 three
14190 "});
14191 cx.simulate_keystroke(".");
14192 cx.executor().run_until_parked();
14193 cx.condition(|editor, _| editor.context_menu_visible())
14194 .await;
14195 cx.update_editor(|editor, window, cx| {
14196 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14197 {
14198 assert_eq!(
14199 completion_menu_entries(menu),
14200 &["first", "last"],
14201 "When LSP server is fast to reply, no fallback word completions are used"
14202 );
14203 } else {
14204 panic!("expected completion menu to be open");
14205 }
14206 editor.cancel(&Cancel, window, cx);
14207 });
14208 cx.executor().run_until_parked();
14209 cx.condition(|editor, _| !editor.context_menu_visible())
14210 .await;
14211
14212 throttle_completions.store(true, atomic::Ordering::Release);
14213 cx.simulate_keystroke(".");
14214 cx.executor()
14215 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
14216 cx.executor().run_until_parked();
14217 cx.condition(|editor, _| editor.context_menu_visible())
14218 .await;
14219 cx.update_editor(|editor, _, _| {
14220 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14221 {
14222 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
14223 "When LSP server is slow, document words can be shown instead, if configured accordingly");
14224 } else {
14225 panic!("expected completion menu to be open");
14226 }
14227 });
14228}
14229
14230#[gpui::test]
14231async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
14232 init_test(cx, |language_settings| {
14233 language_settings.defaults.completions = Some(CompletionSettings {
14234 words: WordsCompletionMode::Enabled,
14235 words_min_length: 0,
14236 lsp: true,
14237 lsp_fetch_timeout_ms: 0,
14238 lsp_insert_mode: LspInsertMode::Insert,
14239 });
14240 });
14241
14242 let mut cx = EditorLspTestContext::new_rust(
14243 lsp::ServerCapabilities {
14244 completion_provider: Some(lsp::CompletionOptions {
14245 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14246 ..lsp::CompletionOptions::default()
14247 }),
14248 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14249 ..lsp::ServerCapabilities::default()
14250 },
14251 cx,
14252 )
14253 .await;
14254
14255 let _completion_requests_handler =
14256 cx.lsp
14257 .server
14258 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14259 Ok(Some(lsp::CompletionResponse::Array(vec![
14260 lsp::CompletionItem {
14261 label: "first".into(),
14262 ..lsp::CompletionItem::default()
14263 },
14264 lsp::CompletionItem {
14265 label: "last".into(),
14266 ..lsp::CompletionItem::default()
14267 },
14268 ])))
14269 });
14270
14271 cx.set_state(indoc! {"ˇ
14272 first
14273 last
14274 second
14275 "});
14276 cx.simulate_keystroke(".");
14277 cx.executor().run_until_parked();
14278 cx.condition(|editor, _| editor.context_menu_visible())
14279 .await;
14280 cx.update_editor(|editor, _, _| {
14281 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14282 {
14283 assert_eq!(
14284 completion_menu_entries(menu),
14285 &["first", "last", "second"],
14286 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
14287 );
14288 } else {
14289 panic!("expected completion menu to be open");
14290 }
14291 });
14292}
14293
14294#[gpui::test]
14295async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
14296 init_test(cx, |language_settings| {
14297 language_settings.defaults.completions = Some(CompletionSettings {
14298 words: WordsCompletionMode::Disabled,
14299 words_min_length: 0,
14300 lsp: true,
14301 lsp_fetch_timeout_ms: 0,
14302 lsp_insert_mode: LspInsertMode::Insert,
14303 });
14304 });
14305
14306 let mut cx = EditorLspTestContext::new_rust(
14307 lsp::ServerCapabilities {
14308 completion_provider: Some(lsp::CompletionOptions {
14309 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14310 ..lsp::CompletionOptions::default()
14311 }),
14312 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14313 ..lsp::ServerCapabilities::default()
14314 },
14315 cx,
14316 )
14317 .await;
14318
14319 let _completion_requests_handler =
14320 cx.lsp
14321 .server
14322 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14323 panic!("LSP completions should not be queried when dealing with word completions")
14324 });
14325
14326 cx.set_state(indoc! {"ˇ
14327 first
14328 last
14329 second
14330 "});
14331 cx.update_editor(|editor, window, cx| {
14332 editor.show_word_completions(&ShowWordCompletions, window, cx);
14333 });
14334 cx.executor().run_until_parked();
14335 cx.condition(|editor, _| editor.context_menu_visible())
14336 .await;
14337 cx.update_editor(|editor, _, _| {
14338 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14339 {
14340 assert_eq!(
14341 completion_menu_entries(menu),
14342 &["first", "last", "second"],
14343 "`ShowWordCompletions` action should show word completions"
14344 );
14345 } else {
14346 panic!("expected completion menu to be open");
14347 }
14348 });
14349
14350 cx.simulate_keystroke("l");
14351 cx.executor().run_until_parked();
14352 cx.condition(|editor, _| editor.context_menu_visible())
14353 .await;
14354 cx.update_editor(|editor, _, _| {
14355 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14356 {
14357 assert_eq!(
14358 completion_menu_entries(menu),
14359 &["last"],
14360 "After showing word completions, further editing should filter them and not query the LSP"
14361 );
14362 } else {
14363 panic!("expected completion menu to be open");
14364 }
14365 });
14366}
14367
14368#[gpui::test]
14369async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
14370 init_test(cx, |language_settings| {
14371 language_settings.defaults.completions = Some(CompletionSettings {
14372 words: WordsCompletionMode::Fallback,
14373 words_min_length: 0,
14374 lsp: false,
14375 lsp_fetch_timeout_ms: 0,
14376 lsp_insert_mode: LspInsertMode::Insert,
14377 });
14378 });
14379
14380 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14381
14382 cx.set_state(indoc! {"ˇ
14383 0_usize
14384 let
14385 33
14386 4.5f32
14387 "});
14388 cx.update_editor(|editor, window, cx| {
14389 editor.show_completions(&ShowCompletions::default(), window, cx);
14390 });
14391 cx.executor().run_until_parked();
14392 cx.condition(|editor, _| editor.context_menu_visible())
14393 .await;
14394 cx.update_editor(|editor, window, cx| {
14395 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14396 {
14397 assert_eq!(
14398 completion_menu_entries(menu),
14399 &["let"],
14400 "With no digits in the completion query, no digits should be in the word completions"
14401 );
14402 } else {
14403 panic!("expected completion menu to be open");
14404 }
14405 editor.cancel(&Cancel, window, cx);
14406 });
14407
14408 cx.set_state(indoc! {"3ˇ
14409 0_usize
14410 let
14411 3
14412 33.35f32
14413 "});
14414 cx.update_editor(|editor, window, cx| {
14415 editor.show_completions(&ShowCompletions::default(), window, cx);
14416 });
14417 cx.executor().run_until_parked();
14418 cx.condition(|editor, _| editor.context_menu_visible())
14419 .await;
14420 cx.update_editor(|editor, _, _| {
14421 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14422 {
14423 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
14424 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
14425 } else {
14426 panic!("expected completion menu to be open");
14427 }
14428 });
14429}
14430
14431#[gpui::test]
14432async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
14433 init_test(cx, |language_settings| {
14434 language_settings.defaults.completions = Some(CompletionSettings {
14435 words: WordsCompletionMode::Enabled,
14436 words_min_length: 3,
14437 lsp: true,
14438 lsp_fetch_timeout_ms: 0,
14439 lsp_insert_mode: LspInsertMode::Insert,
14440 });
14441 });
14442
14443 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14444 cx.set_state(indoc! {"ˇ
14445 wow
14446 wowen
14447 wowser
14448 "});
14449 cx.simulate_keystroke("w");
14450 cx.executor().run_until_parked();
14451 cx.update_editor(|editor, _, _| {
14452 if editor.context_menu.borrow_mut().is_some() {
14453 panic!(
14454 "expected completion menu to be hidden, as words completion threshold is not met"
14455 );
14456 }
14457 });
14458
14459 cx.update_editor(|editor, window, cx| {
14460 editor.show_word_completions(&ShowWordCompletions, window, cx);
14461 });
14462 cx.executor().run_until_parked();
14463 cx.update_editor(|editor, window, cx| {
14464 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14465 {
14466 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");
14467 } else {
14468 panic!("expected completion menu to be open after the word completions are called with an action");
14469 }
14470
14471 editor.cancel(&Cancel, window, cx);
14472 });
14473 cx.update_editor(|editor, _, _| {
14474 if editor.context_menu.borrow_mut().is_some() {
14475 panic!("expected completion menu to be hidden after canceling");
14476 }
14477 });
14478
14479 cx.simulate_keystroke("o");
14480 cx.executor().run_until_parked();
14481 cx.update_editor(|editor, _, _| {
14482 if editor.context_menu.borrow_mut().is_some() {
14483 panic!(
14484 "expected completion menu to be hidden, as words completion threshold is not met still"
14485 );
14486 }
14487 });
14488
14489 cx.simulate_keystroke("w");
14490 cx.executor().run_until_parked();
14491 cx.update_editor(|editor, _, _| {
14492 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14493 {
14494 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
14495 } else {
14496 panic!("expected completion menu to be open after the word completions threshold is met");
14497 }
14498 });
14499}
14500
14501#[gpui::test]
14502async fn test_word_completions_disabled(cx: &mut TestAppContext) {
14503 init_test(cx, |language_settings| {
14504 language_settings.defaults.completions = Some(CompletionSettings {
14505 words: WordsCompletionMode::Enabled,
14506 words_min_length: 0,
14507 lsp: true,
14508 lsp_fetch_timeout_ms: 0,
14509 lsp_insert_mode: LspInsertMode::Insert,
14510 });
14511 });
14512
14513 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14514 cx.update_editor(|editor, _, _| {
14515 editor.disable_word_completions();
14516 });
14517 cx.set_state(indoc! {"ˇ
14518 wow
14519 wowen
14520 wowser
14521 "});
14522 cx.simulate_keystroke("w");
14523 cx.executor().run_until_parked();
14524 cx.update_editor(|editor, _, _| {
14525 if editor.context_menu.borrow_mut().is_some() {
14526 panic!(
14527 "expected completion menu to be hidden, as words completion are disabled for this editor"
14528 );
14529 }
14530 });
14531
14532 cx.update_editor(|editor, window, cx| {
14533 editor.show_word_completions(&ShowWordCompletions, window, cx);
14534 });
14535 cx.executor().run_until_parked();
14536 cx.update_editor(|editor, _, _| {
14537 if editor.context_menu.borrow_mut().is_some() {
14538 panic!(
14539 "expected completion menu to be hidden even if called for explicitly, as words completion are disabled for this editor"
14540 );
14541 }
14542 });
14543}
14544
14545fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
14546 let position = || lsp::Position {
14547 line: params.text_document_position.position.line,
14548 character: params.text_document_position.position.character,
14549 };
14550 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14551 range: lsp::Range {
14552 start: position(),
14553 end: position(),
14554 },
14555 new_text: text.to_string(),
14556 }))
14557}
14558
14559#[gpui::test]
14560async fn test_multiline_completion(cx: &mut TestAppContext) {
14561 init_test(cx, |_| {});
14562
14563 let fs = FakeFs::new(cx.executor());
14564 fs.insert_tree(
14565 path!("/a"),
14566 json!({
14567 "main.ts": "a",
14568 }),
14569 )
14570 .await;
14571
14572 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14573 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14574 let typescript_language = Arc::new(Language::new(
14575 LanguageConfig {
14576 name: "TypeScript".into(),
14577 matcher: LanguageMatcher {
14578 path_suffixes: vec!["ts".to_string()],
14579 ..LanguageMatcher::default()
14580 },
14581 line_comments: vec!["// ".into()],
14582 ..LanguageConfig::default()
14583 },
14584 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
14585 ));
14586 language_registry.add(typescript_language.clone());
14587 let mut fake_servers = language_registry.register_fake_lsp(
14588 "TypeScript",
14589 FakeLspAdapter {
14590 capabilities: lsp::ServerCapabilities {
14591 completion_provider: Some(lsp::CompletionOptions {
14592 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14593 ..lsp::CompletionOptions::default()
14594 }),
14595 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14596 ..lsp::ServerCapabilities::default()
14597 },
14598 // Emulate vtsls label generation
14599 label_for_completion: Some(Box::new(|item, _| {
14600 let text = if let Some(description) = item
14601 .label_details
14602 .as_ref()
14603 .and_then(|label_details| label_details.description.as_ref())
14604 {
14605 format!("{} {}", item.label, description)
14606 } else if let Some(detail) = &item.detail {
14607 format!("{} {}", item.label, detail)
14608 } else {
14609 item.label.clone()
14610 };
14611 let len = text.len();
14612 Some(language::CodeLabel {
14613 text,
14614 runs: Vec::new(),
14615 filter_range: 0..len,
14616 })
14617 })),
14618 ..FakeLspAdapter::default()
14619 },
14620 );
14621 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14622 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14623 let worktree_id = workspace
14624 .update(cx, |workspace, _window, cx| {
14625 workspace.project().update(cx, |project, cx| {
14626 project.worktrees(cx).next().unwrap().read(cx).id()
14627 })
14628 })
14629 .unwrap();
14630 let _buffer = project
14631 .update(cx, |project, cx| {
14632 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
14633 })
14634 .await
14635 .unwrap();
14636 let editor = workspace
14637 .update(cx, |workspace, window, cx| {
14638 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
14639 })
14640 .unwrap()
14641 .await
14642 .unwrap()
14643 .downcast::<Editor>()
14644 .unwrap();
14645 let fake_server = fake_servers.next().await.unwrap();
14646
14647 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
14648 let multiline_label_2 = "a\nb\nc\n";
14649 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
14650 let multiline_description = "d\ne\nf\n";
14651 let multiline_detail_2 = "g\nh\ni\n";
14652
14653 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
14654 move |params, _| async move {
14655 Ok(Some(lsp::CompletionResponse::Array(vec![
14656 lsp::CompletionItem {
14657 label: multiline_label.to_string(),
14658 text_edit: gen_text_edit(¶ms, "new_text_1"),
14659 ..lsp::CompletionItem::default()
14660 },
14661 lsp::CompletionItem {
14662 label: "single line label 1".to_string(),
14663 detail: Some(multiline_detail.to_string()),
14664 text_edit: gen_text_edit(¶ms, "new_text_2"),
14665 ..lsp::CompletionItem::default()
14666 },
14667 lsp::CompletionItem {
14668 label: "single line label 2".to_string(),
14669 label_details: Some(lsp::CompletionItemLabelDetails {
14670 description: Some(multiline_description.to_string()),
14671 detail: None,
14672 }),
14673 text_edit: gen_text_edit(¶ms, "new_text_2"),
14674 ..lsp::CompletionItem::default()
14675 },
14676 lsp::CompletionItem {
14677 label: multiline_label_2.to_string(),
14678 detail: Some(multiline_detail_2.to_string()),
14679 text_edit: gen_text_edit(¶ms, "new_text_3"),
14680 ..lsp::CompletionItem::default()
14681 },
14682 lsp::CompletionItem {
14683 label: "Label with many spaces and \t but without newlines".to_string(),
14684 detail: Some(
14685 "Details with many spaces and \t but without newlines".to_string(),
14686 ),
14687 text_edit: gen_text_edit(¶ms, "new_text_4"),
14688 ..lsp::CompletionItem::default()
14689 },
14690 ])))
14691 },
14692 );
14693
14694 editor.update_in(cx, |editor, window, cx| {
14695 cx.focus_self(window);
14696 editor.move_to_end(&MoveToEnd, window, cx);
14697 editor.handle_input(".", window, cx);
14698 });
14699 cx.run_until_parked();
14700 completion_handle.next().await.unwrap();
14701
14702 editor.update(cx, |editor, _| {
14703 assert!(editor.context_menu_visible());
14704 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14705 {
14706 let completion_labels = menu
14707 .completions
14708 .borrow()
14709 .iter()
14710 .map(|c| c.label.text.clone())
14711 .collect::<Vec<_>>();
14712 assert_eq!(
14713 completion_labels,
14714 &[
14715 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
14716 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
14717 "single line label 2 d e f ",
14718 "a b c g h i ",
14719 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
14720 ],
14721 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
14722 );
14723
14724 for completion in menu
14725 .completions
14726 .borrow()
14727 .iter() {
14728 assert_eq!(
14729 completion.label.filter_range,
14730 0..completion.label.text.len(),
14731 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
14732 );
14733 }
14734 } else {
14735 panic!("expected completion menu to be open");
14736 }
14737 });
14738}
14739
14740#[gpui::test]
14741async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
14742 init_test(cx, |_| {});
14743 let mut cx = EditorLspTestContext::new_rust(
14744 lsp::ServerCapabilities {
14745 completion_provider: Some(lsp::CompletionOptions {
14746 trigger_characters: Some(vec![".".to_string()]),
14747 ..Default::default()
14748 }),
14749 ..Default::default()
14750 },
14751 cx,
14752 )
14753 .await;
14754 cx.lsp
14755 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14756 Ok(Some(lsp::CompletionResponse::Array(vec![
14757 lsp::CompletionItem {
14758 label: "first".into(),
14759 ..Default::default()
14760 },
14761 lsp::CompletionItem {
14762 label: "last".into(),
14763 ..Default::default()
14764 },
14765 ])))
14766 });
14767 cx.set_state("variableˇ");
14768 cx.simulate_keystroke(".");
14769 cx.executor().run_until_parked();
14770
14771 cx.update_editor(|editor, _, _| {
14772 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14773 {
14774 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
14775 } else {
14776 panic!("expected completion menu to be open");
14777 }
14778 });
14779
14780 cx.update_editor(|editor, window, cx| {
14781 editor.move_page_down(&MovePageDown::default(), window, cx);
14782 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14783 {
14784 assert!(
14785 menu.selected_item == 1,
14786 "expected PageDown to select the last item from the context menu"
14787 );
14788 } else {
14789 panic!("expected completion menu to stay open after PageDown");
14790 }
14791 });
14792
14793 cx.update_editor(|editor, window, cx| {
14794 editor.move_page_up(&MovePageUp::default(), window, cx);
14795 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14796 {
14797 assert!(
14798 menu.selected_item == 0,
14799 "expected PageUp to select the first item from the context menu"
14800 );
14801 } else {
14802 panic!("expected completion menu to stay open after PageUp");
14803 }
14804 });
14805}
14806
14807#[gpui::test]
14808async fn test_as_is_completions(cx: &mut TestAppContext) {
14809 init_test(cx, |_| {});
14810 let mut cx = EditorLspTestContext::new_rust(
14811 lsp::ServerCapabilities {
14812 completion_provider: Some(lsp::CompletionOptions {
14813 ..Default::default()
14814 }),
14815 ..Default::default()
14816 },
14817 cx,
14818 )
14819 .await;
14820 cx.lsp
14821 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14822 Ok(Some(lsp::CompletionResponse::Array(vec![
14823 lsp::CompletionItem {
14824 label: "unsafe".into(),
14825 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14826 range: lsp::Range {
14827 start: lsp::Position {
14828 line: 1,
14829 character: 2,
14830 },
14831 end: lsp::Position {
14832 line: 1,
14833 character: 3,
14834 },
14835 },
14836 new_text: "unsafe".to_string(),
14837 })),
14838 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
14839 ..Default::default()
14840 },
14841 ])))
14842 });
14843 cx.set_state("fn a() {}\n nˇ");
14844 cx.executor().run_until_parked();
14845 cx.update_editor(|editor, window, cx| {
14846 editor.show_completions(
14847 &ShowCompletions {
14848 trigger: Some("\n".into()),
14849 },
14850 window,
14851 cx,
14852 );
14853 });
14854 cx.executor().run_until_parked();
14855
14856 cx.update_editor(|editor, window, cx| {
14857 editor.confirm_completion(&Default::default(), window, cx)
14858 });
14859 cx.executor().run_until_parked();
14860 cx.assert_editor_state("fn a() {}\n unsafeˇ");
14861}
14862
14863#[gpui::test]
14864async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
14865 init_test(cx, |_| {});
14866 let language =
14867 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
14868 let mut cx = EditorLspTestContext::new(
14869 language,
14870 lsp::ServerCapabilities {
14871 completion_provider: Some(lsp::CompletionOptions {
14872 ..lsp::CompletionOptions::default()
14873 }),
14874 ..lsp::ServerCapabilities::default()
14875 },
14876 cx,
14877 )
14878 .await;
14879
14880 cx.set_state(
14881 "#ifndef BAR_H
14882#define BAR_H
14883
14884#include <stdbool.h>
14885
14886int fn_branch(bool do_branch1, bool do_branch2);
14887
14888#endif // BAR_H
14889ˇ",
14890 );
14891 cx.executor().run_until_parked();
14892 cx.update_editor(|editor, window, cx| {
14893 editor.handle_input("#", window, cx);
14894 });
14895 cx.executor().run_until_parked();
14896 cx.update_editor(|editor, window, cx| {
14897 editor.handle_input("i", window, cx);
14898 });
14899 cx.executor().run_until_parked();
14900 cx.update_editor(|editor, window, cx| {
14901 editor.handle_input("n", window, cx);
14902 });
14903 cx.executor().run_until_parked();
14904 cx.assert_editor_state(
14905 "#ifndef BAR_H
14906#define BAR_H
14907
14908#include <stdbool.h>
14909
14910int fn_branch(bool do_branch1, bool do_branch2);
14911
14912#endif // BAR_H
14913#inˇ",
14914 );
14915
14916 cx.lsp
14917 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14918 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
14919 is_incomplete: false,
14920 item_defaults: None,
14921 items: vec![lsp::CompletionItem {
14922 kind: Some(lsp::CompletionItemKind::SNIPPET),
14923 label_details: Some(lsp::CompletionItemLabelDetails {
14924 detail: Some("header".to_string()),
14925 description: None,
14926 }),
14927 label: " include".to_string(),
14928 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14929 range: lsp::Range {
14930 start: lsp::Position {
14931 line: 8,
14932 character: 1,
14933 },
14934 end: lsp::Position {
14935 line: 8,
14936 character: 1,
14937 },
14938 },
14939 new_text: "include \"$0\"".to_string(),
14940 })),
14941 sort_text: Some("40b67681include".to_string()),
14942 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
14943 filter_text: Some("include".to_string()),
14944 insert_text: Some("include \"$0\"".to_string()),
14945 ..lsp::CompletionItem::default()
14946 }],
14947 })))
14948 });
14949 cx.update_editor(|editor, window, cx| {
14950 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14951 });
14952 cx.executor().run_until_parked();
14953 cx.update_editor(|editor, window, cx| {
14954 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
14955 });
14956 cx.executor().run_until_parked();
14957 cx.assert_editor_state(
14958 "#ifndef BAR_H
14959#define BAR_H
14960
14961#include <stdbool.h>
14962
14963int fn_branch(bool do_branch1, bool do_branch2);
14964
14965#endif // BAR_H
14966#include \"ˇ\"",
14967 );
14968
14969 cx.lsp
14970 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14971 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
14972 is_incomplete: true,
14973 item_defaults: None,
14974 items: vec![lsp::CompletionItem {
14975 kind: Some(lsp::CompletionItemKind::FILE),
14976 label: "AGL/".to_string(),
14977 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14978 range: lsp::Range {
14979 start: lsp::Position {
14980 line: 8,
14981 character: 10,
14982 },
14983 end: lsp::Position {
14984 line: 8,
14985 character: 11,
14986 },
14987 },
14988 new_text: "AGL/".to_string(),
14989 })),
14990 sort_text: Some("40b67681AGL/".to_string()),
14991 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
14992 filter_text: Some("AGL/".to_string()),
14993 insert_text: Some("AGL/".to_string()),
14994 ..lsp::CompletionItem::default()
14995 }],
14996 })))
14997 });
14998 cx.update_editor(|editor, window, cx| {
14999 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15000 });
15001 cx.executor().run_until_parked();
15002 cx.update_editor(|editor, window, cx| {
15003 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15004 });
15005 cx.executor().run_until_parked();
15006 cx.assert_editor_state(
15007 r##"#ifndef BAR_H
15008#define BAR_H
15009
15010#include <stdbool.h>
15011
15012int fn_branch(bool do_branch1, bool do_branch2);
15013
15014#endif // BAR_H
15015#include "AGL/ˇ"##,
15016 );
15017
15018 cx.update_editor(|editor, window, cx| {
15019 editor.handle_input("\"", window, cx);
15020 });
15021 cx.executor().run_until_parked();
15022 cx.assert_editor_state(
15023 r##"#ifndef BAR_H
15024#define BAR_H
15025
15026#include <stdbool.h>
15027
15028int fn_branch(bool do_branch1, bool do_branch2);
15029
15030#endif // BAR_H
15031#include "AGL/"ˇ"##,
15032 );
15033}
15034
15035#[gpui::test]
15036async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
15037 init_test(cx, |_| {});
15038
15039 let mut cx = EditorLspTestContext::new_rust(
15040 lsp::ServerCapabilities {
15041 completion_provider: Some(lsp::CompletionOptions {
15042 trigger_characters: Some(vec![".".to_string()]),
15043 resolve_provider: Some(true),
15044 ..Default::default()
15045 }),
15046 ..Default::default()
15047 },
15048 cx,
15049 )
15050 .await;
15051
15052 cx.set_state("fn main() { let a = 2ˇ; }");
15053 cx.simulate_keystroke(".");
15054 let completion_item = lsp::CompletionItem {
15055 label: "Some".into(),
15056 kind: Some(lsp::CompletionItemKind::SNIPPET),
15057 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15058 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15059 kind: lsp::MarkupKind::Markdown,
15060 value: "```rust\nSome(2)\n```".to_string(),
15061 })),
15062 deprecated: Some(false),
15063 sort_text: Some("Some".to_string()),
15064 filter_text: Some("Some".to_string()),
15065 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15066 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15067 range: lsp::Range {
15068 start: lsp::Position {
15069 line: 0,
15070 character: 22,
15071 },
15072 end: lsp::Position {
15073 line: 0,
15074 character: 22,
15075 },
15076 },
15077 new_text: "Some(2)".to_string(),
15078 })),
15079 additional_text_edits: Some(vec![lsp::TextEdit {
15080 range: lsp::Range {
15081 start: lsp::Position {
15082 line: 0,
15083 character: 20,
15084 },
15085 end: lsp::Position {
15086 line: 0,
15087 character: 22,
15088 },
15089 },
15090 new_text: "".to_string(),
15091 }]),
15092 ..Default::default()
15093 };
15094
15095 let closure_completion_item = completion_item.clone();
15096 let counter = Arc::new(AtomicUsize::new(0));
15097 let counter_clone = counter.clone();
15098 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15099 let task_completion_item = closure_completion_item.clone();
15100 counter_clone.fetch_add(1, atomic::Ordering::Release);
15101 async move {
15102 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15103 is_incomplete: true,
15104 item_defaults: None,
15105 items: vec![task_completion_item],
15106 })))
15107 }
15108 });
15109
15110 cx.condition(|editor, _| editor.context_menu_visible())
15111 .await;
15112 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
15113 assert!(request.next().await.is_some());
15114 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15115
15116 cx.simulate_keystrokes("S o m");
15117 cx.condition(|editor, _| editor.context_menu_visible())
15118 .await;
15119 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
15120 assert!(request.next().await.is_some());
15121 assert!(request.next().await.is_some());
15122 assert!(request.next().await.is_some());
15123 request.close();
15124 assert!(request.next().await.is_none());
15125 assert_eq!(
15126 counter.load(atomic::Ordering::Acquire),
15127 4,
15128 "With the completions menu open, only one LSP request should happen per input"
15129 );
15130}
15131
15132#[gpui::test]
15133async fn test_toggle_comment(cx: &mut TestAppContext) {
15134 init_test(cx, |_| {});
15135 let mut cx = EditorTestContext::new(cx).await;
15136 let language = Arc::new(Language::new(
15137 LanguageConfig {
15138 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15139 ..Default::default()
15140 },
15141 Some(tree_sitter_rust::LANGUAGE.into()),
15142 ));
15143 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15144
15145 // If multiple selections intersect a line, the line is only toggled once.
15146 cx.set_state(indoc! {"
15147 fn a() {
15148 «//b();
15149 ˇ»// «c();
15150 //ˇ» d();
15151 }
15152 "});
15153
15154 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15155
15156 cx.assert_editor_state(indoc! {"
15157 fn a() {
15158 «b();
15159 c();
15160 ˇ» d();
15161 }
15162 "});
15163
15164 // The comment prefix is inserted at the same column for every line in a
15165 // selection.
15166 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15167
15168 cx.assert_editor_state(indoc! {"
15169 fn a() {
15170 // «b();
15171 // c();
15172 ˇ»// d();
15173 }
15174 "});
15175
15176 // If a selection ends at the beginning of a line, that line is not toggled.
15177 cx.set_selections_state(indoc! {"
15178 fn a() {
15179 // b();
15180 «// c();
15181 ˇ» // d();
15182 }
15183 "});
15184
15185 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15186
15187 cx.assert_editor_state(indoc! {"
15188 fn a() {
15189 // b();
15190 «c();
15191 ˇ» // d();
15192 }
15193 "});
15194
15195 // If a selection span a single line and is empty, the line is toggled.
15196 cx.set_state(indoc! {"
15197 fn a() {
15198 a();
15199 b();
15200 ˇ
15201 }
15202 "});
15203
15204 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15205
15206 cx.assert_editor_state(indoc! {"
15207 fn a() {
15208 a();
15209 b();
15210 //•ˇ
15211 }
15212 "});
15213
15214 // If a selection span multiple lines, empty lines are not toggled.
15215 cx.set_state(indoc! {"
15216 fn a() {
15217 «a();
15218
15219 c();ˇ»
15220 }
15221 "});
15222
15223 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15224
15225 cx.assert_editor_state(indoc! {"
15226 fn a() {
15227 // «a();
15228
15229 // c();ˇ»
15230 }
15231 "});
15232
15233 // If a selection includes multiple comment prefixes, all lines are uncommented.
15234 cx.set_state(indoc! {"
15235 fn a() {
15236 «// a();
15237 /// b();
15238 //! c();ˇ»
15239 }
15240 "});
15241
15242 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15243
15244 cx.assert_editor_state(indoc! {"
15245 fn a() {
15246 «a();
15247 b();
15248 c();ˇ»
15249 }
15250 "});
15251}
15252
15253#[gpui::test]
15254async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
15255 init_test(cx, |_| {});
15256 let mut cx = EditorTestContext::new(cx).await;
15257 let language = Arc::new(Language::new(
15258 LanguageConfig {
15259 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15260 ..Default::default()
15261 },
15262 Some(tree_sitter_rust::LANGUAGE.into()),
15263 ));
15264 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15265
15266 let toggle_comments = &ToggleComments {
15267 advance_downwards: false,
15268 ignore_indent: true,
15269 };
15270
15271 // If multiple selections intersect a line, the line is only toggled once.
15272 cx.set_state(indoc! {"
15273 fn a() {
15274 // «b();
15275 // c();
15276 // ˇ» d();
15277 }
15278 "});
15279
15280 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15281
15282 cx.assert_editor_state(indoc! {"
15283 fn a() {
15284 «b();
15285 c();
15286 ˇ» d();
15287 }
15288 "});
15289
15290 // The comment prefix is inserted at the beginning of each line
15291 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15292
15293 cx.assert_editor_state(indoc! {"
15294 fn a() {
15295 // «b();
15296 // c();
15297 // ˇ» d();
15298 }
15299 "});
15300
15301 // If a selection ends at the beginning of a line, that line is not toggled.
15302 cx.set_selections_state(indoc! {"
15303 fn a() {
15304 // b();
15305 // «c();
15306 ˇ»// d();
15307 }
15308 "});
15309
15310 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15311
15312 cx.assert_editor_state(indoc! {"
15313 fn a() {
15314 // b();
15315 «c();
15316 ˇ»// d();
15317 }
15318 "});
15319
15320 // If a selection span a single line and is empty, the line is toggled.
15321 cx.set_state(indoc! {"
15322 fn a() {
15323 a();
15324 b();
15325 ˇ
15326 }
15327 "});
15328
15329 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15330
15331 cx.assert_editor_state(indoc! {"
15332 fn a() {
15333 a();
15334 b();
15335 //ˇ
15336 }
15337 "});
15338
15339 // If a selection span multiple lines, empty lines are not toggled.
15340 cx.set_state(indoc! {"
15341 fn a() {
15342 «a();
15343
15344 c();ˇ»
15345 }
15346 "});
15347
15348 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15349
15350 cx.assert_editor_state(indoc! {"
15351 fn a() {
15352 // «a();
15353
15354 // c();ˇ»
15355 }
15356 "});
15357
15358 // If a selection includes multiple comment prefixes, all lines are uncommented.
15359 cx.set_state(indoc! {"
15360 fn a() {
15361 // «a();
15362 /// b();
15363 //! c();ˇ»
15364 }
15365 "});
15366
15367 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15368
15369 cx.assert_editor_state(indoc! {"
15370 fn a() {
15371 «a();
15372 b();
15373 c();ˇ»
15374 }
15375 "});
15376}
15377
15378#[gpui::test]
15379async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
15380 init_test(cx, |_| {});
15381
15382 let language = Arc::new(Language::new(
15383 LanguageConfig {
15384 line_comments: vec!["// ".into()],
15385 ..Default::default()
15386 },
15387 Some(tree_sitter_rust::LANGUAGE.into()),
15388 ));
15389
15390 let mut cx = EditorTestContext::new(cx).await;
15391
15392 cx.language_registry().add(language.clone());
15393 cx.update_buffer(|buffer, cx| {
15394 buffer.set_language(Some(language), cx);
15395 });
15396
15397 let toggle_comments = &ToggleComments {
15398 advance_downwards: true,
15399 ignore_indent: false,
15400 };
15401
15402 // Single cursor on one line -> advance
15403 // Cursor moves horizontally 3 characters as well on non-blank line
15404 cx.set_state(indoc!(
15405 "fn a() {
15406 ˇdog();
15407 cat();
15408 }"
15409 ));
15410 cx.update_editor(|editor, window, cx| {
15411 editor.toggle_comments(toggle_comments, window, cx);
15412 });
15413 cx.assert_editor_state(indoc!(
15414 "fn a() {
15415 // dog();
15416 catˇ();
15417 }"
15418 ));
15419
15420 // Single selection on one line -> don't advance
15421 cx.set_state(indoc!(
15422 "fn a() {
15423 «dog()ˇ»;
15424 cat();
15425 }"
15426 ));
15427 cx.update_editor(|editor, window, cx| {
15428 editor.toggle_comments(toggle_comments, window, cx);
15429 });
15430 cx.assert_editor_state(indoc!(
15431 "fn a() {
15432 // «dog()ˇ»;
15433 cat();
15434 }"
15435 ));
15436
15437 // Multiple cursors on one line -> advance
15438 cx.set_state(indoc!(
15439 "fn a() {
15440 ˇdˇog();
15441 cat();
15442 }"
15443 ));
15444 cx.update_editor(|editor, window, cx| {
15445 editor.toggle_comments(toggle_comments, window, cx);
15446 });
15447 cx.assert_editor_state(indoc!(
15448 "fn a() {
15449 // dog();
15450 catˇ(ˇ);
15451 }"
15452 ));
15453
15454 // Multiple cursors on one line, with selection -> don't advance
15455 cx.set_state(indoc!(
15456 "fn a() {
15457 ˇdˇog«()ˇ»;
15458 cat();
15459 }"
15460 ));
15461 cx.update_editor(|editor, window, cx| {
15462 editor.toggle_comments(toggle_comments, window, cx);
15463 });
15464 cx.assert_editor_state(indoc!(
15465 "fn a() {
15466 // ˇdˇog«()ˇ»;
15467 cat();
15468 }"
15469 ));
15470
15471 // Single cursor on one line -> advance
15472 // Cursor moves to column 0 on blank line
15473 cx.set_state(indoc!(
15474 "fn a() {
15475 ˇdog();
15476
15477 cat();
15478 }"
15479 ));
15480 cx.update_editor(|editor, window, cx| {
15481 editor.toggle_comments(toggle_comments, window, cx);
15482 });
15483 cx.assert_editor_state(indoc!(
15484 "fn a() {
15485 // dog();
15486 ˇ
15487 cat();
15488 }"
15489 ));
15490
15491 // Single cursor on one line -> advance
15492 // Cursor starts and ends at column 0
15493 cx.set_state(indoc!(
15494 "fn a() {
15495 ˇ dog();
15496 cat();
15497 }"
15498 ));
15499 cx.update_editor(|editor, window, cx| {
15500 editor.toggle_comments(toggle_comments, window, cx);
15501 });
15502 cx.assert_editor_state(indoc!(
15503 "fn a() {
15504 // dog();
15505 ˇ cat();
15506 }"
15507 ));
15508}
15509
15510#[gpui::test]
15511async fn test_toggle_block_comment(cx: &mut TestAppContext) {
15512 init_test(cx, |_| {});
15513
15514 let mut cx = EditorTestContext::new(cx).await;
15515
15516 let html_language = Arc::new(
15517 Language::new(
15518 LanguageConfig {
15519 name: "HTML".into(),
15520 block_comment: Some(BlockCommentConfig {
15521 start: "<!-- ".into(),
15522 prefix: "".into(),
15523 end: " -->".into(),
15524 tab_size: 0,
15525 }),
15526 ..Default::default()
15527 },
15528 Some(tree_sitter_html::LANGUAGE.into()),
15529 )
15530 .with_injection_query(
15531 r#"
15532 (script_element
15533 (raw_text) @injection.content
15534 (#set! injection.language "javascript"))
15535 "#,
15536 )
15537 .unwrap(),
15538 );
15539
15540 let javascript_language = Arc::new(Language::new(
15541 LanguageConfig {
15542 name: "JavaScript".into(),
15543 line_comments: vec!["// ".into()],
15544 ..Default::default()
15545 },
15546 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
15547 ));
15548
15549 cx.language_registry().add(html_language.clone());
15550 cx.language_registry().add(javascript_language);
15551 cx.update_buffer(|buffer, cx| {
15552 buffer.set_language(Some(html_language), cx);
15553 });
15554
15555 // Toggle comments for empty selections
15556 cx.set_state(
15557 &r#"
15558 <p>A</p>ˇ
15559 <p>B</p>ˇ
15560 <p>C</p>ˇ
15561 "#
15562 .unindent(),
15563 );
15564 cx.update_editor(|editor, window, cx| {
15565 editor.toggle_comments(&ToggleComments::default(), window, cx)
15566 });
15567 cx.assert_editor_state(
15568 &r#"
15569 <!-- <p>A</p>ˇ -->
15570 <!-- <p>B</p>ˇ -->
15571 <!-- <p>C</p>ˇ -->
15572 "#
15573 .unindent(),
15574 );
15575 cx.update_editor(|editor, window, cx| {
15576 editor.toggle_comments(&ToggleComments::default(), window, cx)
15577 });
15578 cx.assert_editor_state(
15579 &r#"
15580 <p>A</p>ˇ
15581 <p>B</p>ˇ
15582 <p>C</p>ˇ
15583 "#
15584 .unindent(),
15585 );
15586
15587 // Toggle comments for mixture of empty and non-empty selections, where
15588 // multiple selections occupy a given line.
15589 cx.set_state(
15590 &r#"
15591 <p>A«</p>
15592 <p>ˇ»B</p>ˇ
15593 <p>C«</p>
15594 <p>ˇ»D</p>ˇ
15595 "#
15596 .unindent(),
15597 );
15598
15599 cx.update_editor(|editor, window, cx| {
15600 editor.toggle_comments(&ToggleComments::default(), window, cx)
15601 });
15602 cx.assert_editor_state(
15603 &r#"
15604 <!-- <p>A«</p>
15605 <p>ˇ»B</p>ˇ -->
15606 <!-- <p>C«</p>
15607 <p>ˇ»D</p>ˇ -->
15608 "#
15609 .unindent(),
15610 );
15611 cx.update_editor(|editor, window, cx| {
15612 editor.toggle_comments(&ToggleComments::default(), window, cx)
15613 });
15614 cx.assert_editor_state(
15615 &r#"
15616 <p>A«</p>
15617 <p>ˇ»B</p>ˇ
15618 <p>C«</p>
15619 <p>ˇ»D</p>ˇ
15620 "#
15621 .unindent(),
15622 );
15623
15624 // Toggle comments when different languages are active for different
15625 // selections.
15626 cx.set_state(
15627 &r#"
15628 ˇ<script>
15629 ˇvar x = new Y();
15630 ˇ</script>
15631 "#
15632 .unindent(),
15633 );
15634 cx.executor().run_until_parked();
15635 cx.update_editor(|editor, window, cx| {
15636 editor.toggle_comments(&ToggleComments::default(), window, cx)
15637 });
15638 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
15639 // Uncommenting and commenting from this position brings in even more wrong artifacts.
15640 cx.assert_editor_state(
15641 &r#"
15642 <!-- ˇ<script> -->
15643 // ˇvar x = new Y();
15644 <!-- ˇ</script> -->
15645 "#
15646 .unindent(),
15647 );
15648}
15649
15650#[gpui::test]
15651fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
15652 init_test(cx, |_| {});
15653
15654 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15655 let multibuffer = cx.new(|cx| {
15656 let mut multibuffer = MultiBuffer::new(ReadWrite);
15657 multibuffer.push_excerpts(
15658 buffer.clone(),
15659 [
15660 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
15661 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
15662 ],
15663 cx,
15664 );
15665 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
15666 multibuffer
15667 });
15668
15669 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
15670 editor.update_in(cx, |editor, window, cx| {
15671 assert_eq!(editor.text(cx), "aaaa\nbbbb");
15672 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15673 s.select_ranges([
15674 Point::new(0, 0)..Point::new(0, 0),
15675 Point::new(1, 0)..Point::new(1, 0),
15676 ])
15677 });
15678
15679 editor.handle_input("X", window, cx);
15680 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
15681 assert_eq!(
15682 editor.selections.ranges(cx),
15683 [
15684 Point::new(0, 1)..Point::new(0, 1),
15685 Point::new(1, 1)..Point::new(1, 1),
15686 ]
15687 );
15688
15689 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
15690 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15691 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
15692 });
15693 editor.backspace(&Default::default(), window, cx);
15694 assert_eq!(editor.text(cx), "Xa\nbbb");
15695 assert_eq!(
15696 editor.selections.ranges(cx),
15697 [Point::new(1, 0)..Point::new(1, 0)]
15698 );
15699
15700 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15701 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
15702 });
15703 editor.backspace(&Default::default(), window, cx);
15704 assert_eq!(editor.text(cx), "X\nbb");
15705 assert_eq!(
15706 editor.selections.ranges(cx),
15707 [Point::new(0, 1)..Point::new(0, 1)]
15708 );
15709 });
15710}
15711
15712#[gpui::test]
15713fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
15714 init_test(cx, |_| {});
15715
15716 let markers = vec![('[', ']').into(), ('(', ')').into()];
15717 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
15718 indoc! {"
15719 [aaaa
15720 (bbbb]
15721 cccc)",
15722 },
15723 markers.clone(),
15724 );
15725 let excerpt_ranges = markers.into_iter().map(|marker| {
15726 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
15727 ExcerptRange::new(context)
15728 });
15729 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
15730 let multibuffer = cx.new(|cx| {
15731 let mut multibuffer = MultiBuffer::new(ReadWrite);
15732 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
15733 multibuffer
15734 });
15735
15736 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
15737 editor.update_in(cx, |editor, window, cx| {
15738 let (expected_text, selection_ranges) = marked_text_ranges(
15739 indoc! {"
15740 aaaa
15741 bˇbbb
15742 bˇbbˇb
15743 cccc"
15744 },
15745 true,
15746 );
15747 assert_eq!(editor.text(cx), expected_text);
15748 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15749 s.select_ranges(selection_ranges)
15750 });
15751
15752 editor.handle_input("X", window, cx);
15753
15754 let (expected_text, expected_selections) = marked_text_ranges(
15755 indoc! {"
15756 aaaa
15757 bXˇbbXb
15758 bXˇbbXˇb
15759 cccc"
15760 },
15761 false,
15762 );
15763 assert_eq!(editor.text(cx), expected_text);
15764 assert_eq!(editor.selections.ranges(cx), expected_selections);
15765
15766 editor.newline(&Newline, window, cx);
15767 let (expected_text, expected_selections) = marked_text_ranges(
15768 indoc! {"
15769 aaaa
15770 bX
15771 ˇbbX
15772 b
15773 bX
15774 ˇbbX
15775 ˇb
15776 cccc"
15777 },
15778 false,
15779 );
15780 assert_eq!(editor.text(cx), expected_text);
15781 assert_eq!(editor.selections.ranges(cx), expected_selections);
15782 });
15783}
15784
15785#[gpui::test]
15786fn test_refresh_selections(cx: &mut TestAppContext) {
15787 init_test(cx, |_| {});
15788
15789 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15790 let mut excerpt1_id = None;
15791 let multibuffer = cx.new(|cx| {
15792 let mut multibuffer = MultiBuffer::new(ReadWrite);
15793 excerpt1_id = multibuffer
15794 .push_excerpts(
15795 buffer.clone(),
15796 [
15797 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
15798 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
15799 ],
15800 cx,
15801 )
15802 .into_iter()
15803 .next();
15804 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
15805 multibuffer
15806 });
15807
15808 let editor = cx.add_window(|window, cx| {
15809 let mut editor = build_editor(multibuffer.clone(), window, cx);
15810 let snapshot = editor.snapshot(window, cx);
15811 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15812 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
15813 });
15814 editor.begin_selection(
15815 Point::new(2, 1).to_display_point(&snapshot),
15816 true,
15817 1,
15818 window,
15819 cx,
15820 );
15821 assert_eq!(
15822 editor.selections.ranges(cx),
15823 [
15824 Point::new(1, 3)..Point::new(1, 3),
15825 Point::new(2, 1)..Point::new(2, 1),
15826 ]
15827 );
15828 editor
15829 });
15830
15831 // Refreshing selections is a no-op when excerpts haven't changed.
15832 _ = editor.update(cx, |editor, window, cx| {
15833 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
15834 assert_eq!(
15835 editor.selections.ranges(cx),
15836 [
15837 Point::new(1, 3)..Point::new(1, 3),
15838 Point::new(2, 1)..Point::new(2, 1),
15839 ]
15840 );
15841 });
15842
15843 multibuffer.update(cx, |multibuffer, cx| {
15844 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
15845 });
15846 _ = editor.update(cx, |editor, window, cx| {
15847 // Removing an excerpt causes the first selection to become degenerate.
15848 assert_eq!(
15849 editor.selections.ranges(cx),
15850 [
15851 Point::new(0, 0)..Point::new(0, 0),
15852 Point::new(0, 1)..Point::new(0, 1)
15853 ]
15854 );
15855
15856 // Refreshing selections will relocate the first selection to the original buffer
15857 // location.
15858 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
15859 assert_eq!(
15860 editor.selections.ranges(cx),
15861 [
15862 Point::new(0, 1)..Point::new(0, 1),
15863 Point::new(0, 3)..Point::new(0, 3)
15864 ]
15865 );
15866 assert!(editor.selections.pending_anchor().is_some());
15867 });
15868}
15869
15870#[gpui::test]
15871fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
15872 init_test(cx, |_| {});
15873
15874 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15875 let mut excerpt1_id = None;
15876 let multibuffer = cx.new(|cx| {
15877 let mut multibuffer = MultiBuffer::new(ReadWrite);
15878 excerpt1_id = multibuffer
15879 .push_excerpts(
15880 buffer.clone(),
15881 [
15882 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
15883 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
15884 ],
15885 cx,
15886 )
15887 .into_iter()
15888 .next();
15889 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
15890 multibuffer
15891 });
15892
15893 let editor = cx.add_window(|window, cx| {
15894 let mut editor = build_editor(multibuffer.clone(), window, cx);
15895 let snapshot = editor.snapshot(window, cx);
15896 editor.begin_selection(
15897 Point::new(1, 3).to_display_point(&snapshot),
15898 false,
15899 1,
15900 window,
15901 cx,
15902 );
15903 assert_eq!(
15904 editor.selections.ranges(cx),
15905 [Point::new(1, 3)..Point::new(1, 3)]
15906 );
15907 editor
15908 });
15909
15910 multibuffer.update(cx, |multibuffer, cx| {
15911 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
15912 });
15913 _ = editor.update(cx, |editor, window, cx| {
15914 assert_eq!(
15915 editor.selections.ranges(cx),
15916 [Point::new(0, 0)..Point::new(0, 0)]
15917 );
15918
15919 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
15920 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
15921 assert_eq!(
15922 editor.selections.ranges(cx),
15923 [Point::new(0, 3)..Point::new(0, 3)]
15924 );
15925 assert!(editor.selections.pending_anchor().is_some());
15926 });
15927}
15928
15929#[gpui::test]
15930async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
15931 init_test(cx, |_| {});
15932
15933 let language = Arc::new(
15934 Language::new(
15935 LanguageConfig {
15936 brackets: BracketPairConfig {
15937 pairs: vec![
15938 BracketPair {
15939 start: "{".to_string(),
15940 end: "}".to_string(),
15941 close: true,
15942 surround: true,
15943 newline: true,
15944 },
15945 BracketPair {
15946 start: "/* ".to_string(),
15947 end: " */".to_string(),
15948 close: true,
15949 surround: true,
15950 newline: true,
15951 },
15952 ],
15953 ..Default::default()
15954 },
15955 ..Default::default()
15956 },
15957 Some(tree_sitter_rust::LANGUAGE.into()),
15958 )
15959 .with_indents_query("")
15960 .unwrap(),
15961 );
15962
15963 let text = concat!(
15964 "{ }\n", //
15965 " x\n", //
15966 " /* */\n", //
15967 "x\n", //
15968 "{{} }\n", //
15969 );
15970
15971 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
15972 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
15973 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
15974 editor
15975 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
15976 .await;
15977
15978 editor.update_in(cx, |editor, window, cx| {
15979 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15980 s.select_display_ranges([
15981 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
15982 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
15983 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
15984 ])
15985 });
15986 editor.newline(&Newline, window, cx);
15987
15988 assert_eq!(
15989 editor.buffer().read(cx).read(cx).text(),
15990 concat!(
15991 "{ \n", // Suppress rustfmt
15992 "\n", //
15993 "}\n", //
15994 " x\n", //
15995 " /* \n", //
15996 " \n", //
15997 " */\n", //
15998 "x\n", //
15999 "{{} \n", //
16000 "}\n", //
16001 )
16002 );
16003 });
16004}
16005
16006#[gpui::test]
16007fn test_highlighted_ranges(cx: &mut TestAppContext) {
16008 init_test(cx, |_| {});
16009
16010 let editor = cx.add_window(|window, cx| {
16011 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
16012 build_editor(buffer, window, cx)
16013 });
16014
16015 _ = editor.update(cx, |editor, window, cx| {
16016 struct Type1;
16017 struct Type2;
16018
16019 let buffer = editor.buffer.read(cx).snapshot(cx);
16020
16021 let anchor_range =
16022 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
16023
16024 editor.highlight_background::<Type1>(
16025 &[
16026 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
16027 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
16028 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
16029 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
16030 ],
16031 |_| Hsla::red(),
16032 cx,
16033 );
16034 editor.highlight_background::<Type2>(
16035 &[
16036 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
16037 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
16038 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
16039 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
16040 ],
16041 |_| Hsla::green(),
16042 cx,
16043 );
16044
16045 let snapshot = editor.snapshot(window, cx);
16046 let highlighted_ranges = editor.sorted_background_highlights_in_range(
16047 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
16048 &snapshot,
16049 cx.theme(),
16050 );
16051 assert_eq!(
16052 highlighted_ranges,
16053 &[
16054 (
16055 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
16056 Hsla::green(),
16057 ),
16058 (
16059 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
16060 Hsla::red(),
16061 ),
16062 (
16063 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
16064 Hsla::green(),
16065 ),
16066 (
16067 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16068 Hsla::red(),
16069 ),
16070 ]
16071 );
16072 assert_eq!(
16073 editor.sorted_background_highlights_in_range(
16074 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
16075 &snapshot,
16076 cx.theme(),
16077 ),
16078 &[(
16079 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16080 Hsla::red(),
16081 )]
16082 );
16083 });
16084}
16085
16086#[gpui::test]
16087async fn test_following(cx: &mut TestAppContext) {
16088 init_test(cx, |_| {});
16089
16090 let fs = FakeFs::new(cx.executor());
16091 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16092
16093 let buffer = project.update(cx, |project, cx| {
16094 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, false, cx);
16095 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
16096 });
16097 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
16098 let follower = cx.update(|cx| {
16099 cx.open_window(
16100 WindowOptions {
16101 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
16102 gpui::Point::new(px(0.), px(0.)),
16103 gpui::Point::new(px(10.), px(80.)),
16104 ))),
16105 ..Default::default()
16106 },
16107 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
16108 )
16109 .unwrap()
16110 });
16111
16112 let is_still_following = Rc::new(RefCell::new(true));
16113 let follower_edit_event_count = Rc::new(RefCell::new(0));
16114 let pending_update = Rc::new(RefCell::new(None));
16115 let leader_entity = leader.root(cx).unwrap();
16116 let follower_entity = follower.root(cx).unwrap();
16117 _ = follower.update(cx, {
16118 let update = pending_update.clone();
16119 let is_still_following = is_still_following.clone();
16120 let follower_edit_event_count = follower_edit_event_count.clone();
16121 |_, window, cx| {
16122 cx.subscribe_in(
16123 &leader_entity,
16124 window,
16125 move |_, leader, event, window, cx| {
16126 leader.read(cx).add_event_to_update_proto(
16127 event,
16128 &mut update.borrow_mut(),
16129 window,
16130 cx,
16131 );
16132 },
16133 )
16134 .detach();
16135
16136 cx.subscribe_in(
16137 &follower_entity,
16138 window,
16139 move |_, _, event: &EditorEvent, _window, _cx| {
16140 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
16141 *is_still_following.borrow_mut() = false;
16142 }
16143
16144 if let EditorEvent::BufferEdited = event {
16145 *follower_edit_event_count.borrow_mut() += 1;
16146 }
16147 },
16148 )
16149 .detach();
16150 }
16151 });
16152
16153 // Update the selections only
16154 _ = leader.update(cx, |leader, window, cx| {
16155 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16156 s.select_ranges([1..1])
16157 });
16158 });
16159 follower
16160 .update(cx, |follower, window, cx| {
16161 follower.apply_update_proto(
16162 &project,
16163 pending_update.borrow_mut().take().unwrap(),
16164 window,
16165 cx,
16166 )
16167 })
16168 .unwrap()
16169 .await
16170 .unwrap();
16171 _ = follower.update(cx, |follower, _, cx| {
16172 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
16173 });
16174 assert!(*is_still_following.borrow());
16175 assert_eq!(*follower_edit_event_count.borrow(), 0);
16176
16177 // Update the scroll position only
16178 _ = leader.update(cx, |leader, window, cx| {
16179 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16180 });
16181 follower
16182 .update(cx, |follower, window, cx| {
16183 follower.apply_update_proto(
16184 &project,
16185 pending_update.borrow_mut().take().unwrap(),
16186 window,
16187 cx,
16188 )
16189 })
16190 .unwrap()
16191 .await
16192 .unwrap();
16193 assert_eq!(
16194 follower
16195 .update(cx, |follower, _, cx| follower.scroll_position(cx))
16196 .unwrap(),
16197 gpui::Point::new(1.5, 3.5)
16198 );
16199 assert!(*is_still_following.borrow());
16200 assert_eq!(*follower_edit_event_count.borrow(), 0);
16201
16202 // Update the selections and scroll position. The follower's scroll position is updated
16203 // via autoscroll, not via the leader's exact scroll position.
16204 _ = leader.update(cx, |leader, window, cx| {
16205 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16206 s.select_ranges([0..0])
16207 });
16208 leader.request_autoscroll(Autoscroll::newest(), cx);
16209 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16210 });
16211 follower
16212 .update(cx, |follower, window, cx| {
16213 follower.apply_update_proto(
16214 &project,
16215 pending_update.borrow_mut().take().unwrap(),
16216 window,
16217 cx,
16218 )
16219 })
16220 .unwrap()
16221 .await
16222 .unwrap();
16223 _ = follower.update(cx, |follower, _, cx| {
16224 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
16225 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
16226 });
16227 assert!(*is_still_following.borrow());
16228
16229 // Creating a pending selection that precedes another selection
16230 _ = leader.update(cx, |leader, window, cx| {
16231 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16232 s.select_ranges([1..1])
16233 });
16234 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
16235 });
16236 follower
16237 .update(cx, |follower, window, cx| {
16238 follower.apply_update_proto(
16239 &project,
16240 pending_update.borrow_mut().take().unwrap(),
16241 window,
16242 cx,
16243 )
16244 })
16245 .unwrap()
16246 .await
16247 .unwrap();
16248 _ = follower.update(cx, |follower, _, cx| {
16249 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
16250 });
16251 assert!(*is_still_following.borrow());
16252
16253 // Extend the pending selection so that it surrounds another selection
16254 _ = leader.update(cx, |leader, window, cx| {
16255 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
16256 });
16257 follower
16258 .update(cx, |follower, window, cx| {
16259 follower.apply_update_proto(
16260 &project,
16261 pending_update.borrow_mut().take().unwrap(),
16262 window,
16263 cx,
16264 )
16265 })
16266 .unwrap()
16267 .await
16268 .unwrap();
16269 _ = follower.update(cx, |follower, _, cx| {
16270 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
16271 });
16272
16273 // Scrolling locally breaks the follow
16274 _ = follower.update(cx, |follower, window, cx| {
16275 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
16276 follower.set_scroll_anchor(
16277 ScrollAnchor {
16278 anchor: top_anchor,
16279 offset: gpui::Point::new(0.0, 0.5),
16280 },
16281 window,
16282 cx,
16283 );
16284 });
16285 assert!(!(*is_still_following.borrow()));
16286}
16287
16288#[gpui::test]
16289async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
16290 init_test(cx, |_| {});
16291
16292 let fs = FakeFs::new(cx.executor());
16293 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16294 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16295 let pane = workspace
16296 .update(cx, |workspace, _, _| workspace.active_pane().clone())
16297 .unwrap();
16298
16299 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16300
16301 let leader = pane.update_in(cx, |_, window, cx| {
16302 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
16303 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
16304 });
16305
16306 // Start following the editor when it has no excerpts.
16307 let mut state_message =
16308 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16309 let workspace_entity = workspace.root(cx).unwrap();
16310 let follower_1 = cx
16311 .update_window(*workspace.deref(), |_, window, cx| {
16312 Editor::from_state_proto(
16313 workspace_entity,
16314 ViewId {
16315 creator: CollaboratorId::PeerId(PeerId::default()),
16316 id: 0,
16317 },
16318 &mut state_message,
16319 window,
16320 cx,
16321 )
16322 })
16323 .unwrap()
16324 .unwrap()
16325 .await
16326 .unwrap();
16327
16328 let update_message = Rc::new(RefCell::new(None));
16329 follower_1.update_in(cx, {
16330 let update = update_message.clone();
16331 |_, window, cx| {
16332 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
16333 leader.read(cx).add_event_to_update_proto(
16334 event,
16335 &mut update.borrow_mut(),
16336 window,
16337 cx,
16338 );
16339 })
16340 .detach();
16341 }
16342 });
16343
16344 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
16345 (
16346 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, false, cx),
16347 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, false, cx),
16348 )
16349 });
16350
16351 // Insert some excerpts.
16352 leader.update(cx, |leader, cx| {
16353 leader.buffer.update(cx, |multibuffer, cx| {
16354 multibuffer.set_excerpts_for_path(
16355 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
16356 buffer_1.clone(),
16357 vec![
16358 Point::row_range(0..3),
16359 Point::row_range(1..6),
16360 Point::row_range(12..15),
16361 ],
16362 0,
16363 cx,
16364 );
16365 multibuffer.set_excerpts_for_path(
16366 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
16367 buffer_2.clone(),
16368 vec![Point::row_range(0..6), Point::row_range(8..12)],
16369 0,
16370 cx,
16371 );
16372 });
16373 });
16374
16375 // Apply the update of adding the excerpts.
16376 follower_1
16377 .update_in(cx, |follower, window, cx| {
16378 follower.apply_update_proto(
16379 &project,
16380 update_message.borrow().clone().unwrap(),
16381 window,
16382 cx,
16383 )
16384 })
16385 .await
16386 .unwrap();
16387 assert_eq!(
16388 follower_1.update(cx, |editor, cx| editor.text(cx)),
16389 leader.update(cx, |editor, cx| editor.text(cx))
16390 );
16391 update_message.borrow_mut().take();
16392
16393 // Start following separately after it already has excerpts.
16394 let mut state_message =
16395 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16396 let workspace_entity = workspace.root(cx).unwrap();
16397 let follower_2 = cx
16398 .update_window(*workspace.deref(), |_, window, cx| {
16399 Editor::from_state_proto(
16400 workspace_entity,
16401 ViewId {
16402 creator: CollaboratorId::PeerId(PeerId::default()),
16403 id: 0,
16404 },
16405 &mut state_message,
16406 window,
16407 cx,
16408 )
16409 })
16410 .unwrap()
16411 .unwrap()
16412 .await
16413 .unwrap();
16414 assert_eq!(
16415 follower_2.update(cx, |editor, cx| editor.text(cx)),
16416 leader.update(cx, |editor, cx| editor.text(cx))
16417 );
16418
16419 // Remove some excerpts.
16420 leader.update(cx, |leader, cx| {
16421 leader.buffer.update(cx, |multibuffer, cx| {
16422 let excerpt_ids = multibuffer.excerpt_ids();
16423 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
16424 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
16425 });
16426 });
16427
16428 // Apply the update of removing the excerpts.
16429 follower_1
16430 .update_in(cx, |follower, window, cx| {
16431 follower.apply_update_proto(
16432 &project,
16433 update_message.borrow().clone().unwrap(),
16434 window,
16435 cx,
16436 )
16437 })
16438 .await
16439 .unwrap();
16440 follower_2
16441 .update_in(cx, |follower, window, cx| {
16442 follower.apply_update_proto(
16443 &project,
16444 update_message.borrow().clone().unwrap(),
16445 window,
16446 cx,
16447 )
16448 })
16449 .await
16450 .unwrap();
16451 update_message.borrow_mut().take();
16452 assert_eq!(
16453 follower_1.update(cx, |editor, cx| editor.text(cx)),
16454 leader.update(cx, |editor, cx| editor.text(cx))
16455 );
16456}
16457
16458#[gpui::test]
16459async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16460 init_test(cx, |_| {});
16461
16462 let mut cx = EditorTestContext::new(cx).await;
16463 let lsp_store =
16464 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
16465
16466 cx.set_state(indoc! {"
16467 ˇfn func(abc def: i32) -> u32 {
16468 }
16469 "});
16470
16471 cx.update(|_, cx| {
16472 lsp_store.update(cx, |lsp_store, cx| {
16473 lsp_store
16474 .update_diagnostics(
16475 LanguageServerId(0),
16476 lsp::PublishDiagnosticsParams {
16477 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
16478 version: None,
16479 diagnostics: vec![
16480 lsp::Diagnostic {
16481 range: lsp::Range::new(
16482 lsp::Position::new(0, 11),
16483 lsp::Position::new(0, 12),
16484 ),
16485 severity: Some(lsp::DiagnosticSeverity::ERROR),
16486 ..Default::default()
16487 },
16488 lsp::Diagnostic {
16489 range: lsp::Range::new(
16490 lsp::Position::new(0, 12),
16491 lsp::Position::new(0, 15),
16492 ),
16493 severity: Some(lsp::DiagnosticSeverity::ERROR),
16494 ..Default::default()
16495 },
16496 lsp::Diagnostic {
16497 range: lsp::Range::new(
16498 lsp::Position::new(0, 25),
16499 lsp::Position::new(0, 28),
16500 ),
16501 severity: Some(lsp::DiagnosticSeverity::ERROR),
16502 ..Default::default()
16503 },
16504 ],
16505 },
16506 None,
16507 DiagnosticSourceKind::Pushed,
16508 &[],
16509 cx,
16510 )
16511 .unwrap()
16512 });
16513 });
16514
16515 executor.run_until_parked();
16516
16517 cx.update_editor(|editor, window, cx| {
16518 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16519 });
16520
16521 cx.assert_editor_state(indoc! {"
16522 fn func(abc def: i32) -> ˇu32 {
16523 }
16524 "});
16525
16526 cx.update_editor(|editor, window, cx| {
16527 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16528 });
16529
16530 cx.assert_editor_state(indoc! {"
16531 fn func(abc ˇdef: i32) -> u32 {
16532 }
16533 "});
16534
16535 cx.update_editor(|editor, window, cx| {
16536 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16537 });
16538
16539 cx.assert_editor_state(indoc! {"
16540 fn func(abcˇ def: i32) -> u32 {
16541 }
16542 "});
16543
16544 cx.update_editor(|editor, window, cx| {
16545 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16546 });
16547
16548 cx.assert_editor_state(indoc! {"
16549 fn func(abc def: i32) -> ˇu32 {
16550 }
16551 "});
16552}
16553
16554#[gpui::test]
16555async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16556 init_test(cx, |_| {});
16557
16558 let mut cx = EditorTestContext::new(cx).await;
16559
16560 let diff_base = r#"
16561 use some::mod;
16562
16563 const A: u32 = 42;
16564
16565 fn main() {
16566 println!("hello");
16567
16568 println!("world");
16569 }
16570 "#
16571 .unindent();
16572
16573 // Edits are modified, removed, modified, added
16574 cx.set_state(
16575 &r#"
16576 use some::modified;
16577
16578 ˇ
16579 fn main() {
16580 println!("hello there");
16581
16582 println!("around the");
16583 println!("world");
16584 }
16585 "#
16586 .unindent(),
16587 );
16588
16589 cx.set_head_text(&diff_base);
16590 executor.run_until_parked();
16591
16592 cx.update_editor(|editor, window, cx| {
16593 //Wrap around the bottom of the buffer
16594 for _ in 0..3 {
16595 editor.go_to_next_hunk(&GoToHunk, window, cx);
16596 }
16597 });
16598
16599 cx.assert_editor_state(
16600 &r#"
16601 ˇuse some::modified;
16602
16603
16604 fn main() {
16605 println!("hello there");
16606
16607 println!("around the");
16608 println!("world");
16609 }
16610 "#
16611 .unindent(),
16612 );
16613
16614 cx.update_editor(|editor, window, cx| {
16615 //Wrap around the top of the buffer
16616 for _ in 0..2 {
16617 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16618 }
16619 });
16620
16621 cx.assert_editor_state(
16622 &r#"
16623 use some::modified;
16624
16625
16626 fn main() {
16627 ˇ println!("hello there");
16628
16629 println!("around the");
16630 println!("world");
16631 }
16632 "#
16633 .unindent(),
16634 );
16635
16636 cx.update_editor(|editor, window, cx| {
16637 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16638 });
16639
16640 cx.assert_editor_state(
16641 &r#"
16642 use some::modified;
16643
16644 ˇ
16645 fn main() {
16646 println!("hello there");
16647
16648 println!("around the");
16649 println!("world");
16650 }
16651 "#
16652 .unindent(),
16653 );
16654
16655 cx.update_editor(|editor, window, cx| {
16656 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16657 });
16658
16659 cx.assert_editor_state(
16660 &r#"
16661 ˇuse some::modified;
16662
16663
16664 fn main() {
16665 println!("hello there");
16666
16667 println!("around the");
16668 println!("world");
16669 }
16670 "#
16671 .unindent(),
16672 );
16673
16674 cx.update_editor(|editor, window, cx| {
16675 for _ in 0..2 {
16676 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16677 }
16678 });
16679
16680 cx.assert_editor_state(
16681 &r#"
16682 use some::modified;
16683
16684
16685 fn main() {
16686 ˇ println!("hello there");
16687
16688 println!("around the");
16689 println!("world");
16690 }
16691 "#
16692 .unindent(),
16693 );
16694
16695 cx.update_editor(|editor, window, cx| {
16696 editor.fold(&Fold, window, cx);
16697 });
16698
16699 cx.update_editor(|editor, window, cx| {
16700 editor.go_to_next_hunk(&GoToHunk, window, cx);
16701 });
16702
16703 cx.assert_editor_state(
16704 &r#"
16705 ˇuse some::modified;
16706
16707
16708 fn main() {
16709 println!("hello there");
16710
16711 println!("around the");
16712 println!("world");
16713 }
16714 "#
16715 .unindent(),
16716 );
16717}
16718
16719#[test]
16720fn test_split_words() {
16721 fn split(text: &str) -> Vec<&str> {
16722 split_words(text).collect()
16723 }
16724
16725 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
16726 assert_eq!(split("hello_world"), &["hello_", "world"]);
16727 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
16728 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
16729 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
16730 assert_eq!(split("helloworld"), &["helloworld"]);
16731
16732 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
16733}
16734
16735#[gpui::test]
16736async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
16737 init_test(cx, |_| {});
16738
16739 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
16740 let mut assert = |before, after| {
16741 let _state_context = cx.set_state(before);
16742 cx.run_until_parked();
16743 cx.update_editor(|editor, window, cx| {
16744 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
16745 });
16746 cx.run_until_parked();
16747 cx.assert_editor_state(after);
16748 };
16749
16750 // Outside bracket jumps to outside of matching bracket
16751 assert("console.logˇ(var);", "console.log(var)ˇ;");
16752 assert("console.log(var)ˇ;", "console.logˇ(var);");
16753
16754 // Inside bracket jumps to inside of matching bracket
16755 assert("console.log(ˇvar);", "console.log(varˇ);");
16756 assert("console.log(varˇ);", "console.log(ˇvar);");
16757
16758 // When outside a bracket and inside, favor jumping to the inside bracket
16759 assert(
16760 "console.log('foo', [1, 2, 3]ˇ);",
16761 "console.log(ˇ'foo', [1, 2, 3]);",
16762 );
16763 assert(
16764 "console.log(ˇ'foo', [1, 2, 3]);",
16765 "console.log('foo', [1, 2, 3]ˇ);",
16766 );
16767
16768 // Bias forward if two options are equally likely
16769 assert(
16770 "let result = curried_fun()ˇ();",
16771 "let result = curried_fun()()ˇ;",
16772 );
16773
16774 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
16775 assert(
16776 indoc! {"
16777 function test() {
16778 console.log('test')ˇ
16779 }"},
16780 indoc! {"
16781 function test() {
16782 console.logˇ('test')
16783 }"},
16784 );
16785}
16786
16787#[gpui::test]
16788async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
16789 init_test(cx, |_| {});
16790
16791 let fs = FakeFs::new(cx.executor());
16792 fs.insert_tree(
16793 path!("/a"),
16794 json!({
16795 "main.rs": "fn main() { let a = 5; }",
16796 "other.rs": "// Test file",
16797 }),
16798 )
16799 .await;
16800 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16801
16802 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
16803 language_registry.add(Arc::new(Language::new(
16804 LanguageConfig {
16805 name: "Rust".into(),
16806 matcher: LanguageMatcher {
16807 path_suffixes: vec!["rs".to_string()],
16808 ..Default::default()
16809 },
16810 brackets: BracketPairConfig {
16811 pairs: vec![BracketPair {
16812 start: "{".to_string(),
16813 end: "}".to_string(),
16814 close: true,
16815 surround: true,
16816 newline: true,
16817 }],
16818 disabled_scopes_by_bracket_ix: Vec::new(),
16819 },
16820 ..Default::default()
16821 },
16822 Some(tree_sitter_rust::LANGUAGE.into()),
16823 )));
16824 let mut fake_servers = language_registry.register_fake_lsp(
16825 "Rust",
16826 FakeLspAdapter {
16827 capabilities: lsp::ServerCapabilities {
16828 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
16829 first_trigger_character: "{".to_string(),
16830 more_trigger_character: None,
16831 }),
16832 ..Default::default()
16833 },
16834 ..Default::default()
16835 },
16836 );
16837
16838 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16839
16840 let cx = &mut VisualTestContext::from_window(*workspace, cx);
16841
16842 let worktree_id = workspace
16843 .update(cx, |workspace, _, cx| {
16844 workspace.project().update(cx, |project, cx| {
16845 project.worktrees(cx).next().unwrap().read(cx).id()
16846 })
16847 })
16848 .unwrap();
16849
16850 let buffer = project
16851 .update(cx, |project, cx| {
16852 project.open_local_buffer(path!("/a/main.rs"), cx)
16853 })
16854 .await
16855 .unwrap();
16856 let editor_handle = workspace
16857 .update(cx, |workspace, window, cx| {
16858 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
16859 })
16860 .unwrap()
16861 .await
16862 .unwrap()
16863 .downcast::<Editor>()
16864 .unwrap();
16865
16866 cx.executor().start_waiting();
16867 let fake_server = fake_servers.next().await.unwrap();
16868
16869 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
16870 |params, _| async move {
16871 assert_eq!(
16872 params.text_document_position.text_document.uri,
16873 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
16874 );
16875 assert_eq!(
16876 params.text_document_position.position,
16877 lsp::Position::new(0, 21),
16878 );
16879
16880 Ok(Some(vec![lsp::TextEdit {
16881 new_text: "]".to_string(),
16882 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
16883 }]))
16884 },
16885 );
16886
16887 editor_handle.update_in(cx, |editor, window, cx| {
16888 window.focus(&editor.focus_handle(cx));
16889 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16890 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
16891 });
16892 editor.handle_input("{", window, cx);
16893 });
16894
16895 cx.executor().run_until_parked();
16896
16897 buffer.update(cx, |buffer, _| {
16898 assert_eq!(
16899 buffer.text(),
16900 "fn main() { let a = {5}; }",
16901 "No extra braces from on type formatting should appear in the buffer"
16902 )
16903 });
16904}
16905
16906#[gpui::test(iterations = 20, seeds(31))]
16907async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
16908 init_test(cx, |_| {});
16909
16910 let mut cx = EditorLspTestContext::new_rust(
16911 lsp::ServerCapabilities {
16912 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
16913 first_trigger_character: ".".to_string(),
16914 more_trigger_character: None,
16915 }),
16916 ..Default::default()
16917 },
16918 cx,
16919 )
16920 .await;
16921
16922 cx.update_buffer(|buffer, _| {
16923 // This causes autoindent to be async.
16924 buffer.set_sync_parse_timeout(Duration::ZERO)
16925 });
16926
16927 cx.set_state("fn c() {\n d()ˇ\n}\n");
16928 cx.simulate_keystroke("\n");
16929 cx.run_until_parked();
16930
16931 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
16932 let mut request =
16933 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
16934 let buffer_cloned = buffer_cloned.clone();
16935 async move {
16936 buffer_cloned.update(&mut cx, |buffer, _| {
16937 assert_eq!(
16938 buffer.text(),
16939 "fn c() {\n d()\n .\n}\n",
16940 "OnTypeFormatting should triggered after autoindent applied"
16941 )
16942 })?;
16943
16944 Ok(Some(vec![]))
16945 }
16946 });
16947
16948 cx.simulate_keystroke(".");
16949 cx.run_until_parked();
16950
16951 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
16952 assert!(request.next().await.is_some());
16953 request.close();
16954 assert!(request.next().await.is_none());
16955}
16956
16957#[gpui::test]
16958async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
16959 init_test(cx, |_| {});
16960
16961 let fs = FakeFs::new(cx.executor());
16962 fs.insert_tree(
16963 path!("/a"),
16964 json!({
16965 "main.rs": "fn main() { let a = 5; }",
16966 "other.rs": "// Test file",
16967 }),
16968 )
16969 .await;
16970
16971 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16972
16973 let server_restarts = Arc::new(AtomicUsize::new(0));
16974 let closure_restarts = Arc::clone(&server_restarts);
16975 let language_server_name = "test language server";
16976 let language_name: LanguageName = "Rust".into();
16977
16978 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
16979 language_registry.add(Arc::new(Language::new(
16980 LanguageConfig {
16981 name: language_name.clone(),
16982 matcher: LanguageMatcher {
16983 path_suffixes: vec!["rs".to_string()],
16984 ..Default::default()
16985 },
16986 ..Default::default()
16987 },
16988 Some(tree_sitter_rust::LANGUAGE.into()),
16989 )));
16990 let mut fake_servers = language_registry.register_fake_lsp(
16991 "Rust",
16992 FakeLspAdapter {
16993 name: language_server_name,
16994 initialization_options: Some(json!({
16995 "testOptionValue": true
16996 })),
16997 initializer: Some(Box::new(move |fake_server| {
16998 let task_restarts = Arc::clone(&closure_restarts);
16999 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
17000 task_restarts.fetch_add(1, atomic::Ordering::Release);
17001 futures::future::ready(Ok(()))
17002 });
17003 })),
17004 ..Default::default()
17005 },
17006 );
17007
17008 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17009 let _buffer = project
17010 .update(cx, |project, cx| {
17011 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
17012 })
17013 .await
17014 .unwrap();
17015 let _fake_server = fake_servers.next().await.unwrap();
17016 update_test_language_settings(cx, |language_settings| {
17017 language_settings.languages.0.insert(
17018 language_name.clone(),
17019 LanguageSettingsContent {
17020 tab_size: NonZeroU32::new(8),
17021 ..Default::default()
17022 },
17023 );
17024 });
17025 cx.executor().run_until_parked();
17026 assert_eq!(
17027 server_restarts.load(atomic::Ordering::Acquire),
17028 0,
17029 "Should not restart LSP server on an unrelated change"
17030 );
17031
17032 update_test_project_settings(cx, |project_settings| {
17033 project_settings.lsp.insert(
17034 "Some other server name".into(),
17035 LspSettings {
17036 binary: None,
17037 settings: None,
17038 initialization_options: Some(json!({
17039 "some other init value": false
17040 })),
17041 enable_lsp_tasks: false,
17042 fetch: None,
17043 },
17044 );
17045 });
17046 cx.executor().run_until_parked();
17047 assert_eq!(
17048 server_restarts.load(atomic::Ordering::Acquire),
17049 0,
17050 "Should not restart LSP server on an unrelated LSP settings change"
17051 );
17052
17053 update_test_project_settings(cx, |project_settings| {
17054 project_settings.lsp.insert(
17055 language_server_name.into(),
17056 LspSettings {
17057 binary: None,
17058 settings: None,
17059 initialization_options: Some(json!({
17060 "anotherInitValue": false
17061 })),
17062 enable_lsp_tasks: false,
17063 fetch: None,
17064 },
17065 );
17066 });
17067 cx.executor().run_until_parked();
17068 assert_eq!(
17069 server_restarts.load(atomic::Ordering::Acquire),
17070 1,
17071 "Should restart LSP server on a related LSP settings change"
17072 );
17073
17074 update_test_project_settings(cx, |project_settings| {
17075 project_settings.lsp.insert(
17076 language_server_name.into(),
17077 LspSettings {
17078 binary: None,
17079 settings: None,
17080 initialization_options: Some(json!({
17081 "anotherInitValue": false
17082 })),
17083 enable_lsp_tasks: false,
17084 fetch: None,
17085 },
17086 );
17087 });
17088 cx.executor().run_until_parked();
17089 assert_eq!(
17090 server_restarts.load(atomic::Ordering::Acquire),
17091 1,
17092 "Should not restart LSP server on a related LSP settings change that is the same"
17093 );
17094
17095 update_test_project_settings(cx, |project_settings| {
17096 project_settings.lsp.insert(
17097 language_server_name.into(),
17098 LspSettings {
17099 binary: None,
17100 settings: None,
17101 initialization_options: None,
17102 enable_lsp_tasks: false,
17103 fetch: None,
17104 },
17105 );
17106 });
17107 cx.executor().run_until_parked();
17108 assert_eq!(
17109 server_restarts.load(atomic::Ordering::Acquire),
17110 2,
17111 "Should restart LSP server on another related LSP settings change"
17112 );
17113}
17114
17115#[gpui::test]
17116async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
17117 init_test(cx, |_| {});
17118
17119 let mut cx = EditorLspTestContext::new_rust(
17120 lsp::ServerCapabilities {
17121 completion_provider: Some(lsp::CompletionOptions {
17122 trigger_characters: Some(vec![".".to_string()]),
17123 resolve_provider: Some(true),
17124 ..Default::default()
17125 }),
17126 ..Default::default()
17127 },
17128 cx,
17129 )
17130 .await;
17131
17132 cx.set_state("fn main() { let a = 2ˇ; }");
17133 cx.simulate_keystroke(".");
17134 let completion_item = lsp::CompletionItem {
17135 label: "some".into(),
17136 kind: Some(lsp::CompletionItemKind::SNIPPET),
17137 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
17138 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
17139 kind: lsp::MarkupKind::Markdown,
17140 value: "```rust\nSome(2)\n```".to_string(),
17141 })),
17142 deprecated: Some(false),
17143 sort_text: Some("fffffff2".to_string()),
17144 filter_text: Some("some".to_string()),
17145 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
17146 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17147 range: lsp::Range {
17148 start: lsp::Position {
17149 line: 0,
17150 character: 22,
17151 },
17152 end: lsp::Position {
17153 line: 0,
17154 character: 22,
17155 },
17156 },
17157 new_text: "Some(2)".to_string(),
17158 })),
17159 additional_text_edits: Some(vec![lsp::TextEdit {
17160 range: lsp::Range {
17161 start: lsp::Position {
17162 line: 0,
17163 character: 20,
17164 },
17165 end: lsp::Position {
17166 line: 0,
17167 character: 22,
17168 },
17169 },
17170 new_text: "".to_string(),
17171 }]),
17172 ..Default::default()
17173 };
17174
17175 let closure_completion_item = completion_item.clone();
17176 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17177 let task_completion_item = closure_completion_item.clone();
17178 async move {
17179 Ok(Some(lsp::CompletionResponse::Array(vec![
17180 task_completion_item,
17181 ])))
17182 }
17183 });
17184
17185 request.next().await;
17186
17187 cx.condition(|editor, _| editor.context_menu_visible())
17188 .await;
17189 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
17190 editor
17191 .confirm_completion(&ConfirmCompletion::default(), window, cx)
17192 .unwrap()
17193 });
17194 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
17195
17196 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
17197 let task_completion_item = completion_item.clone();
17198 async move { Ok(task_completion_item) }
17199 })
17200 .next()
17201 .await
17202 .unwrap();
17203 apply_additional_edits.await.unwrap();
17204 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
17205}
17206
17207#[gpui::test]
17208async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
17209 init_test(cx, |_| {});
17210
17211 let mut cx = EditorLspTestContext::new_rust(
17212 lsp::ServerCapabilities {
17213 completion_provider: Some(lsp::CompletionOptions {
17214 trigger_characters: Some(vec![".".to_string()]),
17215 resolve_provider: Some(true),
17216 ..Default::default()
17217 }),
17218 ..Default::default()
17219 },
17220 cx,
17221 )
17222 .await;
17223
17224 cx.set_state("fn main() { let a = 2ˇ; }");
17225 cx.simulate_keystroke(".");
17226
17227 let item1 = lsp::CompletionItem {
17228 label: "method id()".to_string(),
17229 filter_text: Some("id".to_string()),
17230 detail: None,
17231 documentation: None,
17232 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17233 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17234 new_text: ".id".to_string(),
17235 })),
17236 ..lsp::CompletionItem::default()
17237 };
17238
17239 let item2 = lsp::CompletionItem {
17240 label: "other".to_string(),
17241 filter_text: Some("other".to_string()),
17242 detail: None,
17243 documentation: None,
17244 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17245 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17246 new_text: ".other".to_string(),
17247 })),
17248 ..lsp::CompletionItem::default()
17249 };
17250
17251 let item1 = item1.clone();
17252 cx.set_request_handler::<lsp::request::Completion, _, _>({
17253 let item1 = item1.clone();
17254 move |_, _, _| {
17255 let item1 = item1.clone();
17256 let item2 = item2.clone();
17257 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
17258 }
17259 })
17260 .next()
17261 .await;
17262
17263 cx.condition(|editor, _| editor.context_menu_visible())
17264 .await;
17265 cx.update_editor(|editor, _, _| {
17266 let context_menu = editor.context_menu.borrow_mut();
17267 let context_menu = context_menu
17268 .as_ref()
17269 .expect("Should have the context menu deployed");
17270 match context_menu {
17271 CodeContextMenu::Completions(completions_menu) => {
17272 let completions = completions_menu.completions.borrow_mut();
17273 assert_eq!(
17274 completions
17275 .iter()
17276 .map(|completion| &completion.label.text)
17277 .collect::<Vec<_>>(),
17278 vec!["method id()", "other"]
17279 )
17280 }
17281 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17282 }
17283 });
17284
17285 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
17286 let item1 = item1.clone();
17287 move |_, item_to_resolve, _| {
17288 let item1 = item1.clone();
17289 async move {
17290 if item1 == item_to_resolve {
17291 Ok(lsp::CompletionItem {
17292 label: "method id()".to_string(),
17293 filter_text: Some("id".to_string()),
17294 detail: Some("Now resolved!".to_string()),
17295 documentation: Some(lsp::Documentation::String("Docs".to_string())),
17296 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17297 range: lsp::Range::new(
17298 lsp::Position::new(0, 22),
17299 lsp::Position::new(0, 22),
17300 ),
17301 new_text: ".id".to_string(),
17302 })),
17303 ..lsp::CompletionItem::default()
17304 })
17305 } else {
17306 Ok(item_to_resolve)
17307 }
17308 }
17309 }
17310 })
17311 .next()
17312 .await
17313 .unwrap();
17314 cx.run_until_parked();
17315
17316 cx.update_editor(|editor, window, cx| {
17317 editor.context_menu_next(&Default::default(), window, cx);
17318 });
17319
17320 cx.update_editor(|editor, _, _| {
17321 let context_menu = editor.context_menu.borrow_mut();
17322 let context_menu = context_menu
17323 .as_ref()
17324 .expect("Should have the context menu deployed");
17325 match context_menu {
17326 CodeContextMenu::Completions(completions_menu) => {
17327 let completions = completions_menu.completions.borrow_mut();
17328 assert_eq!(
17329 completions
17330 .iter()
17331 .map(|completion| &completion.label.text)
17332 .collect::<Vec<_>>(),
17333 vec!["method id() Now resolved!", "other"],
17334 "Should update first completion label, but not second as the filter text did not match."
17335 );
17336 }
17337 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17338 }
17339 });
17340}
17341
17342#[gpui::test]
17343async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
17344 init_test(cx, |_| {});
17345 let mut cx = EditorLspTestContext::new_rust(
17346 lsp::ServerCapabilities {
17347 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
17348 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
17349 completion_provider: Some(lsp::CompletionOptions {
17350 resolve_provider: Some(true),
17351 ..Default::default()
17352 }),
17353 ..Default::default()
17354 },
17355 cx,
17356 )
17357 .await;
17358 cx.set_state(indoc! {"
17359 struct TestStruct {
17360 field: i32
17361 }
17362
17363 fn mainˇ() {
17364 let unused_var = 42;
17365 let test_struct = TestStruct { field: 42 };
17366 }
17367 "});
17368 let symbol_range = cx.lsp_range(indoc! {"
17369 struct TestStruct {
17370 field: i32
17371 }
17372
17373 «fn main»() {
17374 let unused_var = 42;
17375 let test_struct = TestStruct { field: 42 };
17376 }
17377 "});
17378 let mut hover_requests =
17379 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
17380 Ok(Some(lsp::Hover {
17381 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
17382 kind: lsp::MarkupKind::Markdown,
17383 value: "Function documentation".to_string(),
17384 }),
17385 range: Some(symbol_range),
17386 }))
17387 });
17388
17389 // Case 1: Test that code action menu hide hover popover
17390 cx.dispatch_action(Hover);
17391 hover_requests.next().await;
17392 cx.condition(|editor, _| editor.hover_state.visible()).await;
17393 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
17394 move |_, _, _| async move {
17395 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
17396 lsp::CodeAction {
17397 title: "Remove unused variable".to_string(),
17398 kind: Some(CodeActionKind::QUICKFIX),
17399 edit: Some(lsp::WorkspaceEdit {
17400 changes: Some(
17401 [(
17402 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
17403 vec![lsp::TextEdit {
17404 range: lsp::Range::new(
17405 lsp::Position::new(5, 4),
17406 lsp::Position::new(5, 27),
17407 ),
17408 new_text: "".to_string(),
17409 }],
17410 )]
17411 .into_iter()
17412 .collect(),
17413 ),
17414 ..Default::default()
17415 }),
17416 ..Default::default()
17417 },
17418 )]))
17419 },
17420 );
17421 cx.update_editor(|editor, window, cx| {
17422 editor.toggle_code_actions(
17423 &ToggleCodeActions {
17424 deployed_from: None,
17425 quick_launch: false,
17426 },
17427 window,
17428 cx,
17429 );
17430 });
17431 code_action_requests.next().await;
17432 cx.run_until_parked();
17433 cx.condition(|editor, _| editor.context_menu_visible())
17434 .await;
17435 cx.update_editor(|editor, _, _| {
17436 assert!(
17437 !editor.hover_state.visible(),
17438 "Hover popover should be hidden when code action menu is shown"
17439 );
17440 // Hide code actions
17441 editor.context_menu.take();
17442 });
17443
17444 // Case 2: Test that code completions hide hover popover
17445 cx.dispatch_action(Hover);
17446 hover_requests.next().await;
17447 cx.condition(|editor, _| editor.hover_state.visible()).await;
17448 let counter = Arc::new(AtomicUsize::new(0));
17449 let mut completion_requests =
17450 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17451 let counter = counter.clone();
17452 async move {
17453 counter.fetch_add(1, atomic::Ordering::Release);
17454 Ok(Some(lsp::CompletionResponse::Array(vec![
17455 lsp::CompletionItem {
17456 label: "main".into(),
17457 kind: Some(lsp::CompletionItemKind::FUNCTION),
17458 detail: Some("() -> ()".to_string()),
17459 ..Default::default()
17460 },
17461 lsp::CompletionItem {
17462 label: "TestStruct".into(),
17463 kind: Some(lsp::CompletionItemKind::STRUCT),
17464 detail: Some("struct TestStruct".to_string()),
17465 ..Default::default()
17466 },
17467 ])))
17468 }
17469 });
17470 cx.update_editor(|editor, window, cx| {
17471 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
17472 });
17473 completion_requests.next().await;
17474 cx.condition(|editor, _| editor.context_menu_visible())
17475 .await;
17476 cx.update_editor(|editor, _, _| {
17477 assert!(
17478 !editor.hover_state.visible(),
17479 "Hover popover should be hidden when completion menu is shown"
17480 );
17481 });
17482}
17483
17484#[gpui::test]
17485async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
17486 init_test(cx, |_| {});
17487
17488 let mut cx = EditorLspTestContext::new_rust(
17489 lsp::ServerCapabilities {
17490 completion_provider: Some(lsp::CompletionOptions {
17491 trigger_characters: Some(vec![".".to_string()]),
17492 resolve_provider: Some(true),
17493 ..Default::default()
17494 }),
17495 ..Default::default()
17496 },
17497 cx,
17498 )
17499 .await;
17500
17501 cx.set_state("fn main() { let a = 2ˇ; }");
17502 cx.simulate_keystroke(".");
17503
17504 let unresolved_item_1 = lsp::CompletionItem {
17505 label: "id".to_string(),
17506 filter_text: Some("id".to_string()),
17507 detail: None,
17508 documentation: None,
17509 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17510 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17511 new_text: ".id".to_string(),
17512 })),
17513 ..lsp::CompletionItem::default()
17514 };
17515 let resolved_item_1 = lsp::CompletionItem {
17516 additional_text_edits: Some(vec![lsp::TextEdit {
17517 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17518 new_text: "!!".to_string(),
17519 }]),
17520 ..unresolved_item_1.clone()
17521 };
17522 let unresolved_item_2 = lsp::CompletionItem {
17523 label: "other".to_string(),
17524 filter_text: Some("other".to_string()),
17525 detail: None,
17526 documentation: None,
17527 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17528 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17529 new_text: ".other".to_string(),
17530 })),
17531 ..lsp::CompletionItem::default()
17532 };
17533 let resolved_item_2 = lsp::CompletionItem {
17534 additional_text_edits: Some(vec![lsp::TextEdit {
17535 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17536 new_text: "??".to_string(),
17537 }]),
17538 ..unresolved_item_2.clone()
17539 };
17540
17541 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
17542 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
17543 cx.lsp
17544 .server
17545 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
17546 let unresolved_item_1 = unresolved_item_1.clone();
17547 let resolved_item_1 = resolved_item_1.clone();
17548 let unresolved_item_2 = unresolved_item_2.clone();
17549 let resolved_item_2 = resolved_item_2.clone();
17550 let resolve_requests_1 = resolve_requests_1.clone();
17551 let resolve_requests_2 = resolve_requests_2.clone();
17552 move |unresolved_request, _| {
17553 let unresolved_item_1 = unresolved_item_1.clone();
17554 let resolved_item_1 = resolved_item_1.clone();
17555 let unresolved_item_2 = unresolved_item_2.clone();
17556 let resolved_item_2 = resolved_item_2.clone();
17557 let resolve_requests_1 = resolve_requests_1.clone();
17558 let resolve_requests_2 = resolve_requests_2.clone();
17559 async move {
17560 if unresolved_request == unresolved_item_1 {
17561 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
17562 Ok(resolved_item_1.clone())
17563 } else if unresolved_request == unresolved_item_2 {
17564 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
17565 Ok(resolved_item_2.clone())
17566 } else {
17567 panic!("Unexpected completion item {unresolved_request:?}")
17568 }
17569 }
17570 }
17571 })
17572 .detach();
17573
17574 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17575 let unresolved_item_1 = unresolved_item_1.clone();
17576 let unresolved_item_2 = unresolved_item_2.clone();
17577 async move {
17578 Ok(Some(lsp::CompletionResponse::Array(vec![
17579 unresolved_item_1,
17580 unresolved_item_2,
17581 ])))
17582 }
17583 })
17584 .next()
17585 .await;
17586
17587 cx.condition(|editor, _| editor.context_menu_visible())
17588 .await;
17589 cx.update_editor(|editor, _, _| {
17590 let context_menu = editor.context_menu.borrow_mut();
17591 let context_menu = context_menu
17592 .as_ref()
17593 .expect("Should have the context menu deployed");
17594 match context_menu {
17595 CodeContextMenu::Completions(completions_menu) => {
17596 let completions = completions_menu.completions.borrow_mut();
17597 assert_eq!(
17598 completions
17599 .iter()
17600 .map(|completion| &completion.label.text)
17601 .collect::<Vec<_>>(),
17602 vec!["id", "other"]
17603 )
17604 }
17605 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17606 }
17607 });
17608 cx.run_until_parked();
17609
17610 cx.update_editor(|editor, window, cx| {
17611 editor.context_menu_next(&ContextMenuNext, window, cx);
17612 });
17613 cx.run_until_parked();
17614 cx.update_editor(|editor, window, cx| {
17615 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
17616 });
17617 cx.run_until_parked();
17618 cx.update_editor(|editor, window, cx| {
17619 editor.context_menu_next(&ContextMenuNext, window, cx);
17620 });
17621 cx.run_until_parked();
17622 cx.update_editor(|editor, window, cx| {
17623 editor
17624 .compose_completion(&ComposeCompletion::default(), window, cx)
17625 .expect("No task returned")
17626 })
17627 .await
17628 .expect("Completion failed");
17629 cx.run_until_parked();
17630
17631 cx.update_editor(|editor, _, cx| {
17632 assert_eq!(
17633 resolve_requests_1.load(atomic::Ordering::Acquire),
17634 1,
17635 "Should always resolve once despite multiple selections"
17636 );
17637 assert_eq!(
17638 resolve_requests_2.load(atomic::Ordering::Acquire),
17639 1,
17640 "Should always resolve once after multiple selections and applying the completion"
17641 );
17642 assert_eq!(
17643 editor.text(cx),
17644 "fn main() { let a = ??.other; }",
17645 "Should use resolved data when applying the completion"
17646 );
17647 });
17648}
17649
17650#[gpui::test]
17651async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
17652 init_test(cx, |_| {});
17653
17654 let item_0 = lsp::CompletionItem {
17655 label: "abs".into(),
17656 insert_text: Some("abs".into()),
17657 data: Some(json!({ "very": "special"})),
17658 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
17659 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
17660 lsp::InsertReplaceEdit {
17661 new_text: "abs".to_string(),
17662 insert: lsp::Range::default(),
17663 replace: lsp::Range::default(),
17664 },
17665 )),
17666 ..lsp::CompletionItem::default()
17667 };
17668 let items = iter::once(item_0.clone())
17669 .chain((11..51).map(|i| lsp::CompletionItem {
17670 label: format!("item_{}", i),
17671 insert_text: Some(format!("item_{}", i)),
17672 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
17673 ..lsp::CompletionItem::default()
17674 }))
17675 .collect::<Vec<_>>();
17676
17677 let default_commit_characters = vec!["?".to_string()];
17678 let default_data = json!({ "default": "data"});
17679 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
17680 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
17681 let default_edit_range = lsp::Range {
17682 start: lsp::Position {
17683 line: 0,
17684 character: 5,
17685 },
17686 end: lsp::Position {
17687 line: 0,
17688 character: 5,
17689 },
17690 };
17691
17692 let mut cx = EditorLspTestContext::new_rust(
17693 lsp::ServerCapabilities {
17694 completion_provider: Some(lsp::CompletionOptions {
17695 trigger_characters: Some(vec![".".to_string()]),
17696 resolve_provider: Some(true),
17697 ..Default::default()
17698 }),
17699 ..Default::default()
17700 },
17701 cx,
17702 )
17703 .await;
17704
17705 cx.set_state("fn main() { let a = 2ˇ; }");
17706 cx.simulate_keystroke(".");
17707
17708 let completion_data = default_data.clone();
17709 let completion_characters = default_commit_characters.clone();
17710 let completion_items = items.clone();
17711 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17712 let default_data = completion_data.clone();
17713 let default_commit_characters = completion_characters.clone();
17714 let items = completion_items.clone();
17715 async move {
17716 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
17717 items,
17718 item_defaults: Some(lsp::CompletionListItemDefaults {
17719 data: Some(default_data.clone()),
17720 commit_characters: Some(default_commit_characters.clone()),
17721 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
17722 default_edit_range,
17723 )),
17724 insert_text_format: Some(default_insert_text_format),
17725 insert_text_mode: Some(default_insert_text_mode),
17726 }),
17727 ..lsp::CompletionList::default()
17728 })))
17729 }
17730 })
17731 .next()
17732 .await;
17733
17734 let resolved_items = Arc::new(Mutex::new(Vec::new()));
17735 cx.lsp
17736 .server
17737 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
17738 let closure_resolved_items = resolved_items.clone();
17739 move |item_to_resolve, _| {
17740 let closure_resolved_items = closure_resolved_items.clone();
17741 async move {
17742 closure_resolved_items.lock().push(item_to_resolve.clone());
17743 Ok(item_to_resolve)
17744 }
17745 }
17746 })
17747 .detach();
17748
17749 cx.condition(|editor, _| editor.context_menu_visible())
17750 .await;
17751 cx.run_until_parked();
17752 cx.update_editor(|editor, _, _| {
17753 let menu = editor.context_menu.borrow_mut();
17754 match menu.as_ref().expect("should have the completions menu") {
17755 CodeContextMenu::Completions(completions_menu) => {
17756 assert_eq!(
17757 completions_menu
17758 .entries
17759 .borrow()
17760 .iter()
17761 .map(|mat| mat.string.clone())
17762 .collect::<Vec<String>>(),
17763 items
17764 .iter()
17765 .map(|completion| completion.label.clone())
17766 .collect::<Vec<String>>()
17767 );
17768 }
17769 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
17770 }
17771 });
17772 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
17773 // with 4 from the end.
17774 assert_eq!(
17775 *resolved_items.lock(),
17776 [&items[0..16], &items[items.len() - 4..items.len()]]
17777 .concat()
17778 .iter()
17779 .cloned()
17780 .map(|mut item| {
17781 if item.data.is_none() {
17782 item.data = Some(default_data.clone());
17783 }
17784 item
17785 })
17786 .collect::<Vec<lsp::CompletionItem>>(),
17787 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
17788 );
17789 resolved_items.lock().clear();
17790
17791 cx.update_editor(|editor, window, cx| {
17792 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
17793 });
17794 cx.run_until_parked();
17795 // Completions that have already been resolved are skipped.
17796 assert_eq!(
17797 *resolved_items.lock(),
17798 items[items.len() - 17..items.len() - 4]
17799 .iter()
17800 .cloned()
17801 .map(|mut item| {
17802 if item.data.is_none() {
17803 item.data = Some(default_data.clone());
17804 }
17805 item
17806 })
17807 .collect::<Vec<lsp::CompletionItem>>()
17808 );
17809 resolved_items.lock().clear();
17810}
17811
17812#[gpui::test]
17813async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
17814 init_test(cx, |_| {});
17815
17816 let mut cx = EditorLspTestContext::new(
17817 Language::new(
17818 LanguageConfig {
17819 matcher: LanguageMatcher {
17820 path_suffixes: vec!["jsx".into()],
17821 ..Default::default()
17822 },
17823 overrides: [(
17824 "element".into(),
17825 LanguageConfigOverride {
17826 completion_query_characters: Override::Set(['-'].into_iter().collect()),
17827 ..Default::default()
17828 },
17829 )]
17830 .into_iter()
17831 .collect(),
17832 ..Default::default()
17833 },
17834 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
17835 )
17836 .with_override_query("(jsx_self_closing_element) @element")
17837 .unwrap(),
17838 lsp::ServerCapabilities {
17839 completion_provider: Some(lsp::CompletionOptions {
17840 trigger_characters: Some(vec![":".to_string()]),
17841 ..Default::default()
17842 }),
17843 ..Default::default()
17844 },
17845 cx,
17846 )
17847 .await;
17848
17849 cx.lsp
17850 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
17851 Ok(Some(lsp::CompletionResponse::Array(vec![
17852 lsp::CompletionItem {
17853 label: "bg-blue".into(),
17854 ..Default::default()
17855 },
17856 lsp::CompletionItem {
17857 label: "bg-red".into(),
17858 ..Default::default()
17859 },
17860 lsp::CompletionItem {
17861 label: "bg-yellow".into(),
17862 ..Default::default()
17863 },
17864 ])))
17865 });
17866
17867 cx.set_state(r#"<p class="bgˇ" />"#);
17868
17869 // Trigger completion when typing a dash, because the dash is an extra
17870 // word character in the 'element' scope, which contains the cursor.
17871 cx.simulate_keystroke("-");
17872 cx.executor().run_until_parked();
17873 cx.update_editor(|editor, _, _| {
17874 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17875 {
17876 assert_eq!(
17877 completion_menu_entries(menu),
17878 &["bg-blue", "bg-red", "bg-yellow"]
17879 );
17880 } else {
17881 panic!("expected completion menu to be open");
17882 }
17883 });
17884
17885 cx.simulate_keystroke("l");
17886 cx.executor().run_until_parked();
17887 cx.update_editor(|editor, _, _| {
17888 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17889 {
17890 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
17891 } else {
17892 panic!("expected completion menu to be open");
17893 }
17894 });
17895
17896 // When filtering completions, consider the character after the '-' to
17897 // be the start of a subword.
17898 cx.set_state(r#"<p class="yelˇ" />"#);
17899 cx.simulate_keystroke("l");
17900 cx.executor().run_until_parked();
17901 cx.update_editor(|editor, _, _| {
17902 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17903 {
17904 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
17905 } else {
17906 panic!("expected completion menu to be open");
17907 }
17908 });
17909}
17910
17911fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
17912 let entries = menu.entries.borrow();
17913 entries.iter().map(|mat| mat.string.clone()).collect()
17914}
17915
17916#[gpui::test]
17917async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
17918 init_test(cx, |settings| {
17919 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
17920 Formatter::Prettier,
17921 )))
17922 });
17923
17924 let fs = FakeFs::new(cx.executor());
17925 fs.insert_file(path!("/file.ts"), Default::default()).await;
17926
17927 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
17928 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17929
17930 language_registry.add(Arc::new(Language::new(
17931 LanguageConfig {
17932 name: "TypeScript".into(),
17933 matcher: LanguageMatcher {
17934 path_suffixes: vec!["ts".to_string()],
17935 ..Default::default()
17936 },
17937 ..Default::default()
17938 },
17939 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
17940 )));
17941 update_test_language_settings(cx, |settings| {
17942 settings.defaults.prettier = Some(PrettierSettings {
17943 allowed: true,
17944 ..PrettierSettings::default()
17945 });
17946 });
17947
17948 let test_plugin = "test_plugin";
17949 let _ = language_registry.register_fake_lsp(
17950 "TypeScript",
17951 FakeLspAdapter {
17952 prettier_plugins: vec![test_plugin],
17953 ..Default::default()
17954 },
17955 );
17956
17957 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
17958 let buffer = project
17959 .update(cx, |project, cx| {
17960 project.open_local_buffer(path!("/file.ts"), cx)
17961 })
17962 .await
17963 .unwrap();
17964
17965 let buffer_text = "one\ntwo\nthree\n";
17966 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
17967 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
17968 editor.update_in(cx, |editor, window, cx| {
17969 editor.set_text(buffer_text, window, cx)
17970 });
17971
17972 editor
17973 .update_in(cx, |editor, window, cx| {
17974 editor.perform_format(
17975 project.clone(),
17976 FormatTrigger::Manual,
17977 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
17978 window,
17979 cx,
17980 )
17981 })
17982 .unwrap()
17983 .await;
17984 assert_eq!(
17985 editor.update(cx, |editor, cx| editor.text(cx)),
17986 buffer_text.to_string() + prettier_format_suffix,
17987 "Test prettier formatting was not applied to the original buffer text",
17988 );
17989
17990 update_test_language_settings(cx, |settings| {
17991 settings.defaults.formatter = Some(SelectedFormatter::Auto)
17992 });
17993 let format = editor.update_in(cx, |editor, window, cx| {
17994 editor.perform_format(
17995 project.clone(),
17996 FormatTrigger::Manual,
17997 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
17998 window,
17999 cx,
18000 )
18001 });
18002 format.await.unwrap();
18003 assert_eq!(
18004 editor.update(cx, |editor, cx| editor.text(cx)),
18005 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
18006 "Autoformatting (via test prettier) was not applied to the original buffer text",
18007 );
18008}
18009
18010#[gpui::test]
18011async fn test_addition_reverts(cx: &mut TestAppContext) {
18012 init_test(cx, |_| {});
18013 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18014 let base_text = indoc! {r#"
18015 struct Row;
18016 struct Row1;
18017 struct Row2;
18018
18019 struct Row4;
18020 struct Row5;
18021 struct Row6;
18022
18023 struct Row8;
18024 struct Row9;
18025 struct Row10;"#};
18026
18027 // When addition hunks are not adjacent to carets, no hunk revert is performed
18028 assert_hunk_revert(
18029 indoc! {r#"struct Row;
18030 struct Row1;
18031 struct Row1.1;
18032 struct Row1.2;
18033 struct Row2;ˇ
18034
18035 struct Row4;
18036 struct Row5;
18037 struct Row6;
18038
18039 struct Row8;
18040 ˇstruct Row9;
18041 struct Row9.1;
18042 struct Row9.2;
18043 struct Row9.3;
18044 struct Row10;"#},
18045 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18046 indoc! {r#"struct Row;
18047 struct Row1;
18048 struct Row1.1;
18049 struct Row1.2;
18050 struct Row2;ˇ
18051
18052 struct Row4;
18053 struct Row5;
18054 struct Row6;
18055
18056 struct Row8;
18057 ˇstruct Row9;
18058 struct Row9.1;
18059 struct Row9.2;
18060 struct Row9.3;
18061 struct Row10;"#},
18062 base_text,
18063 &mut cx,
18064 );
18065 // Same for selections
18066 assert_hunk_revert(
18067 indoc! {r#"struct Row;
18068 struct Row1;
18069 struct Row2;
18070 struct Row2.1;
18071 struct Row2.2;
18072 «ˇ
18073 struct Row4;
18074 struct» Row5;
18075 «struct Row6;
18076 ˇ»
18077 struct Row9.1;
18078 struct Row9.2;
18079 struct Row9.3;
18080 struct Row8;
18081 struct Row9;
18082 struct Row10;"#},
18083 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18084 indoc! {r#"struct Row;
18085 struct Row1;
18086 struct Row2;
18087 struct Row2.1;
18088 struct Row2.2;
18089 «ˇ
18090 struct Row4;
18091 struct» Row5;
18092 «struct Row6;
18093 ˇ»
18094 struct Row9.1;
18095 struct Row9.2;
18096 struct Row9.3;
18097 struct Row8;
18098 struct Row9;
18099 struct Row10;"#},
18100 base_text,
18101 &mut cx,
18102 );
18103
18104 // When carets and selections intersect the addition hunks, those are reverted.
18105 // Adjacent carets got merged.
18106 assert_hunk_revert(
18107 indoc! {r#"struct Row;
18108 ˇ// something on the top
18109 struct Row1;
18110 struct Row2;
18111 struct Roˇw3.1;
18112 struct Row2.2;
18113 struct Row2.3;ˇ
18114
18115 struct Row4;
18116 struct ˇRow5.1;
18117 struct Row5.2;
18118 struct «Rowˇ»5.3;
18119 struct Row5;
18120 struct Row6;
18121 ˇ
18122 struct Row9.1;
18123 struct «Rowˇ»9.2;
18124 struct «ˇRow»9.3;
18125 struct Row8;
18126 struct Row9;
18127 «ˇ// something on bottom»
18128 struct Row10;"#},
18129 vec![
18130 DiffHunkStatusKind::Added,
18131 DiffHunkStatusKind::Added,
18132 DiffHunkStatusKind::Added,
18133 DiffHunkStatusKind::Added,
18134 DiffHunkStatusKind::Added,
18135 ],
18136 indoc! {r#"struct Row;
18137 ˇstruct Row1;
18138 struct Row2;
18139 ˇ
18140 struct Row4;
18141 ˇstruct Row5;
18142 struct Row6;
18143 ˇ
18144 ˇstruct Row8;
18145 struct Row9;
18146 ˇstruct Row10;"#},
18147 base_text,
18148 &mut cx,
18149 );
18150}
18151
18152#[gpui::test]
18153async fn test_modification_reverts(cx: &mut TestAppContext) {
18154 init_test(cx, |_| {});
18155 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18156 let base_text = indoc! {r#"
18157 struct Row;
18158 struct Row1;
18159 struct Row2;
18160
18161 struct Row4;
18162 struct Row5;
18163 struct Row6;
18164
18165 struct Row8;
18166 struct Row9;
18167 struct Row10;"#};
18168
18169 // Modification hunks behave the same as the addition ones.
18170 assert_hunk_revert(
18171 indoc! {r#"struct Row;
18172 struct Row1;
18173 struct Row33;
18174 ˇ
18175 struct Row4;
18176 struct Row5;
18177 struct Row6;
18178 ˇ
18179 struct Row99;
18180 struct Row9;
18181 struct Row10;"#},
18182 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18183 indoc! {r#"struct Row;
18184 struct Row1;
18185 struct Row33;
18186 ˇ
18187 struct Row4;
18188 struct Row5;
18189 struct Row6;
18190 ˇ
18191 struct Row99;
18192 struct Row9;
18193 struct Row10;"#},
18194 base_text,
18195 &mut cx,
18196 );
18197 assert_hunk_revert(
18198 indoc! {r#"struct Row;
18199 struct Row1;
18200 struct Row33;
18201 «ˇ
18202 struct Row4;
18203 struct» Row5;
18204 «struct Row6;
18205 ˇ»
18206 struct Row99;
18207 struct Row9;
18208 struct Row10;"#},
18209 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18210 indoc! {r#"struct Row;
18211 struct Row1;
18212 struct Row33;
18213 «ˇ
18214 struct Row4;
18215 struct» Row5;
18216 «struct Row6;
18217 ˇ»
18218 struct Row99;
18219 struct Row9;
18220 struct Row10;"#},
18221 base_text,
18222 &mut cx,
18223 );
18224
18225 assert_hunk_revert(
18226 indoc! {r#"ˇstruct Row1.1;
18227 struct Row1;
18228 «ˇstr»uct Row22;
18229
18230 struct ˇRow44;
18231 struct Row5;
18232 struct «Rˇ»ow66;ˇ
18233
18234 «struˇ»ct Row88;
18235 struct Row9;
18236 struct Row1011;ˇ"#},
18237 vec![
18238 DiffHunkStatusKind::Modified,
18239 DiffHunkStatusKind::Modified,
18240 DiffHunkStatusKind::Modified,
18241 DiffHunkStatusKind::Modified,
18242 DiffHunkStatusKind::Modified,
18243 DiffHunkStatusKind::Modified,
18244 ],
18245 indoc! {r#"struct Row;
18246 ˇstruct Row1;
18247 struct Row2;
18248 ˇ
18249 struct Row4;
18250 ˇstruct Row5;
18251 struct Row6;
18252 ˇ
18253 struct Row8;
18254 ˇstruct Row9;
18255 struct Row10;ˇ"#},
18256 base_text,
18257 &mut cx,
18258 );
18259}
18260
18261#[gpui::test]
18262async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
18263 init_test(cx, |_| {});
18264 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18265 let base_text = indoc! {r#"
18266 one
18267
18268 two
18269 three
18270 "#};
18271
18272 cx.set_head_text(base_text);
18273 cx.set_state("\nˇ\n");
18274 cx.executor().run_until_parked();
18275 cx.update_editor(|editor, _window, cx| {
18276 editor.expand_selected_diff_hunks(cx);
18277 });
18278 cx.executor().run_until_parked();
18279 cx.update_editor(|editor, window, cx| {
18280 editor.backspace(&Default::default(), window, cx);
18281 });
18282 cx.run_until_parked();
18283 cx.assert_state_with_diff(
18284 indoc! {r#"
18285
18286 - two
18287 - threeˇ
18288 +
18289 "#}
18290 .to_string(),
18291 );
18292}
18293
18294#[gpui::test]
18295async fn test_deletion_reverts(cx: &mut TestAppContext) {
18296 init_test(cx, |_| {});
18297 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18298 let base_text = indoc! {r#"struct Row;
18299struct Row1;
18300struct Row2;
18301
18302struct Row4;
18303struct Row5;
18304struct Row6;
18305
18306struct Row8;
18307struct Row9;
18308struct Row10;"#};
18309
18310 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
18311 assert_hunk_revert(
18312 indoc! {r#"struct Row;
18313 struct Row2;
18314
18315 ˇstruct Row4;
18316 struct Row5;
18317 struct Row6;
18318 ˇ
18319 struct Row8;
18320 struct Row10;"#},
18321 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18322 indoc! {r#"struct Row;
18323 struct Row2;
18324
18325 ˇstruct Row4;
18326 struct Row5;
18327 struct Row6;
18328 ˇ
18329 struct Row8;
18330 struct Row10;"#},
18331 base_text,
18332 &mut cx,
18333 );
18334 assert_hunk_revert(
18335 indoc! {r#"struct Row;
18336 struct Row2;
18337
18338 «ˇstruct Row4;
18339 struct» Row5;
18340 «struct Row6;
18341 ˇ»
18342 struct Row8;
18343 struct Row10;"#},
18344 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18345 indoc! {r#"struct Row;
18346 struct Row2;
18347
18348 «ˇstruct Row4;
18349 struct» Row5;
18350 «struct Row6;
18351 ˇ»
18352 struct Row8;
18353 struct Row10;"#},
18354 base_text,
18355 &mut cx,
18356 );
18357
18358 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
18359 assert_hunk_revert(
18360 indoc! {r#"struct Row;
18361 ˇstruct Row2;
18362
18363 struct Row4;
18364 struct Row5;
18365 struct Row6;
18366
18367 struct Row8;ˇ
18368 struct Row10;"#},
18369 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18370 indoc! {r#"struct Row;
18371 struct Row1;
18372 ˇstruct Row2;
18373
18374 struct Row4;
18375 struct Row5;
18376 struct Row6;
18377
18378 struct Row8;ˇ
18379 struct Row9;
18380 struct Row10;"#},
18381 base_text,
18382 &mut cx,
18383 );
18384 assert_hunk_revert(
18385 indoc! {r#"struct Row;
18386 struct Row2«ˇ;
18387 struct Row4;
18388 struct» Row5;
18389 «struct Row6;
18390
18391 struct Row8;ˇ»
18392 struct Row10;"#},
18393 vec![
18394 DiffHunkStatusKind::Deleted,
18395 DiffHunkStatusKind::Deleted,
18396 DiffHunkStatusKind::Deleted,
18397 ],
18398 indoc! {r#"struct Row;
18399 struct Row1;
18400 struct Row2«ˇ;
18401
18402 struct Row4;
18403 struct» Row5;
18404 «struct Row6;
18405
18406 struct Row8;ˇ»
18407 struct Row9;
18408 struct Row10;"#},
18409 base_text,
18410 &mut cx,
18411 );
18412}
18413
18414#[gpui::test]
18415async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
18416 init_test(cx, |_| {});
18417
18418 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
18419 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
18420 let base_text_3 =
18421 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
18422
18423 let text_1 = edit_first_char_of_every_line(base_text_1);
18424 let text_2 = edit_first_char_of_every_line(base_text_2);
18425 let text_3 = edit_first_char_of_every_line(base_text_3);
18426
18427 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
18428 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
18429 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
18430
18431 let multibuffer = cx.new(|cx| {
18432 let mut multibuffer = MultiBuffer::new(ReadWrite);
18433 multibuffer.push_excerpts(
18434 buffer_1.clone(),
18435 [
18436 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18437 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18438 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18439 ],
18440 cx,
18441 );
18442 multibuffer.push_excerpts(
18443 buffer_2.clone(),
18444 [
18445 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18446 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18447 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18448 ],
18449 cx,
18450 );
18451 multibuffer.push_excerpts(
18452 buffer_3.clone(),
18453 [
18454 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18455 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18456 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18457 ],
18458 cx,
18459 );
18460 multibuffer
18461 });
18462
18463 let fs = FakeFs::new(cx.executor());
18464 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
18465 let (editor, cx) = cx
18466 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
18467 editor.update_in(cx, |editor, _window, cx| {
18468 for (buffer, diff_base) in [
18469 (buffer_1.clone(), base_text_1),
18470 (buffer_2.clone(), base_text_2),
18471 (buffer_3.clone(), base_text_3),
18472 ] {
18473 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
18474 editor
18475 .buffer
18476 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
18477 }
18478 });
18479 cx.executor().run_until_parked();
18480
18481 editor.update_in(cx, |editor, window, cx| {
18482 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}");
18483 editor.select_all(&SelectAll, window, cx);
18484 editor.git_restore(&Default::default(), window, cx);
18485 });
18486 cx.executor().run_until_parked();
18487
18488 // When all ranges are selected, all buffer hunks are reverted.
18489 editor.update(cx, |editor, cx| {
18490 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");
18491 });
18492 buffer_1.update(cx, |buffer, _| {
18493 assert_eq!(buffer.text(), base_text_1);
18494 });
18495 buffer_2.update(cx, |buffer, _| {
18496 assert_eq!(buffer.text(), base_text_2);
18497 });
18498 buffer_3.update(cx, |buffer, _| {
18499 assert_eq!(buffer.text(), base_text_3);
18500 });
18501
18502 editor.update_in(cx, |editor, window, cx| {
18503 editor.undo(&Default::default(), window, cx);
18504 });
18505
18506 editor.update_in(cx, |editor, window, cx| {
18507 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18508 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
18509 });
18510 editor.git_restore(&Default::default(), window, cx);
18511 });
18512
18513 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
18514 // but not affect buffer_2 and its related excerpts.
18515 editor.update(cx, |editor, cx| {
18516 assert_eq!(
18517 editor.text(cx),
18518 "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}"
18519 );
18520 });
18521 buffer_1.update(cx, |buffer, _| {
18522 assert_eq!(buffer.text(), base_text_1);
18523 });
18524 buffer_2.update(cx, |buffer, _| {
18525 assert_eq!(
18526 buffer.text(),
18527 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
18528 );
18529 });
18530 buffer_3.update(cx, |buffer, _| {
18531 assert_eq!(
18532 buffer.text(),
18533 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
18534 );
18535 });
18536
18537 fn edit_first_char_of_every_line(text: &str) -> String {
18538 text.split('\n')
18539 .map(|line| format!("X{}", &line[1..]))
18540 .collect::<Vec<_>>()
18541 .join("\n")
18542 }
18543}
18544
18545#[gpui::test]
18546async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
18547 init_test(cx, |_| {});
18548
18549 let cols = 4;
18550 let rows = 10;
18551 let sample_text_1 = sample_text(rows, cols, 'a');
18552 assert_eq!(
18553 sample_text_1,
18554 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
18555 );
18556 let sample_text_2 = sample_text(rows, cols, 'l');
18557 assert_eq!(
18558 sample_text_2,
18559 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
18560 );
18561 let sample_text_3 = sample_text(rows, cols, 'v');
18562 assert_eq!(
18563 sample_text_3,
18564 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
18565 );
18566
18567 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
18568 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
18569 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
18570
18571 let multi_buffer = cx.new(|cx| {
18572 let mut multibuffer = MultiBuffer::new(ReadWrite);
18573 multibuffer.push_excerpts(
18574 buffer_1.clone(),
18575 [
18576 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18577 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18578 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18579 ],
18580 cx,
18581 );
18582 multibuffer.push_excerpts(
18583 buffer_2.clone(),
18584 [
18585 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18586 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18587 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18588 ],
18589 cx,
18590 );
18591 multibuffer.push_excerpts(
18592 buffer_3.clone(),
18593 [
18594 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18595 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18596 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18597 ],
18598 cx,
18599 );
18600 multibuffer
18601 });
18602
18603 let fs = FakeFs::new(cx.executor());
18604 fs.insert_tree(
18605 "/a",
18606 json!({
18607 "main.rs": sample_text_1,
18608 "other.rs": sample_text_2,
18609 "lib.rs": sample_text_3,
18610 }),
18611 )
18612 .await;
18613 let project = Project::test(fs, ["/a".as_ref()], cx).await;
18614 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18615 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18616 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18617 Editor::new(
18618 EditorMode::full(),
18619 multi_buffer,
18620 Some(project.clone()),
18621 window,
18622 cx,
18623 )
18624 });
18625 let multibuffer_item_id = workspace
18626 .update(cx, |workspace, window, cx| {
18627 assert!(
18628 workspace.active_item(cx).is_none(),
18629 "active item should be None before the first item is added"
18630 );
18631 workspace.add_item_to_active_pane(
18632 Box::new(multi_buffer_editor.clone()),
18633 None,
18634 true,
18635 window,
18636 cx,
18637 );
18638 let active_item = workspace
18639 .active_item(cx)
18640 .expect("should have an active item after adding the multi buffer");
18641 assert!(
18642 !active_item.is_singleton(cx),
18643 "A multi buffer was expected to active after adding"
18644 );
18645 active_item.item_id()
18646 })
18647 .unwrap();
18648 cx.executor().run_until_parked();
18649
18650 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18651 editor.change_selections(
18652 SelectionEffects::scroll(Autoscroll::Next),
18653 window,
18654 cx,
18655 |s| s.select_ranges(Some(1..2)),
18656 );
18657 editor.open_excerpts(&OpenExcerpts, window, cx);
18658 });
18659 cx.executor().run_until_parked();
18660 let first_item_id = workspace
18661 .update(cx, |workspace, window, cx| {
18662 let active_item = workspace
18663 .active_item(cx)
18664 .expect("should have an active item after navigating into the 1st buffer");
18665 let first_item_id = active_item.item_id();
18666 assert_ne!(
18667 first_item_id, multibuffer_item_id,
18668 "Should navigate into the 1st buffer and activate it"
18669 );
18670 assert!(
18671 active_item.is_singleton(cx),
18672 "New active item should be a singleton buffer"
18673 );
18674 assert_eq!(
18675 active_item
18676 .act_as::<Editor>(cx)
18677 .expect("should have navigated into an editor for the 1st buffer")
18678 .read(cx)
18679 .text(cx),
18680 sample_text_1
18681 );
18682
18683 workspace
18684 .go_back(workspace.active_pane().downgrade(), window, cx)
18685 .detach_and_log_err(cx);
18686
18687 first_item_id
18688 })
18689 .unwrap();
18690 cx.executor().run_until_parked();
18691 workspace
18692 .update(cx, |workspace, _, cx| {
18693 let active_item = workspace
18694 .active_item(cx)
18695 .expect("should have an active item after navigating back");
18696 assert_eq!(
18697 active_item.item_id(),
18698 multibuffer_item_id,
18699 "Should navigate back to the multi buffer"
18700 );
18701 assert!(!active_item.is_singleton(cx));
18702 })
18703 .unwrap();
18704
18705 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18706 editor.change_selections(
18707 SelectionEffects::scroll(Autoscroll::Next),
18708 window,
18709 cx,
18710 |s| s.select_ranges(Some(39..40)),
18711 );
18712 editor.open_excerpts(&OpenExcerpts, window, cx);
18713 });
18714 cx.executor().run_until_parked();
18715 let second_item_id = workspace
18716 .update(cx, |workspace, window, cx| {
18717 let active_item = workspace
18718 .active_item(cx)
18719 .expect("should have an active item after navigating into the 2nd buffer");
18720 let second_item_id = active_item.item_id();
18721 assert_ne!(
18722 second_item_id, multibuffer_item_id,
18723 "Should navigate away from the multibuffer"
18724 );
18725 assert_ne!(
18726 second_item_id, first_item_id,
18727 "Should navigate into the 2nd buffer and activate it"
18728 );
18729 assert!(
18730 active_item.is_singleton(cx),
18731 "New active item should be a singleton buffer"
18732 );
18733 assert_eq!(
18734 active_item
18735 .act_as::<Editor>(cx)
18736 .expect("should have navigated into an editor")
18737 .read(cx)
18738 .text(cx),
18739 sample_text_2
18740 );
18741
18742 workspace
18743 .go_back(workspace.active_pane().downgrade(), window, cx)
18744 .detach_and_log_err(cx);
18745
18746 second_item_id
18747 })
18748 .unwrap();
18749 cx.executor().run_until_parked();
18750 workspace
18751 .update(cx, |workspace, _, cx| {
18752 let active_item = workspace
18753 .active_item(cx)
18754 .expect("should have an active item after navigating back from the 2nd buffer");
18755 assert_eq!(
18756 active_item.item_id(),
18757 multibuffer_item_id,
18758 "Should navigate back from the 2nd buffer to the multi buffer"
18759 );
18760 assert!(!active_item.is_singleton(cx));
18761 })
18762 .unwrap();
18763
18764 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18765 editor.change_selections(
18766 SelectionEffects::scroll(Autoscroll::Next),
18767 window,
18768 cx,
18769 |s| s.select_ranges(Some(70..70)),
18770 );
18771 editor.open_excerpts(&OpenExcerpts, window, cx);
18772 });
18773 cx.executor().run_until_parked();
18774 workspace
18775 .update(cx, |workspace, window, cx| {
18776 let active_item = workspace
18777 .active_item(cx)
18778 .expect("should have an active item after navigating into the 3rd buffer");
18779 let third_item_id = active_item.item_id();
18780 assert_ne!(
18781 third_item_id, multibuffer_item_id,
18782 "Should navigate into the 3rd buffer and activate it"
18783 );
18784 assert_ne!(third_item_id, first_item_id);
18785 assert_ne!(third_item_id, second_item_id);
18786 assert!(
18787 active_item.is_singleton(cx),
18788 "New active item should be a singleton buffer"
18789 );
18790 assert_eq!(
18791 active_item
18792 .act_as::<Editor>(cx)
18793 .expect("should have navigated into an editor")
18794 .read(cx)
18795 .text(cx),
18796 sample_text_3
18797 );
18798
18799 workspace
18800 .go_back(workspace.active_pane().downgrade(), window, cx)
18801 .detach_and_log_err(cx);
18802 })
18803 .unwrap();
18804 cx.executor().run_until_parked();
18805 workspace
18806 .update(cx, |workspace, _, cx| {
18807 let active_item = workspace
18808 .active_item(cx)
18809 .expect("should have an active item after navigating back from the 3rd buffer");
18810 assert_eq!(
18811 active_item.item_id(),
18812 multibuffer_item_id,
18813 "Should navigate back from the 3rd buffer to the multi buffer"
18814 );
18815 assert!(!active_item.is_singleton(cx));
18816 })
18817 .unwrap();
18818}
18819
18820#[gpui::test]
18821async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
18822 init_test(cx, |_| {});
18823
18824 let mut cx = EditorTestContext::new(cx).await;
18825
18826 let diff_base = r#"
18827 use some::mod;
18828
18829 const A: u32 = 42;
18830
18831 fn main() {
18832 println!("hello");
18833
18834 println!("world");
18835 }
18836 "#
18837 .unindent();
18838
18839 cx.set_state(
18840 &r#"
18841 use some::modified;
18842
18843 ˇ
18844 fn main() {
18845 println!("hello there");
18846
18847 println!("around the");
18848 println!("world");
18849 }
18850 "#
18851 .unindent(),
18852 );
18853
18854 cx.set_head_text(&diff_base);
18855 executor.run_until_parked();
18856
18857 cx.update_editor(|editor, window, cx| {
18858 editor.go_to_next_hunk(&GoToHunk, window, cx);
18859 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
18860 });
18861 executor.run_until_parked();
18862 cx.assert_state_with_diff(
18863 r#"
18864 use some::modified;
18865
18866
18867 fn main() {
18868 - println!("hello");
18869 + ˇ println!("hello there");
18870
18871 println!("around the");
18872 println!("world");
18873 }
18874 "#
18875 .unindent(),
18876 );
18877
18878 cx.update_editor(|editor, window, cx| {
18879 for _ in 0..2 {
18880 editor.go_to_next_hunk(&GoToHunk, window, cx);
18881 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
18882 }
18883 });
18884 executor.run_until_parked();
18885 cx.assert_state_with_diff(
18886 r#"
18887 - use some::mod;
18888 + ˇuse some::modified;
18889
18890
18891 fn main() {
18892 - println!("hello");
18893 + println!("hello there");
18894
18895 + println!("around the");
18896 println!("world");
18897 }
18898 "#
18899 .unindent(),
18900 );
18901
18902 cx.update_editor(|editor, window, cx| {
18903 editor.go_to_next_hunk(&GoToHunk, window, cx);
18904 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
18905 });
18906 executor.run_until_parked();
18907 cx.assert_state_with_diff(
18908 r#"
18909 - use some::mod;
18910 + use some::modified;
18911
18912 - const A: u32 = 42;
18913 ˇ
18914 fn main() {
18915 - println!("hello");
18916 + println!("hello there");
18917
18918 + println!("around the");
18919 println!("world");
18920 }
18921 "#
18922 .unindent(),
18923 );
18924
18925 cx.update_editor(|editor, window, cx| {
18926 editor.cancel(&Cancel, window, cx);
18927 });
18928
18929 cx.assert_state_with_diff(
18930 r#"
18931 use some::modified;
18932
18933 ˇ
18934 fn main() {
18935 println!("hello there");
18936
18937 println!("around the");
18938 println!("world");
18939 }
18940 "#
18941 .unindent(),
18942 );
18943}
18944
18945#[gpui::test]
18946async fn test_diff_base_change_with_expanded_diff_hunks(
18947 executor: BackgroundExecutor,
18948 cx: &mut TestAppContext,
18949) {
18950 init_test(cx, |_| {});
18951
18952 let mut cx = EditorTestContext::new(cx).await;
18953
18954 let diff_base = r#"
18955 use some::mod1;
18956 use some::mod2;
18957
18958 const A: u32 = 42;
18959 const B: u32 = 42;
18960 const C: u32 = 42;
18961
18962 fn main() {
18963 println!("hello");
18964
18965 println!("world");
18966 }
18967 "#
18968 .unindent();
18969
18970 cx.set_state(
18971 &r#"
18972 use some::mod2;
18973
18974 const A: u32 = 42;
18975 const C: u32 = 42;
18976
18977 fn main(ˇ) {
18978 //println!("hello");
18979
18980 println!("world");
18981 //
18982 //
18983 }
18984 "#
18985 .unindent(),
18986 );
18987
18988 cx.set_head_text(&diff_base);
18989 executor.run_until_parked();
18990
18991 cx.update_editor(|editor, window, cx| {
18992 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18993 });
18994 executor.run_until_parked();
18995 cx.assert_state_with_diff(
18996 r#"
18997 - use some::mod1;
18998 use some::mod2;
18999
19000 const A: u32 = 42;
19001 - const B: u32 = 42;
19002 const C: u32 = 42;
19003
19004 fn main(ˇ) {
19005 - println!("hello");
19006 + //println!("hello");
19007
19008 println!("world");
19009 + //
19010 + //
19011 }
19012 "#
19013 .unindent(),
19014 );
19015
19016 cx.set_head_text("new diff base!");
19017 executor.run_until_parked();
19018 cx.assert_state_with_diff(
19019 r#"
19020 - new diff base!
19021 + use some::mod2;
19022 +
19023 + const A: u32 = 42;
19024 + const C: u32 = 42;
19025 +
19026 + fn main(ˇ) {
19027 + //println!("hello");
19028 +
19029 + println!("world");
19030 + //
19031 + //
19032 + }
19033 "#
19034 .unindent(),
19035 );
19036}
19037
19038#[gpui::test]
19039async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
19040 init_test(cx, |_| {});
19041
19042 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19043 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19044 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19045 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19046 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
19047 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
19048
19049 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
19050 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
19051 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
19052
19053 let multi_buffer = cx.new(|cx| {
19054 let mut multibuffer = MultiBuffer::new(ReadWrite);
19055 multibuffer.push_excerpts(
19056 buffer_1.clone(),
19057 [
19058 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19059 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19060 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19061 ],
19062 cx,
19063 );
19064 multibuffer.push_excerpts(
19065 buffer_2.clone(),
19066 [
19067 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19068 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19069 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19070 ],
19071 cx,
19072 );
19073 multibuffer.push_excerpts(
19074 buffer_3.clone(),
19075 [
19076 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19077 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19078 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19079 ],
19080 cx,
19081 );
19082 multibuffer
19083 });
19084
19085 let editor =
19086 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19087 editor
19088 .update(cx, |editor, _window, cx| {
19089 for (buffer, diff_base) in [
19090 (buffer_1.clone(), file_1_old),
19091 (buffer_2.clone(), file_2_old),
19092 (buffer_3.clone(), file_3_old),
19093 ] {
19094 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
19095 editor
19096 .buffer
19097 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
19098 }
19099 })
19100 .unwrap();
19101
19102 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19103 cx.run_until_parked();
19104
19105 cx.assert_editor_state(
19106 &"
19107 ˇaaa
19108 ccc
19109 ddd
19110
19111 ggg
19112 hhh
19113
19114
19115 lll
19116 mmm
19117 NNN
19118
19119 qqq
19120 rrr
19121
19122 uuu
19123 111
19124 222
19125 333
19126
19127 666
19128 777
19129
19130 000
19131 !!!"
19132 .unindent(),
19133 );
19134
19135 cx.update_editor(|editor, window, cx| {
19136 editor.select_all(&SelectAll, window, cx);
19137 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19138 });
19139 cx.executor().run_until_parked();
19140
19141 cx.assert_state_with_diff(
19142 "
19143 «aaa
19144 - bbb
19145 ccc
19146 ddd
19147
19148 ggg
19149 hhh
19150
19151
19152 lll
19153 mmm
19154 - nnn
19155 + NNN
19156
19157 qqq
19158 rrr
19159
19160 uuu
19161 111
19162 222
19163 333
19164
19165 + 666
19166 777
19167
19168 000
19169 !!!ˇ»"
19170 .unindent(),
19171 );
19172}
19173
19174#[gpui::test]
19175async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
19176 init_test(cx, |_| {});
19177
19178 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
19179 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
19180
19181 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
19182 let multi_buffer = cx.new(|cx| {
19183 let mut multibuffer = MultiBuffer::new(ReadWrite);
19184 multibuffer.push_excerpts(
19185 buffer.clone(),
19186 [
19187 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
19188 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
19189 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
19190 ],
19191 cx,
19192 );
19193 multibuffer
19194 });
19195
19196 let editor =
19197 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19198 editor
19199 .update(cx, |editor, _window, cx| {
19200 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
19201 editor
19202 .buffer
19203 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
19204 })
19205 .unwrap();
19206
19207 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19208 cx.run_until_parked();
19209
19210 cx.update_editor(|editor, window, cx| {
19211 editor.expand_all_diff_hunks(&Default::default(), window, cx)
19212 });
19213 cx.executor().run_until_parked();
19214
19215 // When the start of a hunk coincides with the start of its excerpt,
19216 // the hunk is expanded. When the start of a a hunk is earlier than
19217 // the start of its excerpt, the hunk is not expanded.
19218 cx.assert_state_with_diff(
19219 "
19220 ˇaaa
19221 - bbb
19222 + BBB
19223
19224 - ddd
19225 - eee
19226 + DDD
19227 + EEE
19228 fff
19229
19230 iii
19231 "
19232 .unindent(),
19233 );
19234}
19235
19236#[gpui::test]
19237async fn test_edits_around_expanded_insertion_hunks(
19238 executor: BackgroundExecutor,
19239 cx: &mut TestAppContext,
19240) {
19241 init_test(cx, |_| {});
19242
19243 let mut cx = EditorTestContext::new(cx).await;
19244
19245 let diff_base = r#"
19246 use some::mod1;
19247 use some::mod2;
19248
19249 const A: u32 = 42;
19250
19251 fn main() {
19252 println!("hello");
19253
19254 println!("world");
19255 }
19256 "#
19257 .unindent();
19258 executor.run_until_parked();
19259 cx.set_state(
19260 &r#"
19261 use some::mod1;
19262 use some::mod2;
19263
19264 const A: u32 = 42;
19265 const B: u32 = 42;
19266 const C: u32 = 42;
19267 ˇ
19268
19269 fn main() {
19270 println!("hello");
19271
19272 println!("world");
19273 }
19274 "#
19275 .unindent(),
19276 );
19277
19278 cx.set_head_text(&diff_base);
19279 executor.run_until_parked();
19280
19281 cx.update_editor(|editor, window, cx| {
19282 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19283 });
19284 executor.run_until_parked();
19285
19286 cx.assert_state_with_diff(
19287 r#"
19288 use some::mod1;
19289 use some::mod2;
19290
19291 const A: u32 = 42;
19292 + const B: u32 = 42;
19293 + const C: u32 = 42;
19294 + ˇ
19295
19296 fn main() {
19297 println!("hello");
19298
19299 println!("world");
19300 }
19301 "#
19302 .unindent(),
19303 );
19304
19305 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
19306 executor.run_until_parked();
19307
19308 cx.assert_state_with_diff(
19309 r#"
19310 use some::mod1;
19311 use some::mod2;
19312
19313 const A: u32 = 42;
19314 + const B: u32 = 42;
19315 + const C: u32 = 42;
19316 + const D: u32 = 42;
19317 + ˇ
19318
19319 fn main() {
19320 println!("hello");
19321
19322 println!("world");
19323 }
19324 "#
19325 .unindent(),
19326 );
19327
19328 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
19329 executor.run_until_parked();
19330
19331 cx.assert_state_with_diff(
19332 r#"
19333 use some::mod1;
19334 use some::mod2;
19335
19336 const A: u32 = 42;
19337 + const B: u32 = 42;
19338 + const C: u32 = 42;
19339 + const D: u32 = 42;
19340 + const E: u32 = 42;
19341 + ˇ
19342
19343 fn main() {
19344 println!("hello");
19345
19346 println!("world");
19347 }
19348 "#
19349 .unindent(),
19350 );
19351
19352 cx.update_editor(|editor, window, cx| {
19353 editor.delete_line(&DeleteLine, window, cx);
19354 });
19355 executor.run_until_parked();
19356
19357 cx.assert_state_with_diff(
19358 r#"
19359 use some::mod1;
19360 use some::mod2;
19361
19362 const A: u32 = 42;
19363 + const B: u32 = 42;
19364 + const C: u32 = 42;
19365 + const D: u32 = 42;
19366 + const E: u32 = 42;
19367 ˇ
19368 fn main() {
19369 println!("hello");
19370
19371 println!("world");
19372 }
19373 "#
19374 .unindent(),
19375 );
19376
19377 cx.update_editor(|editor, window, cx| {
19378 editor.move_up(&MoveUp, window, cx);
19379 editor.delete_line(&DeleteLine, window, cx);
19380 editor.move_up(&MoveUp, window, cx);
19381 editor.delete_line(&DeleteLine, window, cx);
19382 editor.move_up(&MoveUp, window, cx);
19383 editor.delete_line(&DeleteLine, window, cx);
19384 });
19385 executor.run_until_parked();
19386 cx.assert_state_with_diff(
19387 r#"
19388 use some::mod1;
19389 use some::mod2;
19390
19391 const A: u32 = 42;
19392 + const B: u32 = 42;
19393 ˇ
19394 fn main() {
19395 println!("hello");
19396
19397 println!("world");
19398 }
19399 "#
19400 .unindent(),
19401 );
19402
19403 cx.update_editor(|editor, window, cx| {
19404 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
19405 editor.delete_line(&DeleteLine, window, cx);
19406 });
19407 executor.run_until_parked();
19408 cx.assert_state_with_diff(
19409 r#"
19410 ˇ
19411 fn main() {
19412 println!("hello");
19413
19414 println!("world");
19415 }
19416 "#
19417 .unindent(),
19418 );
19419}
19420
19421#[gpui::test]
19422async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
19423 init_test(cx, |_| {});
19424
19425 let mut cx = EditorTestContext::new(cx).await;
19426 cx.set_head_text(indoc! { "
19427 one
19428 two
19429 three
19430 four
19431 five
19432 "
19433 });
19434 cx.set_state(indoc! { "
19435 one
19436 ˇthree
19437 five
19438 "});
19439 cx.run_until_parked();
19440 cx.update_editor(|editor, window, cx| {
19441 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19442 });
19443 cx.assert_state_with_diff(
19444 indoc! { "
19445 one
19446 - two
19447 ˇthree
19448 - four
19449 five
19450 "}
19451 .to_string(),
19452 );
19453 cx.update_editor(|editor, window, cx| {
19454 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19455 });
19456
19457 cx.assert_state_with_diff(
19458 indoc! { "
19459 one
19460 ˇthree
19461 five
19462 "}
19463 .to_string(),
19464 );
19465
19466 cx.set_state(indoc! { "
19467 one
19468 ˇTWO
19469 three
19470 four
19471 five
19472 "});
19473 cx.run_until_parked();
19474 cx.update_editor(|editor, window, cx| {
19475 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19476 });
19477
19478 cx.assert_state_with_diff(
19479 indoc! { "
19480 one
19481 - two
19482 + ˇTWO
19483 three
19484 four
19485 five
19486 "}
19487 .to_string(),
19488 );
19489 cx.update_editor(|editor, window, cx| {
19490 editor.move_up(&Default::default(), window, cx);
19491 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19492 });
19493 cx.assert_state_with_diff(
19494 indoc! { "
19495 one
19496 ˇTWO
19497 three
19498 four
19499 five
19500 "}
19501 .to_string(),
19502 );
19503}
19504
19505#[gpui::test]
19506async fn test_edits_around_expanded_deletion_hunks(
19507 executor: BackgroundExecutor,
19508 cx: &mut TestAppContext,
19509) {
19510 init_test(cx, |_| {});
19511
19512 let mut cx = EditorTestContext::new(cx).await;
19513
19514 let diff_base = r#"
19515 use some::mod1;
19516 use some::mod2;
19517
19518 const A: u32 = 42;
19519 const B: u32 = 42;
19520 const C: u32 = 42;
19521
19522
19523 fn main() {
19524 println!("hello");
19525
19526 println!("world");
19527 }
19528 "#
19529 .unindent();
19530 executor.run_until_parked();
19531 cx.set_state(
19532 &r#"
19533 use some::mod1;
19534 use some::mod2;
19535
19536 ˇconst B: u32 = 42;
19537 const C: u32 = 42;
19538
19539
19540 fn main() {
19541 println!("hello");
19542
19543 println!("world");
19544 }
19545 "#
19546 .unindent(),
19547 );
19548
19549 cx.set_head_text(&diff_base);
19550 executor.run_until_parked();
19551
19552 cx.update_editor(|editor, window, cx| {
19553 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19554 });
19555 executor.run_until_parked();
19556
19557 cx.assert_state_with_diff(
19558 r#"
19559 use some::mod1;
19560 use some::mod2;
19561
19562 - const A: u32 = 42;
19563 ˇconst B: u32 = 42;
19564 const C: u32 = 42;
19565
19566
19567 fn main() {
19568 println!("hello");
19569
19570 println!("world");
19571 }
19572 "#
19573 .unindent(),
19574 );
19575
19576 cx.update_editor(|editor, window, cx| {
19577 editor.delete_line(&DeleteLine, window, cx);
19578 });
19579 executor.run_until_parked();
19580 cx.assert_state_with_diff(
19581 r#"
19582 use some::mod1;
19583 use some::mod2;
19584
19585 - const A: u32 = 42;
19586 - const B: u32 = 42;
19587 ˇconst C: u32 = 42;
19588
19589
19590 fn main() {
19591 println!("hello");
19592
19593 println!("world");
19594 }
19595 "#
19596 .unindent(),
19597 );
19598
19599 cx.update_editor(|editor, window, cx| {
19600 editor.delete_line(&DeleteLine, window, cx);
19601 });
19602 executor.run_until_parked();
19603 cx.assert_state_with_diff(
19604 r#"
19605 use some::mod1;
19606 use some::mod2;
19607
19608 - const A: u32 = 42;
19609 - const B: u32 = 42;
19610 - const C: u32 = 42;
19611 ˇ
19612
19613 fn main() {
19614 println!("hello");
19615
19616 println!("world");
19617 }
19618 "#
19619 .unindent(),
19620 );
19621
19622 cx.update_editor(|editor, window, cx| {
19623 editor.handle_input("replacement", window, cx);
19624 });
19625 executor.run_until_parked();
19626 cx.assert_state_with_diff(
19627 r#"
19628 use some::mod1;
19629 use some::mod2;
19630
19631 - const A: u32 = 42;
19632 - const B: u32 = 42;
19633 - const C: u32 = 42;
19634 -
19635 + replacementˇ
19636
19637 fn main() {
19638 println!("hello");
19639
19640 println!("world");
19641 }
19642 "#
19643 .unindent(),
19644 );
19645}
19646
19647#[gpui::test]
19648async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19649 init_test(cx, |_| {});
19650
19651 let mut cx = EditorTestContext::new(cx).await;
19652
19653 let base_text = r#"
19654 one
19655 two
19656 three
19657 four
19658 five
19659 "#
19660 .unindent();
19661 executor.run_until_parked();
19662 cx.set_state(
19663 &r#"
19664 one
19665 two
19666 fˇour
19667 five
19668 "#
19669 .unindent(),
19670 );
19671
19672 cx.set_head_text(&base_text);
19673 executor.run_until_parked();
19674
19675 cx.update_editor(|editor, window, cx| {
19676 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19677 });
19678 executor.run_until_parked();
19679
19680 cx.assert_state_with_diff(
19681 r#"
19682 one
19683 two
19684 - three
19685 fˇour
19686 five
19687 "#
19688 .unindent(),
19689 );
19690
19691 cx.update_editor(|editor, window, cx| {
19692 editor.backspace(&Backspace, window, cx);
19693 editor.backspace(&Backspace, window, cx);
19694 });
19695 executor.run_until_parked();
19696 cx.assert_state_with_diff(
19697 r#"
19698 one
19699 two
19700 - threeˇ
19701 - four
19702 + our
19703 five
19704 "#
19705 .unindent(),
19706 );
19707}
19708
19709#[gpui::test]
19710async fn test_edit_after_expanded_modification_hunk(
19711 executor: BackgroundExecutor,
19712 cx: &mut TestAppContext,
19713) {
19714 init_test(cx, |_| {});
19715
19716 let mut cx = EditorTestContext::new(cx).await;
19717
19718 let diff_base = r#"
19719 use some::mod1;
19720 use some::mod2;
19721
19722 const A: u32 = 42;
19723 const B: u32 = 42;
19724 const C: u32 = 42;
19725 const D: u32 = 42;
19726
19727
19728 fn main() {
19729 println!("hello");
19730
19731 println!("world");
19732 }"#
19733 .unindent();
19734
19735 cx.set_state(
19736 &r#"
19737 use some::mod1;
19738 use some::mod2;
19739
19740 const A: u32 = 42;
19741 const B: u32 = 42;
19742 const C: u32 = 43ˇ
19743 const D: u32 = 42;
19744
19745
19746 fn main() {
19747 println!("hello");
19748
19749 println!("world");
19750 }"#
19751 .unindent(),
19752 );
19753
19754 cx.set_head_text(&diff_base);
19755 executor.run_until_parked();
19756 cx.update_editor(|editor, window, cx| {
19757 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19758 });
19759 executor.run_until_parked();
19760
19761 cx.assert_state_with_diff(
19762 r#"
19763 use some::mod1;
19764 use some::mod2;
19765
19766 const A: u32 = 42;
19767 const B: u32 = 42;
19768 - const C: u32 = 42;
19769 + const C: u32 = 43ˇ
19770 const D: u32 = 42;
19771
19772
19773 fn main() {
19774 println!("hello");
19775
19776 println!("world");
19777 }"#
19778 .unindent(),
19779 );
19780
19781 cx.update_editor(|editor, window, cx| {
19782 editor.handle_input("\nnew_line\n", window, cx);
19783 });
19784 executor.run_until_parked();
19785
19786 cx.assert_state_with_diff(
19787 r#"
19788 use some::mod1;
19789 use some::mod2;
19790
19791 const A: u32 = 42;
19792 const B: u32 = 42;
19793 - const C: u32 = 42;
19794 + const C: u32 = 43
19795 + new_line
19796 + ˇ
19797 const D: u32 = 42;
19798
19799
19800 fn main() {
19801 println!("hello");
19802
19803 println!("world");
19804 }"#
19805 .unindent(),
19806 );
19807}
19808
19809#[gpui::test]
19810async fn test_stage_and_unstage_added_file_hunk(
19811 executor: BackgroundExecutor,
19812 cx: &mut TestAppContext,
19813) {
19814 init_test(cx, |_| {});
19815
19816 let mut cx = EditorTestContext::new(cx).await;
19817 cx.update_editor(|editor, _, cx| {
19818 editor.set_expand_all_diff_hunks(cx);
19819 });
19820
19821 let working_copy = r#"
19822 ˇfn main() {
19823 println!("hello, world!");
19824 }
19825 "#
19826 .unindent();
19827
19828 cx.set_state(&working_copy);
19829 executor.run_until_parked();
19830
19831 cx.assert_state_with_diff(
19832 r#"
19833 + ˇfn main() {
19834 + println!("hello, world!");
19835 + }
19836 "#
19837 .unindent(),
19838 );
19839 cx.assert_index_text(None);
19840
19841 cx.update_editor(|editor, window, cx| {
19842 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19843 });
19844 executor.run_until_parked();
19845 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
19846 cx.assert_state_with_diff(
19847 r#"
19848 + ˇfn main() {
19849 + println!("hello, world!");
19850 + }
19851 "#
19852 .unindent(),
19853 );
19854
19855 cx.update_editor(|editor, window, cx| {
19856 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19857 });
19858 executor.run_until_parked();
19859 cx.assert_index_text(None);
19860}
19861
19862async fn setup_indent_guides_editor(
19863 text: &str,
19864 cx: &mut TestAppContext,
19865) -> (BufferId, EditorTestContext) {
19866 init_test(cx, |_| {});
19867
19868 let mut cx = EditorTestContext::new(cx).await;
19869
19870 let buffer_id = cx.update_editor(|editor, window, cx| {
19871 editor.set_text(text, window, cx);
19872 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
19873
19874 buffer_ids[0]
19875 });
19876
19877 (buffer_id, cx)
19878}
19879
19880fn assert_indent_guides(
19881 range: Range<u32>,
19882 expected: Vec<IndentGuide>,
19883 active_indices: Option<Vec<usize>>,
19884 cx: &mut EditorTestContext,
19885) {
19886 let indent_guides = cx.update_editor(|editor, window, cx| {
19887 let snapshot = editor.snapshot(window, cx).display_snapshot;
19888 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
19889 editor,
19890 MultiBufferRow(range.start)..MultiBufferRow(range.end),
19891 true,
19892 &snapshot,
19893 cx,
19894 );
19895
19896 indent_guides.sort_by(|a, b| {
19897 a.depth.cmp(&b.depth).then(
19898 a.start_row
19899 .cmp(&b.start_row)
19900 .then(a.end_row.cmp(&b.end_row)),
19901 )
19902 });
19903 indent_guides
19904 });
19905
19906 if let Some(expected) = active_indices {
19907 let active_indices = cx.update_editor(|editor, window, cx| {
19908 let snapshot = editor.snapshot(window, cx).display_snapshot;
19909 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
19910 });
19911
19912 assert_eq!(
19913 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
19914 expected,
19915 "Active indent guide indices do not match"
19916 );
19917 }
19918
19919 assert_eq!(indent_guides, expected, "Indent guides do not match");
19920}
19921
19922fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
19923 IndentGuide {
19924 buffer_id,
19925 start_row: MultiBufferRow(start_row),
19926 end_row: MultiBufferRow(end_row),
19927 depth,
19928 tab_size: 4,
19929 settings: IndentGuideSettings {
19930 enabled: true,
19931 line_width: 1,
19932 active_line_width: 1,
19933 ..Default::default()
19934 },
19935 }
19936}
19937
19938#[gpui::test]
19939async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
19940 let (buffer_id, mut cx) = setup_indent_guides_editor(
19941 &"
19942 fn main() {
19943 let a = 1;
19944 }"
19945 .unindent(),
19946 cx,
19947 )
19948 .await;
19949
19950 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
19951}
19952
19953#[gpui::test]
19954async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
19955 let (buffer_id, mut cx) = setup_indent_guides_editor(
19956 &"
19957 fn main() {
19958 let a = 1;
19959 let b = 2;
19960 }"
19961 .unindent(),
19962 cx,
19963 )
19964 .await;
19965
19966 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
19967}
19968
19969#[gpui::test]
19970async fn test_indent_guide_nested(cx: &mut TestAppContext) {
19971 let (buffer_id, mut cx) = setup_indent_guides_editor(
19972 &"
19973 fn main() {
19974 let a = 1;
19975 if a == 3 {
19976 let b = 2;
19977 } else {
19978 let c = 3;
19979 }
19980 }"
19981 .unindent(),
19982 cx,
19983 )
19984 .await;
19985
19986 assert_indent_guides(
19987 0..8,
19988 vec![
19989 indent_guide(buffer_id, 1, 6, 0),
19990 indent_guide(buffer_id, 3, 3, 1),
19991 indent_guide(buffer_id, 5, 5, 1),
19992 ],
19993 None,
19994 &mut cx,
19995 );
19996}
19997
19998#[gpui::test]
19999async fn test_indent_guide_tab(cx: &mut TestAppContext) {
20000 let (buffer_id, mut cx) = setup_indent_guides_editor(
20001 &"
20002 fn main() {
20003 let a = 1;
20004 let b = 2;
20005 let c = 3;
20006 }"
20007 .unindent(),
20008 cx,
20009 )
20010 .await;
20011
20012 assert_indent_guides(
20013 0..5,
20014 vec![
20015 indent_guide(buffer_id, 1, 3, 0),
20016 indent_guide(buffer_id, 2, 2, 1),
20017 ],
20018 None,
20019 &mut cx,
20020 );
20021}
20022
20023#[gpui::test]
20024async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
20025 let (buffer_id, mut cx) = setup_indent_guides_editor(
20026 &"
20027 fn main() {
20028 let a = 1;
20029
20030 let c = 3;
20031 }"
20032 .unindent(),
20033 cx,
20034 )
20035 .await;
20036
20037 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
20038}
20039
20040#[gpui::test]
20041async fn test_indent_guide_complex(cx: &mut TestAppContext) {
20042 let (buffer_id, mut cx) = setup_indent_guides_editor(
20043 &"
20044 fn main() {
20045 let a = 1;
20046
20047 let c = 3;
20048
20049 if a == 3 {
20050 let b = 2;
20051 } else {
20052 let c = 3;
20053 }
20054 }"
20055 .unindent(),
20056 cx,
20057 )
20058 .await;
20059
20060 assert_indent_guides(
20061 0..11,
20062 vec![
20063 indent_guide(buffer_id, 1, 9, 0),
20064 indent_guide(buffer_id, 6, 6, 1),
20065 indent_guide(buffer_id, 8, 8, 1),
20066 ],
20067 None,
20068 &mut cx,
20069 );
20070}
20071
20072#[gpui::test]
20073async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
20074 let (buffer_id, mut cx) = setup_indent_guides_editor(
20075 &"
20076 fn main() {
20077 let a = 1;
20078
20079 let c = 3;
20080
20081 if a == 3 {
20082 let b = 2;
20083 } else {
20084 let c = 3;
20085 }
20086 }"
20087 .unindent(),
20088 cx,
20089 )
20090 .await;
20091
20092 assert_indent_guides(
20093 1..11,
20094 vec![
20095 indent_guide(buffer_id, 1, 9, 0),
20096 indent_guide(buffer_id, 6, 6, 1),
20097 indent_guide(buffer_id, 8, 8, 1),
20098 ],
20099 None,
20100 &mut cx,
20101 );
20102}
20103
20104#[gpui::test]
20105async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
20106 let (buffer_id, mut cx) = setup_indent_guides_editor(
20107 &"
20108 fn main() {
20109 let a = 1;
20110
20111 let c = 3;
20112
20113 if a == 3 {
20114 let b = 2;
20115 } else {
20116 let c = 3;
20117 }
20118 }"
20119 .unindent(),
20120 cx,
20121 )
20122 .await;
20123
20124 assert_indent_guides(
20125 1..10,
20126 vec![
20127 indent_guide(buffer_id, 1, 9, 0),
20128 indent_guide(buffer_id, 6, 6, 1),
20129 indent_guide(buffer_id, 8, 8, 1),
20130 ],
20131 None,
20132 &mut cx,
20133 );
20134}
20135
20136#[gpui::test]
20137async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
20138 let (buffer_id, mut cx) = setup_indent_guides_editor(
20139 &"
20140 fn main() {
20141 if a {
20142 b(
20143 c,
20144 d,
20145 )
20146 } else {
20147 e(
20148 f
20149 )
20150 }
20151 }"
20152 .unindent(),
20153 cx,
20154 )
20155 .await;
20156
20157 assert_indent_guides(
20158 0..11,
20159 vec![
20160 indent_guide(buffer_id, 1, 10, 0),
20161 indent_guide(buffer_id, 2, 5, 1),
20162 indent_guide(buffer_id, 7, 9, 1),
20163 indent_guide(buffer_id, 3, 4, 2),
20164 indent_guide(buffer_id, 8, 8, 2),
20165 ],
20166 None,
20167 &mut cx,
20168 );
20169
20170 cx.update_editor(|editor, window, cx| {
20171 editor.fold_at(MultiBufferRow(2), window, cx);
20172 assert_eq!(
20173 editor.display_text(cx),
20174 "
20175 fn main() {
20176 if a {
20177 b(⋯
20178 )
20179 } else {
20180 e(
20181 f
20182 )
20183 }
20184 }"
20185 .unindent()
20186 );
20187 });
20188
20189 assert_indent_guides(
20190 0..11,
20191 vec![
20192 indent_guide(buffer_id, 1, 10, 0),
20193 indent_guide(buffer_id, 2, 5, 1),
20194 indent_guide(buffer_id, 7, 9, 1),
20195 indent_guide(buffer_id, 8, 8, 2),
20196 ],
20197 None,
20198 &mut cx,
20199 );
20200}
20201
20202#[gpui::test]
20203async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
20204 let (buffer_id, mut cx) = setup_indent_guides_editor(
20205 &"
20206 block1
20207 block2
20208 block3
20209 block4
20210 block2
20211 block1
20212 block1"
20213 .unindent(),
20214 cx,
20215 )
20216 .await;
20217
20218 assert_indent_guides(
20219 1..10,
20220 vec![
20221 indent_guide(buffer_id, 1, 4, 0),
20222 indent_guide(buffer_id, 2, 3, 1),
20223 indent_guide(buffer_id, 3, 3, 2),
20224 ],
20225 None,
20226 &mut cx,
20227 );
20228}
20229
20230#[gpui::test]
20231async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
20232 let (buffer_id, mut cx) = setup_indent_guides_editor(
20233 &"
20234 block1
20235 block2
20236 block3
20237
20238 block1
20239 block1"
20240 .unindent(),
20241 cx,
20242 )
20243 .await;
20244
20245 assert_indent_guides(
20246 0..6,
20247 vec![
20248 indent_guide(buffer_id, 1, 2, 0),
20249 indent_guide(buffer_id, 2, 2, 1),
20250 ],
20251 None,
20252 &mut cx,
20253 );
20254}
20255
20256#[gpui::test]
20257async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
20258 let (buffer_id, mut cx) = setup_indent_guides_editor(
20259 &"
20260 function component() {
20261 \treturn (
20262 \t\t\t
20263 \t\t<div>
20264 \t\t\t<abc></abc>
20265 \t\t</div>
20266 \t)
20267 }"
20268 .unindent(),
20269 cx,
20270 )
20271 .await;
20272
20273 assert_indent_guides(
20274 0..8,
20275 vec![
20276 indent_guide(buffer_id, 1, 6, 0),
20277 indent_guide(buffer_id, 2, 5, 1),
20278 indent_guide(buffer_id, 4, 4, 2),
20279 ],
20280 None,
20281 &mut cx,
20282 );
20283}
20284
20285#[gpui::test]
20286async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
20287 let (buffer_id, mut cx) = setup_indent_guides_editor(
20288 &"
20289 function component() {
20290 \treturn (
20291 \t
20292 \t\t<div>
20293 \t\t\t<abc></abc>
20294 \t\t</div>
20295 \t)
20296 }"
20297 .unindent(),
20298 cx,
20299 )
20300 .await;
20301
20302 assert_indent_guides(
20303 0..8,
20304 vec![
20305 indent_guide(buffer_id, 1, 6, 0),
20306 indent_guide(buffer_id, 2, 5, 1),
20307 indent_guide(buffer_id, 4, 4, 2),
20308 ],
20309 None,
20310 &mut cx,
20311 );
20312}
20313
20314#[gpui::test]
20315async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
20316 let (buffer_id, mut cx) = setup_indent_guides_editor(
20317 &"
20318 block1
20319
20320
20321
20322 block2
20323 "
20324 .unindent(),
20325 cx,
20326 )
20327 .await;
20328
20329 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20330}
20331
20332#[gpui::test]
20333async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
20334 let (buffer_id, mut cx) = setup_indent_guides_editor(
20335 &"
20336 def a:
20337 \tb = 3
20338 \tif True:
20339 \t\tc = 4
20340 \t\td = 5
20341 \tprint(b)
20342 "
20343 .unindent(),
20344 cx,
20345 )
20346 .await;
20347
20348 assert_indent_guides(
20349 0..6,
20350 vec![
20351 indent_guide(buffer_id, 1, 5, 0),
20352 indent_guide(buffer_id, 3, 4, 1),
20353 ],
20354 None,
20355 &mut cx,
20356 );
20357}
20358
20359#[gpui::test]
20360async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
20361 let (buffer_id, mut cx) = setup_indent_guides_editor(
20362 &"
20363 fn main() {
20364 let a = 1;
20365 }"
20366 .unindent(),
20367 cx,
20368 )
20369 .await;
20370
20371 cx.update_editor(|editor, window, cx| {
20372 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20373 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20374 });
20375 });
20376
20377 assert_indent_guides(
20378 0..3,
20379 vec![indent_guide(buffer_id, 1, 1, 0)],
20380 Some(vec![0]),
20381 &mut cx,
20382 );
20383}
20384
20385#[gpui::test]
20386async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
20387 let (buffer_id, mut cx) = setup_indent_guides_editor(
20388 &"
20389 fn main() {
20390 if 1 == 2 {
20391 let a = 1;
20392 }
20393 }"
20394 .unindent(),
20395 cx,
20396 )
20397 .await;
20398
20399 cx.update_editor(|editor, window, cx| {
20400 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20401 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20402 });
20403 });
20404
20405 assert_indent_guides(
20406 0..4,
20407 vec![
20408 indent_guide(buffer_id, 1, 3, 0),
20409 indent_guide(buffer_id, 2, 2, 1),
20410 ],
20411 Some(vec![1]),
20412 &mut cx,
20413 );
20414
20415 cx.update_editor(|editor, window, cx| {
20416 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20417 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20418 });
20419 });
20420
20421 assert_indent_guides(
20422 0..4,
20423 vec![
20424 indent_guide(buffer_id, 1, 3, 0),
20425 indent_guide(buffer_id, 2, 2, 1),
20426 ],
20427 Some(vec![1]),
20428 &mut cx,
20429 );
20430
20431 cx.update_editor(|editor, window, cx| {
20432 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20433 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
20434 });
20435 });
20436
20437 assert_indent_guides(
20438 0..4,
20439 vec![
20440 indent_guide(buffer_id, 1, 3, 0),
20441 indent_guide(buffer_id, 2, 2, 1),
20442 ],
20443 Some(vec![0]),
20444 &mut cx,
20445 );
20446}
20447
20448#[gpui::test]
20449async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
20450 let (buffer_id, mut cx) = setup_indent_guides_editor(
20451 &"
20452 fn main() {
20453 let a = 1;
20454
20455 let b = 2;
20456 }"
20457 .unindent(),
20458 cx,
20459 )
20460 .await;
20461
20462 cx.update_editor(|editor, window, cx| {
20463 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20464 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20465 });
20466 });
20467
20468 assert_indent_guides(
20469 0..5,
20470 vec![indent_guide(buffer_id, 1, 3, 0)],
20471 Some(vec![0]),
20472 &mut cx,
20473 );
20474}
20475
20476#[gpui::test]
20477async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
20478 let (buffer_id, mut cx) = setup_indent_guides_editor(
20479 &"
20480 def m:
20481 a = 1
20482 pass"
20483 .unindent(),
20484 cx,
20485 )
20486 .await;
20487
20488 cx.update_editor(|editor, window, cx| {
20489 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20490 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20491 });
20492 });
20493
20494 assert_indent_guides(
20495 0..3,
20496 vec![indent_guide(buffer_id, 1, 2, 0)],
20497 Some(vec![0]),
20498 &mut cx,
20499 );
20500}
20501
20502#[gpui::test]
20503async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
20504 init_test(cx, |_| {});
20505 let mut cx = EditorTestContext::new(cx).await;
20506 let text = indoc! {
20507 "
20508 impl A {
20509 fn b() {
20510 0;
20511 3;
20512 5;
20513 6;
20514 7;
20515 }
20516 }
20517 "
20518 };
20519 let base_text = indoc! {
20520 "
20521 impl A {
20522 fn b() {
20523 0;
20524 1;
20525 2;
20526 3;
20527 4;
20528 }
20529 fn c() {
20530 5;
20531 6;
20532 7;
20533 }
20534 }
20535 "
20536 };
20537
20538 cx.update_editor(|editor, window, cx| {
20539 editor.set_text(text, window, cx);
20540
20541 editor.buffer().update(cx, |multibuffer, cx| {
20542 let buffer = multibuffer.as_singleton().unwrap();
20543 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
20544
20545 multibuffer.set_all_diff_hunks_expanded(cx);
20546 multibuffer.add_diff(diff, cx);
20547
20548 buffer.read(cx).remote_id()
20549 })
20550 });
20551 cx.run_until_parked();
20552
20553 cx.assert_state_with_diff(
20554 indoc! { "
20555 impl A {
20556 fn b() {
20557 0;
20558 - 1;
20559 - 2;
20560 3;
20561 - 4;
20562 - }
20563 - fn c() {
20564 5;
20565 6;
20566 7;
20567 }
20568 }
20569 ˇ"
20570 }
20571 .to_string(),
20572 );
20573
20574 let mut actual_guides = cx.update_editor(|editor, window, cx| {
20575 editor
20576 .snapshot(window, cx)
20577 .buffer_snapshot
20578 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
20579 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
20580 .collect::<Vec<_>>()
20581 });
20582 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
20583 assert_eq!(
20584 actual_guides,
20585 vec![
20586 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
20587 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
20588 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
20589 ]
20590 );
20591}
20592
20593#[gpui::test]
20594async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20595 init_test(cx, |_| {});
20596 let mut cx = EditorTestContext::new(cx).await;
20597
20598 let diff_base = r#"
20599 a
20600 b
20601 c
20602 "#
20603 .unindent();
20604
20605 cx.set_state(
20606 &r#"
20607 ˇA
20608 b
20609 C
20610 "#
20611 .unindent(),
20612 );
20613 cx.set_head_text(&diff_base);
20614 cx.update_editor(|editor, window, cx| {
20615 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20616 });
20617 executor.run_until_parked();
20618
20619 let both_hunks_expanded = r#"
20620 - a
20621 + ˇA
20622 b
20623 - c
20624 + C
20625 "#
20626 .unindent();
20627
20628 cx.assert_state_with_diff(both_hunks_expanded.clone());
20629
20630 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20631 let snapshot = editor.snapshot(window, cx);
20632 let hunks = editor
20633 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20634 .collect::<Vec<_>>();
20635 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20636 let buffer_id = hunks[0].buffer_id;
20637 hunks
20638 .into_iter()
20639 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20640 .collect::<Vec<_>>()
20641 });
20642 assert_eq!(hunk_ranges.len(), 2);
20643
20644 cx.update_editor(|editor, _, cx| {
20645 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20646 });
20647 executor.run_until_parked();
20648
20649 let second_hunk_expanded = r#"
20650 ˇA
20651 b
20652 - c
20653 + C
20654 "#
20655 .unindent();
20656
20657 cx.assert_state_with_diff(second_hunk_expanded);
20658
20659 cx.update_editor(|editor, _, cx| {
20660 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20661 });
20662 executor.run_until_parked();
20663
20664 cx.assert_state_with_diff(both_hunks_expanded.clone());
20665
20666 cx.update_editor(|editor, _, cx| {
20667 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20668 });
20669 executor.run_until_parked();
20670
20671 let first_hunk_expanded = r#"
20672 - a
20673 + ˇA
20674 b
20675 C
20676 "#
20677 .unindent();
20678
20679 cx.assert_state_with_diff(first_hunk_expanded);
20680
20681 cx.update_editor(|editor, _, cx| {
20682 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20683 });
20684 executor.run_until_parked();
20685
20686 cx.assert_state_with_diff(both_hunks_expanded);
20687
20688 cx.set_state(
20689 &r#"
20690 ˇA
20691 b
20692 "#
20693 .unindent(),
20694 );
20695 cx.run_until_parked();
20696
20697 // TODO this cursor position seems bad
20698 cx.assert_state_with_diff(
20699 r#"
20700 - ˇa
20701 + A
20702 b
20703 "#
20704 .unindent(),
20705 );
20706
20707 cx.update_editor(|editor, window, cx| {
20708 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20709 });
20710
20711 cx.assert_state_with_diff(
20712 r#"
20713 - ˇa
20714 + A
20715 b
20716 - c
20717 "#
20718 .unindent(),
20719 );
20720
20721 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20722 let snapshot = editor.snapshot(window, cx);
20723 let hunks = editor
20724 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20725 .collect::<Vec<_>>();
20726 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20727 let buffer_id = hunks[0].buffer_id;
20728 hunks
20729 .into_iter()
20730 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20731 .collect::<Vec<_>>()
20732 });
20733 assert_eq!(hunk_ranges.len(), 2);
20734
20735 cx.update_editor(|editor, _, cx| {
20736 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20737 });
20738 executor.run_until_parked();
20739
20740 cx.assert_state_with_diff(
20741 r#"
20742 - ˇa
20743 + A
20744 b
20745 "#
20746 .unindent(),
20747 );
20748}
20749
20750#[gpui::test]
20751async fn test_toggle_deletion_hunk_at_start_of_file(
20752 executor: BackgroundExecutor,
20753 cx: &mut TestAppContext,
20754) {
20755 init_test(cx, |_| {});
20756 let mut cx = EditorTestContext::new(cx).await;
20757
20758 let diff_base = r#"
20759 a
20760 b
20761 c
20762 "#
20763 .unindent();
20764
20765 cx.set_state(
20766 &r#"
20767 ˇb
20768 c
20769 "#
20770 .unindent(),
20771 );
20772 cx.set_head_text(&diff_base);
20773 cx.update_editor(|editor, window, cx| {
20774 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20775 });
20776 executor.run_until_parked();
20777
20778 let hunk_expanded = r#"
20779 - a
20780 ˇb
20781 c
20782 "#
20783 .unindent();
20784
20785 cx.assert_state_with_diff(hunk_expanded.clone());
20786
20787 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20788 let snapshot = editor.snapshot(window, cx);
20789 let hunks = editor
20790 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20791 .collect::<Vec<_>>();
20792 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20793 let buffer_id = hunks[0].buffer_id;
20794 hunks
20795 .into_iter()
20796 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20797 .collect::<Vec<_>>()
20798 });
20799 assert_eq!(hunk_ranges.len(), 1);
20800
20801 cx.update_editor(|editor, _, cx| {
20802 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20803 });
20804 executor.run_until_parked();
20805
20806 let hunk_collapsed = r#"
20807 ˇb
20808 c
20809 "#
20810 .unindent();
20811
20812 cx.assert_state_with_diff(hunk_collapsed);
20813
20814 cx.update_editor(|editor, _, cx| {
20815 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20816 });
20817 executor.run_until_parked();
20818
20819 cx.assert_state_with_diff(hunk_expanded);
20820}
20821
20822#[gpui::test]
20823async fn test_display_diff_hunks(cx: &mut TestAppContext) {
20824 init_test(cx, |_| {});
20825
20826 let fs = FakeFs::new(cx.executor());
20827 fs.insert_tree(
20828 path!("/test"),
20829 json!({
20830 ".git": {},
20831 "file-1": "ONE\n",
20832 "file-2": "TWO\n",
20833 "file-3": "THREE\n",
20834 }),
20835 )
20836 .await;
20837
20838 fs.set_head_for_repo(
20839 path!("/test/.git").as_ref(),
20840 &[
20841 ("file-1".into(), "one\n".into()),
20842 ("file-2".into(), "two\n".into()),
20843 ("file-3".into(), "three\n".into()),
20844 ],
20845 "deadbeef",
20846 );
20847
20848 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
20849 let mut buffers = vec![];
20850 for i in 1..=3 {
20851 let buffer = project
20852 .update(cx, |project, cx| {
20853 let path = format!(path!("/test/file-{}"), i);
20854 project.open_local_buffer(path, cx)
20855 })
20856 .await
20857 .unwrap();
20858 buffers.push(buffer);
20859 }
20860
20861 let multibuffer = cx.new(|cx| {
20862 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
20863 multibuffer.set_all_diff_hunks_expanded(cx);
20864 for buffer in &buffers {
20865 let snapshot = buffer.read(cx).snapshot();
20866 multibuffer.set_excerpts_for_path(
20867 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
20868 buffer.clone(),
20869 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
20870 2,
20871 cx,
20872 );
20873 }
20874 multibuffer
20875 });
20876
20877 let editor = cx.add_window(|window, cx| {
20878 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
20879 });
20880 cx.run_until_parked();
20881
20882 let snapshot = editor
20883 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
20884 .unwrap();
20885 let hunks = snapshot
20886 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
20887 .map(|hunk| match hunk {
20888 DisplayDiffHunk::Unfolded {
20889 display_row_range, ..
20890 } => display_row_range,
20891 DisplayDiffHunk::Folded { .. } => unreachable!(),
20892 })
20893 .collect::<Vec<_>>();
20894 assert_eq!(
20895 hunks,
20896 [
20897 DisplayRow(2)..DisplayRow(4),
20898 DisplayRow(7)..DisplayRow(9),
20899 DisplayRow(12)..DisplayRow(14),
20900 ]
20901 );
20902}
20903
20904#[gpui::test]
20905async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
20906 init_test(cx, |_| {});
20907
20908 let mut cx = EditorTestContext::new(cx).await;
20909 cx.set_head_text(indoc! { "
20910 one
20911 two
20912 three
20913 four
20914 five
20915 "
20916 });
20917 cx.set_index_text(indoc! { "
20918 one
20919 two
20920 three
20921 four
20922 five
20923 "
20924 });
20925 cx.set_state(indoc! {"
20926 one
20927 TWO
20928 ˇTHREE
20929 FOUR
20930 five
20931 "});
20932 cx.run_until_parked();
20933 cx.update_editor(|editor, window, cx| {
20934 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20935 });
20936 cx.run_until_parked();
20937 cx.assert_index_text(Some(indoc! {"
20938 one
20939 TWO
20940 THREE
20941 FOUR
20942 five
20943 "}));
20944 cx.set_state(indoc! { "
20945 one
20946 TWO
20947 ˇTHREE-HUNDRED
20948 FOUR
20949 five
20950 "});
20951 cx.run_until_parked();
20952 cx.update_editor(|editor, window, cx| {
20953 let snapshot = editor.snapshot(window, cx);
20954 let hunks = editor
20955 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20956 .collect::<Vec<_>>();
20957 assert_eq!(hunks.len(), 1);
20958 assert_eq!(
20959 hunks[0].status(),
20960 DiffHunkStatus {
20961 kind: DiffHunkStatusKind::Modified,
20962 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
20963 }
20964 );
20965
20966 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20967 });
20968 cx.run_until_parked();
20969 cx.assert_index_text(Some(indoc! {"
20970 one
20971 TWO
20972 THREE-HUNDRED
20973 FOUR
20974 five
20975 "}));
20976}
20977
20978#[gpui::test]
20979fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
20980 init_test(cx, |_| {});
20981
20982 let editor = cx.add_window(|window, cx| {
20983 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
20984 build_editor(buffer, window, cx)
20985 });
20986
20987 let render_args = Arc::new(Mutex::new(None));
20988 let snapshot = editor
20989 .update(cx, |editor, window, cx| {
20990 let snapshot = editor.buffer().read(cx).snapshot(cx);
20991 let range =
20992 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
20993
20994 struct RenderArgs {
20995 row: MultiBufferRow,
20996 folded: bool,
20997 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
20998 }
20999
21000 let crease = Crease::inline(
21001 range,
21002 FoldPlaceholder::test(),
21003 {
21004 let toggle_callback = render_args.clone();
21005 move |row, folded, callback, _window, _cx| {
21006 *toggle_callback.lock() = Some(RenderArgs {
21007 row,
21008 folded,
21009 callback,
21010 });
21011 div()
21012 }
21013 },
21014 |_row, _folded, _window, _cx| div(),
21015 );
21016
21017 editor.insert_creases(Some(crease), cx);
21018 let snapshot = editor.snapshot(window, cx);
21019 let _div =
21020 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
21021 snapshot
21022 })
21023 .unwrap();
21024
21025 let render_args = render_args.lock().take().unwrap();
21026 assert_eq!(render_args.row, MultiBufferRow(1));
21027 assert!(!render_args.folded);
21028 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21029
21030 cx.update_window(*editor, |_, window, cx| {
21031 (render_args.callback)(true, window, cx)
21032 })
21033 .unwrap();
21034 let snapshot = editor
21035 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21036 .unwrap();
21037 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
21038
21039 cx.update_window(*editor, |_, window, cx| {
21040 (render_args.callback)(false, window, cx)
21041 })
21042 .unwrap();
21043 let snapshot = editor
21044 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21045 .unwrap();
21046 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21047}
21048
21049#[gpui::test]
21050async fn test_input_text(cx: &mut TestAppContext) {
21051 init_test(cx, |_| {});
21052 let mut cx = EditorTestContext::new(cx).await;
21053
21054 cx.set_state(
21055 &r#"ˇone
21056 two
21057
21058 three
21059 fourˇ
21060 five
21061
21062 siˇx"#
21063 .unindent(),
21064 );
21065
21066 cx.dispatch_action(HandleInput(String::new()));
21067 cx.assert_editor_state(
21068 &r#"ˇone
21069 two
21070
21071 three
21072 fourˇ
21073 five
21074
21075 siˇx"#
21076 .unindent(),
21077 );
21078
21079 cx.dispatch_action(HandleInput("AAAA".to_string()));
21080 cx.assert_editor_state(
21081 &r#"AAAAˇone
21082 two
21083
21084 three
21085 fourAAAAˇ
21086 five
21087
21088 siAAAAˇx"#
21089 .unindent(),
21090 );
21091}
21092
21093#[gpui::test]
21094async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
21095 init_test(cx, |_| {});
21096
21097 let mut cx = EditorTestContext::new(cx).await;
21098 cx.set_state(
21099 r#"let foo = 1;
21100let foo = 2;
21101let foo = 3;
21102let fooˇ = 4;
21103let foo = 5;
21104let foo = 6;
21105let foo = 7;
21106let foo = 8;
21107let foo = 9;
21108let foo = 10;
21109let foo = 11;
21110let foo = 12;
21111let foo = 13;
21112let foo = 14;
21113let foo = 15;"#,
21114 );
21115
21116 cx.update_editor(|e, window, cx| {
21117 assert_eq!(
21118 e.next_scroll_position,
21119 NextScrollCursorCenterTopBottom::Center,
21120 "Default next scroll direction is center",
21121 );
21122
21123 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21124 assert_eq!(
21125 e.next_scroll_position,
21126 NextScrollCursorCenterTopBottom::Top,
21127 "After center, next scroll direction should be top",
21128 );
21129
21130 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21131 assert_eq!(
21132 e.next_scroll_position,
21133 NextScrollCursorCenterTopBottom::Bottom,
21134 "After top, next scroll direction should be bottom",
21135 );
21136
21137 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21138 assert_eq!(
21139 e.next_scroll_position,
21140 NextScrollCursorCenterTopBottom::Center,
21141 "After bottom, scrolling should start over",
21142 );
21143
21144 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21145 assert_eq!(
21146 e.next_scroll_position,
21147 NextScrollCursorCenterTopBottom::Top,
21148 "Scrolling continues if retriggered fast enough"
21149 );
21150 });
21151
21152 cx.executor()
21153 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
21154 cx.executor().run_until_parked();
21155 cx.update_editor(|e, _, _| {
21156 assert_eq!(
21157 e.next_scroll_position,
21158 NextScrollCursorCenterTopBottom::Center,
21159 "If scrolling is not triggered fast enough, it should reset"
21160 );
21161 });
21162}
21163
21164#[gpui::test]
21165async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
21166 init_test(cx, |_| {});
21167 let mut cx = EditorLspTestContext::new_rust(
21168 lsp::ServerCapabilities {
21169 definition_provider: Some(lsp::OneOf::Left(true)),
21170 references_provider: Some(lsp::OneOf::Left(true)),
21171 ..lsp::ServerCapabilities::default()
21172 },
21173 cx,
21174 )
21175 .await;
21176
21177 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
21178 let go_to_definition = cx
21179 .lsp
21180 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21181 move |params, _| async move {
21182 if empty_go_to_definition {
21183 Ok(None)
21184 } else {
21185 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
21186 uri: params.text_document_position_params.text_document.uri,
21187 range: lsp::Range::new(
21188 lsp::Position::new(4, 3),
21189 lsp::Position::new(4, 6),
21190 ),
21191 })))
21192 }
21193 },
21194 );
21195 let references = cx
21196 .lsp
21197 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21198 Ok(Some(vec![lsp::Location {
21199 uri: params.text_document_position.text_document.uri,
21200 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
21201 }]))
21202 });
21203 (go_to_definition, references)
21204 };
21205
21206 cx.set_state(
21207 &r#"fn one() {
21208 let mut a = ˇtwo();
21209 }
21210
21211 fn two() {}"#
21212 .unindent(),
21213 );
21214 set_up_lsp_handlers(false, &mut cx);
21215 let navigated = cx
21216 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21217 .await
21218 .expect("Failed to navigate to definition");
21219 assert_eq!(
21220 navigated,
21221 Navigated::Yes,
21222 "Should have navigated to definition from the GetDefinition response"
21223 );
21224 cx.assert_editor_state(
21225 &r#"fn one() {
21226 let mut a = two();
21227 }
21228
21229 fn «twoˇ»() {}"#
21230 .unindent(),
21231 );
21232
21233 let editors = cx.update_workspace(|workspace, _, cx| {
21234 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21235 });
21236 cx.update_editor(|_, _, test_editor_cx| {
21237 assert_eq!(
21238 editors.len(),
21239 1,
21240 "Initially, only one, test, editor should be open in the workspace"
21241 );
21242 assert_eq!(
21243 test_editor_cx.entity(),
21244 editors.last().expect("Asserted len is 1").clone()
21245 );
21246 });
21247
21248 set_up_lsp_handlers(true, &mut cx);
21249 let navigated = cx
21250 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21251 .await
21252 .expect("Failed to navigate to lookup references");
21253 assert_eq!(
21254 navigated,
21255 Navigated::Yes,
21256 "Should have navigated to references as a fallback after empty GoToDefinition response"
21257 );
21258 // We should not change the selections in the existing file,
21259 // if opening another milti buffer with the references
21260 cx.assert_editor_state(
21261 &r#"fn one() {
21262 let mut a = two();
21263 }
21264
21265 fn «twoˇ»() {}"#
21266 .unindent(),
21267 );
21268 let editors = cx.update_workspace(|workspace, _, cx| {
21269 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21270 });
21271 cx.update_editor(|_, _, test_editor_cx| {
21272 assert_eq!(
21273 editors.len(),
21274 2,
21275 "After falling back to references search, we open a new editor with the results"
21276 );
21277 let references_fallback_text = editors
21278 .into_iter()
21279 .find(|new_editor| *new_editor != test_editor_cx.entity())
21280 .expect("Should have one non-test editor now")
21281 .read(test_editor_cx)
21282 .text(test_editor_cx);
21283 assert_eq!(
21284 references_fallback_text, "fn one() {\n let mut a = two();\n}",
21285 "Should use the range from the references response and not the GoToDefinition one"
21286 );
21287 });
21288}
21289
21290#[gpui::test]
21291async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
21292 init_test(cx, |_| {});
21293 cx.update(|cx| {
21294 let mut editor_settings = EditorSettings::get_global(cx).clone();
21295 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
21296 EditorSettings::override_global(editor_settings, cx);
21297 });
21298 let mut cx = EditorLspTestContext::new_rust(
21299 lsp::ServerCapabilities {
21300 definition_provider: Some(lsp::OneOf::Left(true)),
21301 references_provider: Some(lsp::OneOf::Left(true)),
21302 ..lsp::ServerCapabilities::default()
21303 },
21304 cx,
21305 )
21306 .await;
21307 let original_state = r#"fn one() {
21308 let mut a = ˇtwo();
21309 }
21310
21311 fn two() {}"#
21312 .unindent();
21313 cx.set_state(&original_state);
21314
21315 let mut go_to_definition = cx
21316 .lsp
21317 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21318 move |_, _| async move { Ok(None) },
21319 );
21320 let _references = cx
21321 .lsp
21322 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
21323 panic!("Should not call for references with no go to definition fallback")
21324 });
21325
21326 let navigated = cx
21327 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21328 .await
21329 .expect("Failed to navigate to lookup references");
21330 go_to_definition
21331 .next()
21332 .await
21333 .expect("Should have called the go_to_definition handler");
21334
21335 assert_eq!(
21336 navigated,
21337 Navigated::No,
21338 "Should have navigated to references as a fallback after empty GoToDefinition response"
21339 );
21340 cx.assert_editor_state(&original_state);
21341 let editors = cx.update_workspace(|workspace, _, cx| {
21342 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21343 });
21344 cx.update_editor(|_, _, _| {
21345 assert_eq!(
21346 editors.len(),
21347 1,
21348 "After unsuccessful fallback, no other editor should have been opened"
21349 );
21350 });
21351}
21352
21353#[gpui::test]
21354async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
21355 init_test(cx, |_| {});
21356
21357 let language = Arc::new(Language::new(
21358 LanguageConfig::default(),
21359 Some(tree_sitter_rust::LANGUAGE.into()),
21360 ));
21361
21362 let text = r#"
21363 #[cfg(test)]
21364 mod tests() {
21365 #[test]
21366 fn runnable_1() {
21367 let a = 1;
21368 }
21369
21370 #[test]
21371 fn runnable_2() {
21372 let a = 1;
21373 let b = 2;
21374 }
21375 }
21376 "#
21377 .unindent();
21378
21379 let fs = FakeFs::new(cx.executor());
21380 fs.insert_file("/file.rs", Default::default()).await;
21381
21382 let project = Project::test(fs, ["/a".as_ref()], cx).await;
21383 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21384 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21385 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
21386 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
21387
21388 let editor = cx.new_window_entity(|window, cx| {
21389 Editor::new(
21390 EditorMode::full(),
21391 multi_buffer,
21392 Some(project.clone()),
21393 window,
21394 cx,
21395 )
21396 });
21397
21398 editor.update_in(cx, |editor, window, cx| {
21399 let snapshot = editor.buffer().read(cx).snapshot(cx);
21400 editor.tasks.insert(
21401 (buffer.read(cx).remote_id(), 3),
21402 RunnableTasks {
21403 templates: vec![],
21404 offset: snapshot.anchor_before(43),
21405 column: 0,
21406 extra_variables: HashMap::default(),
21407 context_range: BufferOffset(43)..BufferOffset(85),
21408 },
21409 );
21410 editor.tasks.insert(
21411 (buffer.read(cx).remote_id(), 8),
21412 RunnableTasks {
21413 templates: vec![],
21414 offset: snapshot.anchor_before(86),
21415 column: 0,
21416 extra_variables: HashMap::default(),
21417 context_range: BufferOffset(86)..BufferOffset(191),
21418 },
21419 );
21420
21421 // Test finding task when cursor is inside function body
21422 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21423 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
21424 });
21425 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
21426 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
21427
21428 // Test finding task when cursor is on function name
21429 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21430 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
21431 });
21432 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
21433 assert_eq!(row, 8, "Should find task when cursor is on function name");
21434 });
21435}
21436
21437#[gpui::test]
21438async fn test_folding_buffers(cx: &mut TestAppContext) {
21439 init_test(cx, |_| {});
21440
21441 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
21442 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
21443 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
21444
21445 let fs = FakeFs::new(cx.executor());
21446 fs.insert_tree(
21447 path!("/a"),
21448 json!({
21449 "first.rs": sample_text_1,
21450 "second.rs": sample_text_2,
21451 "third.rs": sample_text_3,
21452 }),
21453 )
21454 .await;
21455 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21456 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21457 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21458 let worktree = project.update(cx, |project, cx| {
21459 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
21460 assert_eq!(worktrees.len(), 1);
21461 worktrees.pop().unwrap()
21462 });
21463 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
21464
21465 let buffer_1 = project
21466 .update(cx, |project, cx| {
21467 project.open_buffer((worktree_id, "first.rs"), cx)
21468 })
21469 .await
21470 .unwrap();
21471 let buffer_2 = project
21472 .update(cx, |project, cx| {
21473 project.open_buffer((worktree_id, "second.rs"), cx)
21474 })
21475 .await
21476 .unwrap();
21477 let buffer_3 = project
21478 .update(cx, |project, cx| {
21479 project.open_buffer((worktree_id, "third.rs"), cx)
21480 })
21481 .await
21482 .unwrap();
21483
21484 let multi_buffer = cx.new(|cx| {
21485 let mut multi_buffer = MultiBuffer::new(ReadWrite);
21486 multi_buffer.push_excerpts(
21487 buffer_1.clone(),
21488 [
21489 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21490 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21491 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21492 ],
21493 cx,
21494 );
21495 multi_buffer.push_excerpts(
21496 buffer_2.clone(),
21497 [
21498 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21499 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21500 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21501 ],
21502 cx,
21503 );
21504 multi_buffer.push_excerpts(
21505 buffer_3.clone(),
21506 [
21507 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21508 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21509 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21510 ],
21511 cx,
21512 );
21513 multi_buffer
21514 });
21515 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
21516 Editor::new(
21517 EditorMode::full(),
21518 multi_buffer.clone(),
21519 Some(project.clone()),
21520 window,
21521 cx,
21522 )
21523 });
21524
21525 assert_eq!(
21526 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21527 "\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",
21528 );
21529
21530 multi_buffer_editor.update(cx, |editor, cx| {
21531 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
21532 });
21533 assert_eq!(
21534 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21535 "\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",
21536 "After folding the first buffer, its text should not be displayed"
21537 );
21538
21539 multi_buffer_editor.update(cx, |editor, cx| {
21540 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
21541 });
21542 assert_eq!(
21543 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21544 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
21545 "After folding the second buffer, its text should not be displayed"
21546 );
21547
21548 multi_buffer_editor.update(cx, |editor, cx| {
21549 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
21550 });
21551 assert_eq!(
21552 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21553 "\n\n\n\n\n",
21554 "After folding the third buffer, its text should not be displayed"
21555 );
21556
21557 // Emulate selection inside the fold logic, that should work
21558 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21559 editor
21560 .snapshot(window, cx)
21561 .next_line_boundary(Point::new(0, 4));
21562 });
21563
21564 multi_buffer_editor.update(cx, |editor, cx| {
21565 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
21566 });
21567 assert_eq!(
21568 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21569 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
21570 "After unfolding the second buffer, its text should be displayed"
21571 );
21572
21573 // Typing inside of buffer 1 causes that buffer to be unfolded.
21574 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21575 assert_eq!(
21576 multi_buffer
21577 .read(cx)
21578 .snapshot(cx)
21579 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
21580 .collect::<String>(),
21581 "bbbb"
21582 );
21583 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
21584 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
21585 });
21586 editor.handle_input("B", window, cx);
21587 });
21588
21589 assert_eq!(
21590 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21591 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
21592 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
21593 );
21594
21595 multi_buffer_editor.update(cx, |editor, cx| {
21596 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
21597 });
21598 assert_eq!(
21599 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21600 "\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",
21601 "After unfolding the all buffers, all original text should be displayed"
21602 );
21603}
21604
21605#[gpui::test]
21606async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
21607 init_test(cx, |_| {});
21608
21609 let sample_text_1 = "1111\n2222\n3333".to_string();
21610 let sample_text_2 = "4444\n5555\n6666".to_string();
21611 let sample_text_3 = "7777\n8888\n9999".to_string();
21612
21613 let fs = FakeFs::new(cx.executor());
21614 fs.insert_tree(
21615 path!("/a"),
21616 json!({
21617 "first.rs": sample_text_1,
21618 "second.rs": sample_text_2,
21619 "third.rs": sample_text_3,
21620 }),
21621 )
21622 .await;
21623 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21624 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21625 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21626 let worktree = project.update(cx, |project, cx| {
21627 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
21628 assert_eq!(worktrees.len(), 1);
21629 worktrees.pop().unwrap()
21630 });
21631 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
21632
21633 let buffer_1 = project
21634 .update(cx, |project, cx| {
21635 project.open_buffer((worktree_id, "first.rs"), cx)
21636 })
21637 .await
21638 .unwrap();
21639 let buffer_2 = project
21640 .update(cx, |project, cx| {
21641 project.open_buffer((worktree_id, "second.rs"), cx)
21642 })
21643 .await
21644 .unwrap();
21645 let buffer_3 = project
21646 .update(cx, |project, cx| {
21647 project.open_buffer((worktree_id, "third.rs"), cx)
21648 })
21649 .await
21650 .unwrap();
21651
21652 let multi_buffer = cx.new(|cx| {
21653 let mut multi_buffer = MultiBuffer::new(ReadWrite);
21654 multi_buffer.push_excerpts(
21655 buffer_1.clone(),
21656 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21657 cx,
21658 );
21659 multi_buffer.push_excerpts(
21660 buffer_2.clone(),
21661 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21662 cx,
21663 );
21664 multi_buffer.push_excerpts(
21665 buffer_3.clone(),
21666 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21667 cx,
21668 );
21669 multi_buffer
21670 });
21671
21672 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
21673 Editor::new(
21674 EditorMode::full(),
21675 multi_buffer,
21676 Some(project.clone()),
21677 window,
21678 cx,
21679 )
21680 });
21681
21682 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
21683 assert_eq!(
21684 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21685 full_text,
21686 );
21687
21688 multi_buffer_editor.update(cx, |editor, cx| {
21689 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
21690 });
21691 assert_eq!(
21692 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21693 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
21694 "After folding the first buffer, its text should not be displayed"
21695 );
21696
21697 multi_buffer_editor.update(cx, |editor, cx| {
21698 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
21699 });
21700
21701 assert_eq!(
21702 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21703 "\n\n\n\n\n\n7777\n8888\n9999",
21704 "After folding the second buffer, its text should not be displayed"
21705 );
21706
21707 multi_buffer_editor.update(cx, |editor, cx| {
21708 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
21709 });
21710 assert_eq!(
21711 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21712 "\n\n\n\n\n",
21713 "After folding the third buffer, its text should not be displayed"
21714 );
21715
21716 multi_buffer_editor.update(cx, |editor, cx| {
21717 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
21718 });
21719 assert_eq!(
21720 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21721 "\n\n\n\n4444\n5555\n6666\n\n",
21722 "After unfolding the second buffer, its text should be displayed"
21723 );
21724
21725 multi_buffer_editor.update(cx, |editor, cx| {
21726 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
21727 });
21728 assert_eq!(
21729 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21730 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
21731 "After unfolding the first buffer, its text should be displayed"
21732 );
21733
21734 multi_buffer_editor.update(cx, |editor, cx| {
21735 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
21736 });
21737 assert_eq!(
21738 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21739 full_text,
21740 "After unfolding all buffers, all original text should be displayed"
21741 );
21742}
21743
21744#[gpui::test]
21745async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
21746 init_test(cx, |_| {});
21747
21748 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
21749
21750 let fs = FakeFs::new(cx.executor());
21751 fs.insert_tree(
21752 path!("/a"),
21753 json!({
21754 "main.rs": sample_text,
21755 }),
21756 )
21757 .await;
21758 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21759 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21760 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21761 let worktree = project.update(cx, |project, cx| {
21762 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
21763 assert_eq!(worktrees.len(), 1);
21764 worktrees.pop().unwrap()
21765 });
21766 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
21767
21768 let buffer_1 = project
21769 .update(cx, |project, cx| {
21770 project.open_buffer((worktree_id, "main.rs"), cx)
21771 })
21772 .await
21773 .unwrap();
21774
21775 let multi_buffer = cx.new(|cx| {
21776 let mut multi_buffer = MultiBuffer::new(ReadWrite);
21777 multi_buffer.push_excerpts(
21778 buffer_1.clone(),
21779 [ExcerptRange::new(
21780 Point::new(0, 0)
21781 ..Point::new(
21782 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
21783 0,
21784 ),
21785 )],
21786 cx,
21787 );
21788 multi_buffer
21789 });
21790 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
21791 Editor::new(
21792 EditorMode::full(),
21793 multi_buffer,
21794 Some(project.clone()),
21795 window,
21796 cx,
21797 )
21798 });
21799
21800 let selection_range = Point::new(1, 0)..Point::new(2, 0);
21801 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21802 enum TestHighlight {}
21803 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
21804 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
21805 editor.highlight_text::<TestHighlight>(
21806 vec![highlight_range.clone()],
21807 HighlightStyle::color(Hsla::green()),
21808 cx,
21809 );
21810 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21811 s.select_ranges(Some(highlight_range))
21812 });
21813 });
21814
21815 let full_text = format!("\n\n{sample_text}");
21816 assert_eq!(
21817 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21818 full_text,
21819 );
21820}
21821
21822#[gpui::test]
21823async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
21824 init_test(cx, |_| {});
21825 cx.update(|cx| {
21826 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
21827 "keymaps/default-linux.json",
21828 cx,
21829 )
21830 .unwrap();
21831 cx.bind_keys(default_key_bindings);
21832 });
21833
21834 let (editor, cx) = cx.add_window_view(|window, cx| {
21835 let multi_buffer = MultiBuffer::build_multi(
21836 [
21837 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
21838 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
21839 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
21840 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
21841 ],
21842 cx,
21843 );
21844 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
21845
21846 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
21847 // fold all but the second buffer, so that we test navigating between two
21848 // adjacent folded buffers, as well as folded buffers at the start and
21849 // end the multibuffer
21850 editor.fold_buffer(buffer_ids[0], cx);
21851 editor.fold_buffer(buffer_ids[2], cx);
21852 editor.fold_buffer(buffer_ids[3], cx);
21853
21854 editor
21855 });
21856 cx.simulate_resize(size(px(1000.), px(1000.)));
21857
21858 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
21859 cx.assert_excerpts_with_selections(indoc! {"
21860 [EXCERPT]
21861 ˇ[FOLDED]
21862 [EXCERPT]
21863 a1
21864 b1
21865 [EXCERPT]
21866 [FOLDED]
21867 [EXCERPT]
21868 [FOLDED]
21869 "
21870 });
21871 cx.simulate_keystroke("down");
21872 cx.assert_excerpts_with_selections(indoc! {"
21873 [EXCERPT]
21874 [FOLDED]
21875 [EXCERPT]
21876 ˇa1
21877 b1
21878 [EXCERPT]
21879 [FOLDED]
21880 [EXCERPT]
21881 [FOLDED]
21882 "
21883 });
21884 cx.simulate_keystroke("down");
21885 cx.assert_excerpts_with_selections(indoc! {"
21886 [EXCERPT]
21887 [FOLDED]
21888 [EXCERPT]
21889 a1
21890 ˇb1
21891 [EXCERPT]
21892 [FOLDED]
21893 [EXCERPT]
21894 [FOLDED]
21895 "
21896 });
21897 cx.simulate_keystroke("down");
21898 cx.assert_excerpts_with_selections(indoc! {"
21899 [EXCERPT]
21900 [FOLDED]
21901 [EXCERPT]
21902 a1
21903 b1
21904 ˇ[EXCERPT]
21905 [FOLDED]
21906 [EXCERPT]
21907 [FOLDED]
21908 "
21909 });
21910 cx.simulate_keystroke("down");
21911 cx.assert_excerpts_with_selections(indoc! {"
21912 [EXCERPT]
21913 [FOLDED]
21914 [EXCERPT]
21915 a1
21916 b1
21917 [EXCERPT]
21918 ˇ[FOLDED]
21919 [EXCERPT]
21920 [FOLDED]
21921 "
21922 });
21923 for _ in 0..5 {
21924 cx.simulate_keystroke("down");
21925 cx.assert_excerpts_with_selections(indoc! {"
21926 [EXCERPT]
21927 [FOLDED]
21928 [EXCERPT]
21929 a1
21930 b1
21931 [EXCERPT]
21932 [FOLDED]
21933 [EXCERPT]
21934 ˇ[FOLDED]
21935 "
21936 });
21937 }
21938
21939 cx.simulate_keystroke("up");
21940 cx.assert_excerpts_with_selections(indoc! {"
21941 [EXCERPT]
21942 [FOLDED]
21943 [EXCERPT]
21944 a1
21945 b1
21946 [EXCERPT]
21947 ˇ[FOLDED]
21948 [EXCERPT]
21949 [FOLDED]
21950 "
21951 });
21952 cx.simulate_keystroke("up");
21953 cx.assert_excerpts_with_selections(indoc! {"
21954 [EXCERPT]
21955 [FOLDED]
21956 [EXCERPT]
21957 a1
21958 b1
21959 ˇ[EXCERPT]
21960 [FOLDED]
21961 [EXCERPT]
21962 [FOLDED]
21963 "
21964 });
21965 cx.simulate_keystroke("up");
21966 cx.assert_excerpts_with_selections(indoc! {"
21967 [EXCERPT]
21968 [FOLDED]
21969 [EXCERPT]
21970 a1
21971 ˇb1
21972 [EXCERPT]
21973 [FOLDED]
21974 [EXCERPT]
21975 [FOLDED]
21976 "
21977 });
21978 cx.simulate_keystroke("up");
21979 cx.assert_excerpts_with_selections(indoc! {"
21980 [EXCERPT]
21981 [FOLDED]
21982 [EXCERPT]
21983 ˇa1
21984 b1
21985 [EXCERPT]
21986 [FOLDED]
21987 [EXCERPT]
21988 [FOLDED]
21989 "
21990 });
21991 for _ in 0..5 {
21992 cx.simulate_keystroke("up");
21993 cx.assert_excerpts_with_selections(indoc! {"
21994 [EXCERPT]
21995 ˇ[FOLDED]
21996 [EXCERPT]
21997 a1
21998 b1
21999 [EXCERPT]
22000 [FOLDED]
22001 [EXCERPT]
22002 [FOLDED]
22003 "
22004 });
22005 }
22006}
22007
22008#[gpui::test]
22009async fn test_edit_prediction_text(cx: &mut TestAppContext) {
22010 init_test(cx, |_| {});
22011
22012 // Simple insertion
22013 assert_highlighted_edits(
22014 "Hello, world!",
22015 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
22016 true,
22017 cx,
22018 |highlighted_edits, cx| {
22019 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
22020 assert_eq!(highlighted_edits.highlights.len(), 1);
22021 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
22022 assert_eq!(
22023 highlighted_edits.highlights[0].1.background_color,
22024 Some(cx.theme().status().created_background)
22025 );
22026 },
22027 )
22028 .await;
22029
22030 // Replacement
22031 assert_highlighted_edits(
22032 "This is a test.",
22033 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
22034 false,
22035 cx,
22036 |highlighted_edits, cx| {
22037 assert_eq!(highlighted_edits.text, "That is a test.");
22038 assert_eq!(highlighted_edits.highlights.len(), 1);
22039 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
22040 assert_eq!(
22041 highlighted_edits.highlights[0].1.background_color,
22042 Some(cx.theme().status().created_background)
22043 );
22044 },
22045 )
22046 .await;
22047
22048 // Multiple edits
22049 assert_highlighted_edits(
22050 "Hello, world!",
22051 vec![
22052 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
22053 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
22054 ],
22055 false,
22056 cx,
22057 |highlighted_edits, cx| {
22058 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
22059 assert_eq!(highlighted_edits.highlights.len(), 2);
22060 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
22061 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
22062 assert_eq!(
22063 highlighted_edits.highlights[0].1.background_color,
22064 Some(cx.theme().status().created_background)
22065 );
22066 assert_eq!(
22067 highlighted_edits.highlights[1].1.background_color,
22068 Some(cx.theme().status().created_background)
22069 );
22070 },
22071 )
22072 .await;
22073
22074 // Multiple lines with edits
22075 assert_highlighted_edits(
22076 "First line\nSecond line\nThird line\nFourth line",
22077 vec![
22078 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
22079 (
22080 Point::new(2, 0)..Point::new(2, 10),
22081 "New third line".to_string(),
22082 ),
22083 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
22084 ],
22085 false,
22086 cx,
22087 |highlighted_edits, cx| {
22088 assert_eq!(
22089 highlighted_edits.text,
22090 "Second modified\nNew third line\nFourth updated line"
22091 );
22092 assert_eq!(highlighted_edits.highlights.len(), 3);
22093 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
22094 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
22095 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
22096 for highlight in &highlighted_edits.highlights {
22097 assert_eq!(
22098 highlight.1.background_color,
22099 Some(cx.theme().status().created_background)
22100 );
22101 }
22102 },
22103 )
22104 .await;
22105}
22106
22107#[gpui::test]
22108async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
22109 init_test(cx, |_| {});
22110
22111 // Deletion
22112 assert_highlighted_edits(
22113 "Hello, world!",
22114 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
22115 true,
22116 cx,
22117 |highlighted_edits, cx| {
22118 assert_eq!(highlighted_edits.text, "Hello, world!");
22119 assert_eq!(highlighted_edits.highlights.len(), 1);
22120 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
22121 assert_eq!(
22122 highlighted_edits.highlights[0].1.background_color,
22123 Some(cx.theme().status().deleted_background)
22124 );
22125 },
22126 )
22127 .await;
22128
22129 // Insertion
22130 assert_highlighted_edits(
22131 "Hello, world!",
22132 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
22133 true,
22134 cx,
22135 |highlighted_edits, cx| {
22136 assert_eq!(highlighted_edits.highlights.len(), 1);
22137 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
22138 assert_eq!(
22139 highlighted_edits.highlights[0].1.background_color,
22140 Some(cx.theme().status().created_background)
22141 );
22142 },
22143 )
22144 .await;
22145}
22146
22147async fn assert_highlighted_edits(
22148 text: &str,
22149 edits: Vec<(Range<Point>, String)>,
22150 include_deletions: bool,
22151 cx: &mut TestAppContext,
22152 assertion_fn: impl Fn(HighlightedText, &App),
22153) {
22154 let window = cx.add_window(|window, cx| {
22155 let buffer = MultiBuffer::build_simple(text, cx);
22156 Editor::new(EditorMode::full(), buffer, None, window, cx)
22157 });
22158 let cx = &mut VisualTestContext::from_window(*window, cx);
22159
22160 let (buffer, snapshot) = window
22161 .update(cx, |editor, _window, cx| {
22162 (
22163 editor.buffer().clone(),
22164 editor.buffer().read(cx).snapshot(cx),
22165 )
22166 })
22167 .unwrap();
22168
22169 let edits = edits
22170 .into_iter()
22171 .map(|(range, edit)| {
22172 (
22173 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
22174 edit,
22175 )
22176 })
22177 .collect::<Vec<_>>();
22178
22179 let text_anchor_edits = edits
22180 .clone()
22181 .into_iter()
22182 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
22183 .collect::<Vec<_>>();
22184
22185 let edit_preview = window
22186 .update(cx, |_, _window, cx| {
22187 buffer
22188 .read(cx)
22189 .as_singleton()
22190 .unwrap()
22191 .read(cx)
22192 .preview_edits(text_anchor_edits.into(), cx)
22193 })
22194 .unwrap()
22195 .await;
22196
22197 cx.update(|_window, cx| {
22198 let highlighted_edits = edit_prediction_edit_text(
22199 snapshot.as_singleton().unwrap().2,
22200 &edits,
22201 &edit_preview,
22202 include_deletions,
22203 cx,
22204 );
22205 assertion_fn(highlighted_edits, cx)
22206 });
22207}
22208
22209#[track_caller]
22210fn assert_breakpoint(
22211 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
22212 path: &Arc<Path>,
22213 expected: Vec<(u32, Breakpoint)>,
22214) {
22215 if expected.is_empty() {
22216 assert!(!breakpoints.contains_key(path), "{}", path.display());
22217 } else {
22218 let mut breakpoint = breakpoints
22219 .get(path)
22220 .unwrap()
22221 .iter()
22222 .map(|breakpoint| {
22223 (
22224 breakpoint.row,
22225 Breakpoint {
22226 message: breakpoint.message.clone(),
22227 state: breakpoint.state,
22228 condition: breakpoint.condition.clone(),
22229 hit_condition: breakpoint.hit_condition.clone(),
22230 },
22231 )
22232 })
22233 .collect::<Vec<_>>();
22234
22235 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
22236
22237 assert_eq!(expected, breakpoint);
22238 }
22239}
22240
22241fn add_log_breakpoint_at_cursor(
22242 editor: &mut Editor,
22243 log_message: &str,
22244 window: &mut Window,
22245 cx: &mut Context<Editor>,
22246) {
22247 let (anchor, bp) = editor
22248 .breakpoints_at_cursors(window, cx)
22249 .first()
22250 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
22251 .unwrap_or_else(|| {
22252 let cursor_position: Point = editor.selections.newest(cx).head();
22253
22254 let breakpoint_position = editor
22255 .snapshot(window, cx)
22256 .display_snapshot
22257 .buffer_snapshot
22258 .anchor_before(Point::new(cursor_position.row, 0));
22259
22260 (breakpoint_position, Breakpoint::new_log(log_message))
22261 });
22262
22263 editor.edit_breakpoint_at_anchor(
22264 anchor,
22265 bp,
22266 BreakpointEditAction::EditLogMessage(log_message.into()),
22267 cx,
22268 );
22269}
22270
22271#[gpui::test]
22272async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
22273 init_test(cx, |_| {});
22274
22275 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22276 let fs = FakeFs::new(cx.executor());
22277 fs.insert_tree(
22278 path!("/a"),
22279 json!({
22280 "main.rs": sample_text,
22281 }),
22282 )
22283 .await;
22284 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22285 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22286 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22287
22288 let fs = FakeFs::new(cx.executor());
22289 fs.insert_tree(
22290 path!("/a"),
22291 json!({
22292 "main.rs": sample_text,
22293 }),
22294 )
22295 .await;
22296 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22297 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22298 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22299 let worktree_id = workspace
22300 .update(cx, |workspace, _window, cx| {
22301 workspace.project().update(cx, |project, cx| {
22302 project.worktrees(cx).next().unwrap().read(cx).id()
22303 })
22304 })
22305 .unwrap();
22306
22307 let buffer = project
22308 .update(cx, |project, cx| {
22309 project.open_buffer((worktree_id, "main.rs"), cx)
22310 })
22311 .await
22312 .unwrap();
22313
22314 let (editor, cx) = cx.add_window_view(|window, cx| {
22315 Editor::new(
22316 EditorMode::full(),
22317 MultiBuffer::build_from_buffer(buffer, cx),
22318 Some(project.clone()),
22319 window,
22320 cx,
22321 )
22322 });
22323
22324 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22325 let abs_path = project.read_with(cx, |project, cx| {
22326 project
22327 .absolute_path(&project_path, cx)
22328 .map(Arc::from)
22329 .unwrap()
22330 });
22331
22332 // assert we can add breakpoint on the first line
22333 editor.update_in(cx, |editor, window, cx| {
22334 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22335 editor.move_to_end(&MoveToEnd, window, cx);
22336 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22337 });
22338
22339 let breakpoints = editor.update(cx, |editor, cx| {
22340 editor
22341 .breakpoint_store()
22342 .as_ref()
22343 .unwrap()
22344 .read(cx)
22345 .all_source_breakpoints(cx)
22346 });
22347
22348 assert_eq!(1, breakpoints.len());
22349 assert_breakpoint(
22350 &breakpoints,
22351 &abs_path,
22352 vec![
22353 (0, Breakpoint::new_standard()),
22354 (3, Breakpoint::new_standard()),
22355 ],
22356 );
22357
22358 editor.update_in(cx, |editor, window, cx| {
22359 editor.move_to_beginning(&MoveToBeginning, window, cx);
22360 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22361 });
22362
22363 let breakpoints = editor.update(cx, |editor, cx| {
22364 editor
22365 .breakpoint_store()
22366 .as_ref()
22367 .unwrap()
22368 .read(cx)
22369 .all_source_breakpoints(cx)
22370 });
22371
22372 assert_eq!(1, breakpoints.len());
22373 assert_breakpoint(
22374 &breakpoints,
22375 &abs_path,
22376 vec![(3, Breakpoint::new_standard())],
22377 );
22378
22379 editor.update_in(cx, |editor, window, cx| {
22380 editor.move_to_end(&MoveToEnd, window, cx);
22381 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22382 });
22383
22384 let breakpoints = editor.update(cx, |editor, cx| {
22385 editor
22386 .breakpoint_store()
22387 .as_ref()
22388 .unwrap()
22389 .read(cx)
22390 .all_source_breakpoints(cx)
22391 });
22392
22393 assert_eq!(0, breakpoints.len());
22394 assert_breakpoint(&breakpoints, &abs_path, vec![]);
22395}
22396
22397#[gpui::test]
22398async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
22399 init_test(cx, |_| {});
22400
22401 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22402
22403 let fs = FakeFs::new(cx.executor());
22404 fs.insert_tree(
22405 path!("/a"),
22406 json!({
22407 "main.rs": sample_text,
22408 }),
22409 )
22410 .await;
22411 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22412 let (workspace, cx) =
22413 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22414
22415 let worktree_id = workspace.update(cx, |workspace, cx| {
22416 workspace.project().update(cx, |project, cx| {
22417 project.worktrees(cx).next().unwrap().read(cx).id()
22418 })
22419 });
22420
22421 let buffer = project
22422 .update(cx, |project, cx| {
22423 project.open_buffer((worktree_id, "main.rs"), cx)
22424 })
22425 .await
22426 .unwrap();
22427
22428 let (editor, cx) = cx.add_window_view(|window, cx| {
22429 Editor::new(
22430 EditorMode::full(),
22431 MultiBuffer::build_from_buffer(buffer, cx),
22432 Some(project.clone()),
22433 window,
22434 cx,
22435 )
22436 });
22437
22438 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22439 let abs_path = project.read_with(cx, |project, cx| {
22440 project
22441 .absolute_path(&project_path, cx)
22442 .map(Arc::from)
22443 .unwrap()
22444 });
22445
22446 editor.update_in(cx, |editor, window, cx| {
22447 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
22448 });
22449
22450 let breakpoints = editor.update(cx, |editor, cx| {
22451 editor
22452 .breakpoint_store()
22453 .as_ref()
22454 .unwrap()
22455 .read(cx)
22456 .all_source_breakpoints(cx)
22457 });
22458
22459 assert_breakpoint(
22460 &breakpoints,
22461 &abs_path,
22462 vec![(0, Breakpoint::new_log("hello world"))],
22463 );
22464
22465 // Removing a log message from a log breakpoint should remove it
22466 editor.update_in(cx, |editor, window, cx| {
22467 add_log_breakpoint_at_cursor(editor, "", window, cx);
22468 });
22469
22470 let breakpoints = editor.update(cx, |editor, cx| {
22471 editor
22472 .breakpoint_store()
22473 .as_ref()
22474 .unwrap()
22475 .read(cx)
22476 .all_source_breakpoints(cx)
22477 });
22478
22479 assert_breakpoint(&breakpoints, &abs_path, vec![]);
22480
22481 editor.update_in(cx, |editor, window, cx| {
22482 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22483 editor.move_to_end(&MoveToEnd, window, cx);
22484 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22485 // Not adding a log message to a standard breakpoint shouldn't remove it
22486 add_log_breakpoint_at_cursor(editor, "", window, cx);
22487 });
22488
22489 let breakpoints = editor.update(cx, |editor, cx| {
22490 editor
22491 .breakpoint_store()
22492 .as_ref()
22493 .unwrap()
22494 .read(cx)
22495 .all_source_breakpoints(cx)
22496 });
22497
22498 assert_breakpoint(
22499 &breakpoints,
22500 &abs_path,
22501 vec![
22502 (0, Breakpoint::new_standard()),
22503 (3, Breakpoint::new_standard()),
22504 ],
22505 );
22506
22507 editor.update_in(cx, |editor, window, cx| {
22508 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
22509 });
22510
22511 let breakpoints = editor.update(cx, |editor, cx| {
22512 editor
22513 .breakpoint_store()
22514 .as_ref()
22515 .unwrap()
22516 .read(cx)
22517 .all_source_breakpoints(cx)
22518 });
22519
22520 assert_breakpoint(
22521 &breakpoints,
22522 &abs_path,
22523 vec![
22524 (0, Breakpoint::new_standard()),
22525 (3, Breakpoint::new_log("hello world")),
22526 ],
22527 );
22528
22529 editor.update_in(cx, |editor, window, cx| {
22530 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
22531 });
22532
22533 let breakpoints = editor.update(cx, |editor, cx| {
22534 editor
22535 .breakpoint_store()
22536 .as_ref()
22537 .unwrap()
22538 .read(cx)
22539 .all_source_breakpoints(cx)
22540 });
22541
22542 assert_breakpoint(
22543 &breakpoints,
22544 &abs_path,
22545 vec![
22546 (0, Breakpoint::new_standard()),
22547 (3, Breakpoint::new_log("hello Earth!!")),
22548 ],
22549 );
22550}
22551
22552/// This also tests that Editor::breakpoint_at_cursor_head is working properly
22553/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
22554/// or when breakpoints were placed out of order. This tests for a regression too
22555#[gpui::test]
22556async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
22557 init_test(cx, |_| {});
22558
22559 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22560 let fs = FakeFs::new(cx.executor());
22561 fs.insert_tree(
22562 path!("/a"),
22563 json!({
22564 "main.rs": sample_text,
22565 }),
22566 )
22567 .await;
22568 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22569 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22570 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22571
22572 let fs = FakeFs::new(cx.executor());
22573 fs.insert_tree(
22574 path!("/a"),
22575 json!({
22576 "main.rs": sample_text,
22577 }),
22578 )
22579 .await;
22580 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22581 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22582 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22583 let worktree_id = workspace
22584 .update(cx, |workspace, _window, cx| {
22585 workspace.project().update(cx, |project, cx| {
22586 project.worktrees(cx).next().unwrap().read(cx).id()
22587 })
22588 })
22589 .unwrap();
22590
22591 let buffer = project
22592 .update(cx, |project, cx| {
22593 project.open_buffer((worktree_id, "main.rs"), cx)
22594 })
22595 .await
22596 .unwrap();
22597
22598 let (editor, cx) = cx.add_window_view(|window, cx| {
22599 Editor::new(
22600 EditorMode::full(),
22601 MultiBuffer::build_from_buffer(buffer, cx),
22602 Some(project.clone()),
22603 window,
22604 cx,
22605 )
22606 });
22607
22608 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22609 let abs_path = project.read_with(cx, |project, cx| {
22610 project
22611 .absolute_path(&project_path, cx)
22612 .map(Arc::from)
22613 .unwrap()
22614 });
22615
22616 // assert we can add breakpoint on the first line
22617 editor.update_in(cx, |editor, window, cx| {
22618 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22619 editor.move_to_end(&MoveToEnd, window, cx);
22620 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22621 editor.move_up(&MoveUp, window, cx);
22622 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22623 });
22624
22625 let breakpoints = editor.update(cx, |editor, cx| {
22626 editor
22627 .breakpoint_store()
22628 .as_ref()
22629 .unwrap()
22630 .read(cx)
22631 .all_source_breakpoints(cx)
22632 });
22633
22634 assert_eq!(1, breakpoints.len());
22635 assert_breakpoint(
22636 &breakpoints,
22637 &abs_path,
22638 vec![
22639 (0, Breakpoint::new_standard()),
22640 (2, Breakpoint::new_standard()),
22641 (3, Breakpoint::new_standard()),
22642 ],
22643 );
22644
22645 editor.update_in(cx, |editor, window, cx| {
22646 editor.move_to_beginning(&MoveToBeginning, window, cx);
22647 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22648 editor.move_to_end(&MoveToEnd, window, cx);
22649 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22650 // Disabling a breakpoint that doesn't exist should do nothing
22651 editor.move_up(&MoveUp, window, cx);
22652 editor.move_up(&MoveUp, window, cx);
22653 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22654 });
22655
22656 let breakpoints = editor.update(cx, |editor, cx| {
22657 editor
22658 .breakpoint_store()
22659 .as_ref()
22660 .unwrap()
22661 .read(cx)
22662 .all_source_breakpoints(cx)
22663 });
22664
22665 let disable_breakpoint = {
22666 let mut bp = Breakpoint::new_standard();
22667 bp.state = BreakpointState::Disabled;
22668 bp
22669 };
22670
22671 assert_eq!(1, breakpoints.len());
22672 assert_breakpoint(
22673 &breakpoints,
22674 &abs_path,
22675 vec![
22676 (0, disable_breakpoint.clone()),
22677 (2, Breakpoint::new_standard()),
22678 (3, disable_breakpoint.clone()),
22679 ],
22680 );
22681
22682 editor.update_in(cx, |editor, window, cx| {
22683 editor.move_to_beginning(&MoveToBeginning, window, cx);
22684 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
22685 editor.move_to_end(&MoveToEnd, window, cx);
22686 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
22687 editor.move_up(&MoveUp, window, cx);
22688 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22689 });
22690
22691 let breakpoints = editor.update(cx, |editor, cx| {
22692 editor
22693 .breakpoint_store()
22694 .as_ref()
22695 .unwrap()
22696 .read(cx)
22697 .all_source_breakpoints(cx)
22698 });
22699
22700 assert_eq!(1, breakpoints.len());
22701 assert_breakpoint(
22702 &breakpoints,
22703 &abs_path,
22704 vec![
22705 (0, Breakpoint::new_standard()),
22706 (2, disable_breakpoint),
22707 (3, Breakpoint::new_standard()),
22708 ],
22709 );
22710}
22711
22712#[gpui::test]
22713async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
22714 init_test(cx, |_| {});
22715 let capabilities = lsp::ServerCapabilities {
22716 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
22717 prepare_provider: Some(true),
22718 work_done_progress_options: Default::default(),
22719 })),
22720 ..Default::default()
22721 };
22722 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
22723
22724 cx.set_state(indoc! {"
22725 struct Fˇoo {}
22726 "});
22727
22728 cx.update_editor(|editor, _, cx| {
22729 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
22730 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
22731 editor.highlight_background::<DocumentHighlightRead>(
22732 &[highlight_range],
22733 |theme| theme.colors().editor_document_highlight_read_background,
22734 cx,
22735 );
22736 });
22737
22738 let mut prepare_rename_handler = cx
22739 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
22740 move |_, _, _| async move {
22741 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
22742 start: lsp::Position {
22743 line: 0,
22744 character: 7,
22745 },
22746 end: lsp::Position {
22747 line: 0,
22748 character: 10,
22749 },
22750 })))
22751 },
22752 );
22753 let prepare_rename_task = cx
22754 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
22755 .expect("Prepare rename was not started");
22756 prepare_rename_handler.next().await.unwrap();
22757 prepare_rename_task.await.expect("Prepare rename failed");
22758
22759 let mut rename_handler =
22760 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
22761 let edit = lsp::TextEdit {
22762 range: lsp::Range {
22763 start: lsp::Position {
22764 line: 0,
22765 character: 7,
22766 },
22767 end: lsp::Position {
22768 line: 0,
22769 character: 10,
22770 },
22771 },
22772 new_text: "FooRenamed".to_string(),
22773 };
22774 Ok(Some(lsp::WorkspaceEdit::new(
22775 // Specify the same edit twice
22776 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
22777 )))
22778 });
22779 let rename_task = cx
22780 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
22781 .expect("Confirm rename was not started");
22782 rename_handler.next().await.unwrap();
22783 rename_task.await.expect("Confirm rename failed");
22784 cx.run_until_parked();
22785
22786 // Despite two edits, only one is actually applied as those are identical
22787 cx.assert_editor_state(indoc! {"
22788 struct FooRenamedˇ {}
22789 "});
22790}
22791
22792#[gpui::test]
22793async fn test_rename_without_prepare(cx: &mut TestAppContext) {
22794 init_test(cx, |_| {});
22795 // These capabilities indicate that the server does not support prepare rename.
22796 let capabilities = lsp::ServerCapabilities {
22797 rename_provider: Some(lsp::OneOf::Left(true)),
22798 ..Default::default()
22799 };
22800 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
22801
22802 cx.set_state(indoc! {"
22803 struct Fˇoo {}
22804 "});
22805
22806 cx.update_editor(|editor, _window, cx| {
22807 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
22808 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
22809 editor.highlight_background::<DocumentHighlightRead>(
22810 &[highlight_range],
22811 |theme| theme.colors().editor_document_highlight_read_background,
22812 cx,
22813 );
22814 });
22815
22816 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
22817 .expect("Prepare rename was not started")
22818 .await
22819 .expect("Prepare rename failed");
22820
22821 let mut rename_handler =
22822 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
22823 let edit = lsp::TextEdit {
22824 range: lsp::Range {
22825 start: lsp::Position {
22826 line: 0,
22827 character: 7,
22828 },
22829 end: lsp::Position {
22830 line: 0,
22831 character: 10,
22832 },
22833 },
22834 new_text: "FooRenamed".to_string(),
22835 };
22836 Ok(Some(lsp::WorkspaceEdit::new(
22837 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
22838 )))
22839 });
22840 let rename_task = cx
22841 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
22842 .expect("Confirm rename was not started");
22843 rename_handler.next().await.unwrap();
22844 rename_task.await.expect("Confirm rename failed");
22845 cx.run_until_parked();
22846
22847 // Correct range is renamed, as `surrounding_word` is used to find it.
22848 cx.assert_editor_state(indoc! {"
22849 struct FooRenamedˇ {}
22850 "});
22851}
22852
22853#[gpui::test]
22854async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
22855 init_test(cx, |_| {});
22856 let mut cx = EditorTestContext::new(cx).await;
22857
22858 let language = Arc::new(
22859 Language::new(
22860 LanguageConfig::default(),
22861 Some(tree_sitter_html::LANGUAGE.into()),
22862 )
22863 .with_brackets_query(
22864 r#"
22865 ("<" @open "/>" @close)
22866 ("</" @open ">" @close)
22867 ("<" @open ">" @close)
22868 ("\"" @open "\"" @close)
22869 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
22870 "#,
22871 )
22872 .unwrap(),
22873 );
22874 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22875
22876 cx.set_state(indoc! {"
22877 <span>ˇ</span>
22878 "});
22879 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
22880 cx.assert_editor_state(indoc! {"
22881 <span>
22882 ˇ
22883 </span>
22884 "});
22885
22886 cx.set_state(indoc! {"
22887 <span><span></span>ˇ</span>
22888 "});
22889 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
22890 cx.assert_editor_state(indoc! {"
22891 <span><span></span>
22892 ˇ</span>
22893 "});
22894
22895 cx.set_state(indoc! {"
22896 <span>ˇ
22897 </span>
22898 "});
22899 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
22900 cx.assert_editor_state(indoc! {"
22901 <span>
22902 ˇ
22903 </span>
22904 "});
22905}
22906
22907#[gpui::test(iterations = 10)]
22908async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
22909 init_test(cx, |_| {});
22910
22911 let fs = FakeFs::new(cx.executor());
22912 fs.insert_tree(
22913 path!("/dir"),
22914 json!({
22915 "a.ts": "a",
22916 }),
22917 )
22918 .await;
22919
22920 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
22921 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22922 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22923
22924 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22925 language_registry.add(Arc::new(Language::new(
22926 LanguageConfig {
22927 name: "TypeScript".into(),
22928 matcher: LanguageMatcher {
22929 path_suffixes: vec!["ts".to_string()],
22930 ..Default::default()
22931 },
22932 ..Default::default()
22933 },
22934 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
22935 )));
22936 let mut fake_language_servers = language_registry.register_fake_lsp(
22937 "TypeScript",
22938 FakeLspAdapter {
22939 capabilities: lsp::ServerCapabilities {
22940 code_lens_provider: Some(lsp::CodeLensOptions {
22941 resolve_provider: Some(true),
22942 }),
22943 execute_command_provider: Some(lsp::ExecuteCommandOptions {
22944 commands: vec!["_the/command".to_string()],
22945 ..lsp::ExecuteCommandOptions::default()
22946 }),
22947 ..lsp::ServerCapabilities::default()
22948 },
22949 ..FakeLspAdapter::default()
22950 },
22951 );
22952
22953 let editor = workspace
22954 .update(cx, |workspace, window, cx| {
22955 workspace.open_abs_path(
22956 PathBuf::from(path!("/dir/a.ts")),
22957 OpenOptions::default(),
22958 window,
22959 cx,
22960 )
22961 })
22962 .unwrap()
22963 .await
22964 .unwrap()
22965 .downcast::<Editor>()
22966 .unwrap();
22967 cx.executor().run_until_parked();
22968
22969 let fake_server = fake_language_servers.next().await.unwrap();
22970
22971 let buffer = editor.update(cx, |editor, cx| {
22972 editor
22973 .buffer()
22974 .read(cx)
22975 .as_singleton()
22976 .expect("have opened a single file by path")
22977 });
22978
22979 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
22980 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
22981 drop(buffer_snapshot);
22982 let actions = cx
22983 .update_window(*workspace, |_, window, cx| {
22984 project.code_actions(&buffer, anchor..anchor, window, cx)
22985 })
22986 .unwrap();
22987
22988 fake_server
22989 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
22990 Ok(Some(vec![
22991 lsp::CodeLens {
22992 range: lsp::Range::default(),
22993 command: Some(lsp::Command {
22994 title: "Code lens command".to_owned(),
22995 command: "_the/command".to_owned(),
22996 arguments: None,
22997 }),
22998 data: None,
22999 },
23000 lsp::CodeLens {
23001 range: lsp::Range::default(),
23002 command: Some(lsp::Command {
23003 title: "Command not in capabilities".to_owned(),
23004 command: "not in capabilities".to_owned(),
23005 arguments: None,
23006 }),
23007 data: None,
23008 },
23009 lsp::CodeLens {
23010 range: lsp::Range {
23011 start: lsp::Position {
23012 line: 1,
23013 character: 1,
23014 },
23015 end: lsp::Position {
23016 line: 1,
23017 character: 1,
23018 },
23019 },
23020 command: Some(lsp::Command {
23021 title: "Command not in range".to_owned(),
23022 command: "_the/command".to_owned(),
23023 arguments: None,
23024 }),
23025 data: None,
23026 },
23027 ]))
23028 })
23029 .next()
23030 .await;
23031
23032 let actions = actions.await.unwrap();
23033 assert_eq!(
23034 actions.len(),
23035 1,
23036 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
23037 );
23038 let action = actions[0].clone();
23039 let apply = project.update(cx, |project, cx| {
23040 project.apply_code_action(buffer.clone(), action, true, cx)
23041 });
23042
23043 // Resolving the code action does not populate its edits. In absence of
23044 // edits, we must execute the given command.
23045 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
23046 |mut lens, _| async move {
23047 let lens_command = lens.command.as_mut().expect("should have a command");
23048 assert_eq!(lens_command.title, "Code lens command");
23049 lens_command.arguments = Some(vec![json!("the-argument")]);
23050 Ok(lens)
23051 },
23052 );
23053
23054 // While executing the command, the language server sends the editor
23055 // a `workspaceEdit` request.
23056 fake_server
23057 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
23058 let fake = fake_server.clone();
23059 move |params, _| {
23060 assert_eq!(params.command, "_the/command");
23061 let fake = fake.clone();
23062 async move {
23063 fake.server
23064 .request::<lsp::request::ApplyWorkspaceEdit>(
23065 lsp::ApplyWorkspaceEditParams {
23066 label: None,
23067 edit: lsp::WorkspaceEdit {
23068 changes: Some(
23069 [(
23070 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
23071 vec![lsp::TextEdit {
23072 range: lsp::Range::new(
23073 lsp::Position::new(0, 0),
23074 lsp::Position::new(0, 0),
23075 ),
23076 new_text: "X".into(),
23077 }],
23078 )]
23079 .into_iter()
23080 .collect(),
23081 ),
23082 ..lsp::WorkspaceEdit::default()
23083 },
23084 },
23085 )
23086 .await
23087 .into_response()
23088 .unwrap();
23089 Ok(Some(json!(null)))
23090 }
23091 }
23092 })
23093 .next()
23094 .await;
23095
23096 // Applying the code lens command returns a project transaction containing the edits
23097 // sent by the language server in its `workspaceEdit` request.
23098 let transaction = apply.await.unwrap();
23099 assert!(transaction.0.contains_key(&buffer));
23100 buffer.update(cx, |buffer, cx| {
23101 assert_eq!(buffer.text(), "Xa");
23102 buffer.undo(cx);
23103 assert_eq!(buffer.text(), "a");
23104 });
23105
23106 let actions_after_edits = cx
23107 .update_window(*workspace, |_, window, cx| {
23108 project.code_actions(&buffer, anchor..anchor, window, cx)
23109 })
23110 .unwrap()
23111 .await
23112 .unwrap();
23113 assert_eq!(
23114 actions, actions_after_edits,
23115 "For the same selection, same code lens actions should be returned"
23116 );
23117
23118 let _responses =
23119 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23120 panic!("No more code lens requests are expected");
23121 });
23122 editor.update_in(cx, |editor, window, cx| {
23123 editor.select_all(&SelectAll, window, cx);
23124 });
23125 cx.executor().run_until_parked();
23126 let new_actions = cx
23127 .update_window(*workspace, |_, window, cx| {
23128 project.code_actions(&buffer, anchor..anchor, window, cx)
23129 })
23130 .unwrap()
23131 .await
23132 .unwrap();
23133 assert_eq!(
23134 actions, new_actions,
23135 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
23136 );
23137}
23138
23139#[gpui::test]
23140async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
23141 init_test(cx, |_| {});
23142
23143 let fs = FakeFs::new(cx.executor());
23144 let main_text = r#"fn main() {
23145println!("1");
23146println!("2");
23147println!("3");
23148println!("4");
23149println!("5");
23150}"#;
23151 let lib_text = "mod foo {}";
23152 fs.insert_tree(
23153 path!("/a"),
23154 json!({
23155 "lib.rs": lib_text,
23156 "main.rs": main_text,
23157 }),
23158 )
23159 .await;
23160
23161 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23162 let (workspace, cx) =
23163 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23164 let worktree_id = workspace.update(cx, |workspace, cx| {
23165 workspace.project().update(cx, |project, cx| {
23166 project.worktrees(cx).next().unwrap().read(cx).id()
23167 })
23168 });
23169
23170 let expected_ranges = vec![
23171 Point::new(0, 0)..Point::new(0, 0),
23172 Point::new(1, 0)..Point::new(1, 1),
23173 Point::new(2, 0)..Point::new(2, 2),
23174 Point::new(3, 0)..Point::new(3, 3),
23175 ];
23176
23177 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23178 let editor_1 = workspace
23179 .update_in(cx, |workspace, window, cx| {
23180 workspace.open_path(
23181 (worktree_id, "main.rs"),
23182 Some(pane_1.downgrade()),
23183 true,
23184 window,
23185 cx,
23186 )
23187 })
23188 .unwrap()
23189 .await
23190 .downcast::<Editor>()
23191 .unwrap();
23192 pane_1.update(cx, |pane, cx| {
23193 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23194 open_editor.update(cx, |editor, cx| {
23195 assert_eq!(
23196 editor.display_text(cx),
23197 main_text,
23198 "Original main.rs text on initial open",
23199 );
23200 assert_eq!(
23201 editor
23202 .selections
23203 .all::<Point>(cx)
23204 .into_iter()
23205 .map(|s| s.range())
23206 .collect::<Vec<_>>(),
23207 vec![Point::zero()..Point::zero()],
23208 "Default selections on initial open",
23209 );
23210 })
23211 });
23212 editor_1.update_in(cx, |editor, window, cx| {
23213 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23214 s.select_ranges(expected_ranges.clone());
23215 });
23216 });
23217
23218 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
23219 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
23220 });
23221 let editor_2 = workspace
23222 .update_in(cx, |workspace, window, cx| {
23223 workspace.open_path(
23224 (worktree_id, "main.rs"),
23225 Some(pane_2.downgrade()),
23226 true,
23227 window,
23228 cx,
23229 )
23230 })
23231 .unwrap()
23232 .await
23233 .downcast::<Editor>()
23234 .unwrap();
23235 pane_2.update(cx, |pane, cx| {
23236 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23237 open_editor.update(cx, |editor, cx| {
23238 assert_eq!(
23239 editor.display_text(cx),
23240 main_text,
23241 "Original main.rs text on initial open in another panel",
23242 );
23243 assert_eq!(
23244 editor
23245 .selections
23246 .all::<Point>(cx)
23247 .into_iter()
23248 .map(|s| s.range())
23249 .collect::<Vec<_>>(),
23250 vec![Point::zero()..Point::zero()],
23251 "Default selections on initial open in another panel",
23252 );
23253 })
23254 });
23255
23256 editor_2.update_in(cx, |editor, window, cx| {
23257 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
23258 });
23259
23260 let _other_editor_1 = workspace
23261 .update_in(cx, |workspace, window, cx| {
23262 workspace.open_path(
23263 (worktree_id, "lib.rs"),
23264 Some(pane_1.downgrade()),
23265 true,
23266 window,
23267 cx,
23268 )
23269 })
23270 .unwrap()
23271 .await
23272 .downcast::<Editor>()
23273 .unwrap();
23274 pane_1
23275 .update_in(cx, |pane, window, cx| {
23276 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23277 })
23278 .await
23279 .unwrap();
23280 drop(editor_1);
23281 pane_1.update(cx, |pane, cx| {
23282 pane.active_item()
23283 .unwrap()
23284 .downcast::<Editor>()
23285 .unwrap()
23286 .update(cx, |editor, cx| {
23287 assert_eq!(
23288 editor.display_text(cx),
23289 lib_text,
23290 "Other file should be open and active",
23291 );
23292 });
23293 assert_eq!(pane.items().count(), 1, "No other editors should be open");
23294 });
23295
23296 let _other_editor_2 = workspace
23297 .update_in(cx, |workspace, window, cx| {
23298 workspace.open_path(
23299 (worktree_id, "lib.rs"),
23300 Some(pane_2.downgrade()),
23301 true,
23302 window,
23303 cx,
23304 )
23305 })
23306 .unwrap()
23307 .await
23308 .downcast::<Editor>()
23309 .unwrap();
23310 pane_2
23311 .update_in(cx, |pane, window, cx| {
23312 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23313 })
23314 .await
23315 .unwrap();
23316 drop(editor_2);
23317 pane_2.update(cx, |pane, cx| {
23318 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23319 open_editor.update(cx, |editor, cx| {
23320 assert_eq!(
23321 editor.display_text(cx),
23322 lib_text,
23323 "Other file should be open and active in another panel too",
23324 );
23325 });
23326 assert_eq!(
23327 pane.items().count(),
23328 1,
23329 "No other editors should be open in another pane",
23330 );
23331 });
23332
23333 let _editor_1_reopened = workspace
23334 .update_in(cx, |workspace, window, cx| {
23335 workspace.open_path(
23336 (worktree_id, "main.rs"),
23337 Some(pane_1.downgrade()),
23338 true,
23339 window,
23340 cx,
23341 )
23342 })
23343 .unwrap()
23344 .await
23345 .downcast::<Editor>()
23346 .unwrap();
23347 let _editor_2_reopened = workspace
23348 .update_in(cx, |workspace, window, cx| {
23349 workspace.open_path(
23350 (worktree_id, "main.rs"),
23351 Some(pane_2.downgrade()),
23352 true,
23353 window,
23354 cx,
23355 )
23356 })
23357 .unwrap()
23358 .await
23359 .downcast::<Editor>()
23360 .unwrap();
23361 pane_1.update(cx, |pane, cx| {
23362 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23363 open_editor.update(cx, |editor, cx| {
23364 assert_eq!(
23365 editor.display_text(cx),
23366 main_text,
23367 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
23368 );
23369 assert_eq!(
23370 editor
23371 .selections
23372 .all::<Point>(cx)
23373 .into_iter()
23374 .map(|s| s.range())
23375 .collect::<Vec<_>>(),
23376 expected_ranges,
23377 "Previous editor in the 1st panel had selections and should get them restored on reopen",
23378 );
23379 })
23380 });
23381 pane_2.update(cx, |pane, cx| {
23382 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23383 open_editor.update(cx, |editor, cx| {
23384 assert_eq!(
23385 editor.display_text(cx),
23386 r#"fn main() {
23387⋯rintln!("1");
23388⋯intln!("2");
23389⋯ntln!("3");
23390println!("4");
23391println!("5");
23392}"#,
23393 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
23394 );
23395 assert_eq!(
23396 editor
23397 .selections
23398 .all::<Point>(cx)
23399 .into_iter()
23400 .map(|s| s.range())
23401 .collect::<Vec<_>>(),
23402 vec![Point::zero()..Point::zero()],
23403 "Previous editor in the 2nd pane had no selections changed hence should restore none",
23404 );
23405 })
23406 });
23407}
23408
23409#[gpui::test]
23410async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
23411 init_test(cx, |_| {});
23412
23413 let fs = FakeFs::new(cx.executor());
23414 let main_text = r#"fn main() {
23415println!("1");
23416println!("2");
23417println!("3");
23418println!("4");
23419println!("5");
23420}"#;
23421 let lib_text = "mod foo {}";
23422 fs.insert_tree(
23423 path!("/a"),
23424 json!({
23425 "lib.rs": lib_text,
23426 "main.rs": main_text,
23427 }),
23428 )
23429 .await;
23430
23431 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23432 let (workspace, cx) =
23433 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23434 let worktree_id = workspace.update(cx, |workspace, cx| {
23435 workspace.project().update(cx, |project, cx| {
23436 project.worktrees(cx).next().unwrap().read(cx).id()
23437 })
23438 });
23439
23440 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23441 let editor = workspace
23442 .update_in(cx, |workspace, window, cx| {
23443 workspace.open_path(
23444 (worktree_id, "main.rs"),
23445 Some(pane.downgrade()),
23446 true,
23447 window,
23448 cx,
23449 )
23450 })
23451 .unwrap()
23452 .await
23453 .downcast::<Editor>()
23454 .unwrap();
23455 pane.update(cx, |pane, cx| {
23456 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23457 open_editor.update(cx, |editor, cx| {
23458 assert_eq!(
23459 editor.display_text(cx),
23460 main_text,
23461 "Original main.rs text on initial open",
23462 );
23463 })
23464 });
23465 editor.update_in(cx, |editor, window, cx| {
23466 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
23467 });
23468
23469 cx.update_global(|store: &mut SettingsStore, cx| {
23470 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
23471 s.restore_on_file_reopen = Some(false);
23472 });
23473 });
23474 editor.update_in(cx, |editor, window, cx| {
23475 editor.fold_ranges(
23476 vec![
23477 Point::new(1, 0)..Point::new(1, 1),
23478 Point::new(2, 0)..Point::new(2, 2),
23479 Point::new(3, 0)..Point::new(3, 3),
23480 ],
23481 false,
23482 window,
23483 cx,
23484 );
23485 });
23486 pane.update_in(cx, |pane, window, cx| {
23487 pane.close_all_items(&CloseAllItems::default(), window, cx)
23488 })
23489 .await
23490 .unwrap();
23491 pane.update(cx, |pane, _| {
23492 assert!(pane.active_item().is_none());
23493 });
23494 cx.update_global(|store: &mut SettingsStore, cx| {
23495 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
23496 s.restore_on_file_reopen = Some(true);
23497 });
23498 });
23499
23500 let _editor_reopened = workspace
23501 .update_in(cx, |workspace, window, cx| {
23502 workspace.open_path(
23503 (worktree_id, "main.rs"),
23504 Some(pane.downgrade()),
23505 true,
23506 window,
23507 cx,
23508 )
23509 })
23510 .unwrap()
23511 .await
23512 .downcast::<Editor>()
23513 .unwrap();
23514 pane.update(cx, |pane, cx| {
23515 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23516 open_editor.update(cx, |editor, cx| {
23517 assert_eq!(
23518 editor.display_text(cx),
23519 main_text,
23520 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
23521 );
23522 })
23523 });
23524}
23525
23526#[gpui::test]
23527async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
23528 struct EmptyModalView {
23529 focus_handle: gpui::FocusHandle,
23530 }
23531 impl EventEmitter<DismissEvent> for EmptyModalView {}
23532 impl Render for EmptyModalView {
23533 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
23534 div()
23535 }
23536 }
23537 impl Focusable for EmptyModalView {
23538 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
23539 self.focus_handle.clone()
23540 }
23541 }
23542 impl workspace::ModalView for EmptyModalView {}
23543 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
23544 EmptyModalView {
23545 focus_handle: cx.focus_handle(),
23546 }
23547 }
23548
23549 init_test(cx, |_| {});
23550
23551 let fs = FakeFs::new(cx.executor());
23552 let project = Project::test(fs, [], cx).await;
23553 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23554 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
23555 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23556 let editor = cx.new_window_entity(|window, cx| {
23557 Editor::new(
23558 EditorMode::full(),
23559 buffer,
23560 Some(project.clone()),
23561 window,
23562 cx,
23563 )
23564 });
23565 workspace
23566 .update(cx, |workspace, window, cx| {
23567 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
23568 })
23569 .unwrap();
23570 editor.update_in(cx, |editor, window, cx| {
23571 editor.open_context_menu(&OpenContextMenu, window, cx);
23572 assert!(editor.mouse_context_menu.is_some());
23573 });
23574 workspace
23575 .update(cx, |workspace, window, cx| {
23576 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
23577 })
23578 .unwrap();
23579 cx.read(|cx| {
23580 assert!(editor.read(cx).mouse_context_menu.is_none());
23581 });
23582}
23583
23584#[gpui::test]
23585async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
23586 init_test(cx, |_| {});
23587
23588 let fs = FakeFs::new(cx.executor());
23589 fs.insert_file(path!("/file.html"), Default::default())
23590 .await;
23591
23592 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
23593
23594 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23595 let html_language = Arc::new(Language::new(
23596 LanguageConfig {
23597 name: "HTML".into(),
23598 matcher: LanguageMatcher {
23599 path_suffixes: vec!["html".to_string()],
23600 ..LanguageMatcher::default()
23601 },
23602 brackets: BracketPairConfig {
23603 pairs: vec![BracketPair {
23604 start: "<".into(),
23605 end: ">".into(),
23606 close: true,
23607 ..Default::default()
23608 }],
23609 ..Default::default()
23610 },
23611 ..Default::default()
23612 },
23613 Some(tree_sitter_html::LANGUAGE.into()),
23614 ));
23615 language_registry.add(html_language);
23616 let mut fake_servers = language_registry.register_fake_lsp(
23617 "HTML",
23618 FakeLspAdapter {
23619 capabilities: lsp::ServerCapabilities {
23620 completion_provider: Some(lsp::CompletionOptions {
23621 resolve_provider: Some(true),
23622 ..Default::default()
23623 }),
23624 ..Default::default()
23625 },
23626 ..Default::default()
23627 },
23628 );
23629
23630 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23631 let cx = &mut VisualTestContext::from_window(*workspace, cx);
23632
23633 let worktree_id = workspace
23634 .update(cx, |workspace, _window, cx| {
23635 workspace.project().update(cx, |project, cx| {
23636 project.worktrees(cx).next().unwrap().read(cx).id()
23637 })
23638 })
23639 .unwrap();
23640 project
23641 .update(cx, |project, cx| {
23642 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
23643 })
23644 .await
23645 .unwrap();
23646 let editor = workspace
23647 .update(cx, |workspace, window, cx| {
23648 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
23649 })
23650 .unwrap()
23651 .await
23652 .unwrap()
23653 .downcast::<Editor>()
23654 .unwrap();
23655
23656 let fake_server = fake_servers.next().await.unwrap();
23657 editor.update_in(cx, |editor, window, cx| {
23658 editor.set_text("<ad></ad>", window, cx);
23659 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
23660 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
23661 });
23662 let Some((buffer, _)) = editor
23663 .buffer
23664 .read(cx)
23665 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
23666 else {
23667 panic!("Failed to get buffer for selection position");
23668 };
23669 let buffer = buffer.read(cx);
23670 let buffer_id = buffer.remote_id();
23671 let opening_range =
23672 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
23673 let closing_range =
23674 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
23675 let mut linked_ranges = HashMap::default();
23676 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
23677 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
23678 });
23679 let mut completion_handle =
23680 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
23681 Ok(Some(lsp::CompletionResponse::Array(vec![
23682 lsp::CompletionItem {
23683 label: "head".to_string(),
23684 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
23685 lsp::InsertReplaceEdit {
23686 new_text: "head".to_string(),
23687 insert: lsp::Range::new(
23688 lsp::Position::new(0, 1),
23689 lsp::Position::new(0, 3),
23690 ),
23691 replace: lsp::Range::new(
23692 lsp::Position::new(0, 1),
23693 lsp::Position::new(0, 3),
23694 ),
23695 },
23696 )),
23697 ..Default::default()
23698 },
23699 ])))
23700 });
23701 editor.update_in(cx, |editor, window, cx| {
23702 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
23703 });
23704 cx.run_until_parked();
23705 completion_handle.next().await.unwrap();
23706 editor.update(cx, |editor, _| {
23707 assert!(
23708 editor.context_menu_visible(),
23709 "Completion menu should be visible"
23710 );
23711 });
23712 editor.update_in(cx, |editor, window, cx| {
23713 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
23714 });
23715 cx.executor().run_until_parked();
23716 editor.update(cx, |editor, cx| {
23717 assert_eq!(editor.text(cx), "<head></head>");
23718 });
23719}
23720
23721#[gpui::test]
23722async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
23723 init_test(cx, |_| {});
23724
23725 let fs = FakeFs::new(cx.executor());
23726 fs.insert_tree(
23727 path!("/root"),
23728 json!({
23729 "a": {
23730 "main.rs": "fn main() {}",
23731 },
23732 "foo": {
23733 "bar": {
23734 "external_file.rs": "pub mod external {}",
23735 }
23736 }
23737 }),
23738 )
23739 .await;
23740
23741 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
23742 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23743 language_registry.add(rust_lang());
23744 let _fake_servers = language_registry.register_fake_lsp(
23745 "Rust",
23746 FakeLspAdapter {
23747 ..FakeLspAdapter::default()
23748 },
23749 );
23750 let (workspace, cx) =
23751 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23752 let worktree_id = workspace.update(cx, |workspace, cx| {
23753 workspace.project().update(cx, |project, cx| {
23754 project.worktrees(cx).next().unwrap().read(cx).id()
23755 })
23756 });
23757
23758 let assert_language_servers_count =
23759 |expected: usize, context: &str, cx: &mut VisualTestContext| {
23760 project.update(cx, |project, cx| {
23761 let current = project
23762 .lsp_store()
23763 .read(cx)
23764 .as_local()
23765 .unwrap()
23766 .language_servers
23767 .len();
23768 assert_eq!(expected, current, "{context}");
23769 });
23770 };
23771
23772 assert_language_servers_count(
23773 0,
23774 "No servers should be running before any file is open",
23775 cx,
23776 );
23777 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23778 let main_editor = workspace
23779 .update_in(cx, |workspace, window, cx| {
23780 workspace.open_path(
23781 (worktree_id, "main.rs"),
23782 Some(pane.downgrade()),
23783 true,
23784 window,
23785 cx,
23786 )
23787 })
23788 .unwrap()
23789 .await
23790 .downcast::<Editor>()
23791 .unwrap();
23792 pane.update(cx, |pane, cx| {
23793 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23794 open_editor.update(cx, |editor, cx| {
23795 assert_eq!(
23796 editor.display_text(cx),
23797 "fn main() {}",
23798 "Original main.rs text on initial open",
23799 );
23800 });
23801 assert_eq!(open_editor, main_editor);
23802 });
23803 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
23804
23805 let external_editor = workspace
23806 .update_in(cx, |workspace, window, cx| {
23807 workspace.open_abs_path(
23808 PathBuf::from("/root/foo/bar/external_file.rs"),
23809 OpenOptions::default(),
23810 window,
23811 cx,
23812 )
23813 })
23814 .await
23815 .expect("opening external file")
23816 .downcast::<Editor>()
23817 .expect("downcasted external file's open element to editor");
23818 pane.update(cx, |pane, cx| {
23819 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23820 open_editor.update(cx, |editor, cx| {
23821 assert_eq!(
23822 editor.display_text(cx),
23823 "pub mod external {}",
23824 "External file is open now",
23825 );
23826 });
23827 assert_eq!(open_editor, external_editor);
23828 });
23829 assert_language_servers_count(
23830 1,
23831 "Second, external, *.rs file should join the existing server",
23832 cx,
23833 );
23834
23835 pane.update_in(cx, |pane, window, cx| {
23836 pane.close_active_item(&CloseActiveItem::default(), window, cx)
23837 })
23838 .await
23839 .unwrap();
23840 pane.update_in(cx, |pane, window, cx| {
23841 pane.navigate_backward(&Default::default(), window, cx);
23842 });
23843 cx.run_until_parked();
23844 pane.update(cx, |pane, cx| {
23845 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23846 open_editor.update(cx, |editor, cx| {
23847 assert_eq!(
23848 editor.display_text(cx),
23849 "pub mod external {}",
23850 "External file is open now",
23851 );
23852 });
23853 });
23854 assert_language_servers_count(
23855 1,
23856 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
23857 cx,
23858 );
23859
23860 cx.update(|_, cx| {
23861 workspace::reload(cx);
23862 });
23863 assert_language_servers_count(
23864 1,
23865 "After reloading the worktree with local and external files opened, only one project should be started",
23866 cx,
23867 );
23868}
23869
23870#[gpui::test]
23871async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
23872 init_test(cx, |_| {});
23873
23874 let mut cx = EditorTestContext::new(cx).await;
23875 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
23876 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23877
23878 // test cursor move to start of each line on tab
23879 // for `if`, `elif`, `else`, `while`, `with` and `for`
23880 cx.set_state(indoc! {"
23881 def main():
23882 ˇ for item in items:
23883 ˇ while item.active:
23884 ˇ if item.value > 10:
23885 ˇ continue
23886 ˇ elif item.value < 0:
23887 ˇ break
23888 ˇ else:
23889 ˇ with item.context() as ctx:
23890 ˇ yield count
23891 ˇ else:
23892 ˇ log('while else')
23893 ˇ else:
23894 ˇ log('for else')
23895 "});
23896 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
23897 cx.assert_editor_state(indoc! {"
23898 def main():
23899 ˇfor item in items:
23900 ˇwhile item.active:
23901 ˇif item.value > 10:
23902 ˇcontinue
23903 ˇelif item.value < 0:
23904 ˇbreak
23905 ˇelse:
23906 ˇwith item.context() as ctx:
23907 ˇyield count
23908 ˇelse:
23909 ˇlog('while else')
23910 ˇelse:
23911 ˇlog('for else')
23912 "});
23913 // test relative indent is preserved when tab
23914 // for `if`, `elif`, `else`, `while`, `with` and `for`
23915 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
23916 cx.assert_editor_state(indoc! {"
23917 def main():
23918 ˇfor item in items:
23919 ˇwhile item.active:
23920 ˇif item.value > 10:
23921 ˇcontinue
23922 ˇelif item.value < 0:
23923 ˇbreak
23924 ˇelse:
23925 ˇwith item.context() as ctx:
23926 ˇyield count
23927 ˇelse:
23928 ˇlog('while else')
23929 ˇelse:
23930 ˇlog('for else')
23931 "});
23932
23933 // test cursor move to start of each line on tab
23934 // for `try`, `except`, `else`, `finally`, `match` and `def`
23935 cx.set_state(indoc! {"
23936 def main():
23937 ˇ try:
23938 ˇ fetch()
23939 ˇ except ValueError:
23940 ˇ handle_error()
23941 ˇ else:
23942 ˇ match value:
23943 ˇ case _:
23944 ˇ finally:
23945 ˇ def status():
23946 ˇ return 0
23947 "});
23948 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
23949 cx.assert_editor_state(indoc! {"
23950 def main():
23951 ˇtry:
23952 ˇfetch()
23953 ˇexcept ValueError:
23954 ˇhandle_error()
23955 ˇelse:
23956 ˇmatch value:
23957 ˇcase _:
23958 ˇfinally:
23959 ˇdef status():
23960 ˇreturn 0
23961 "});
23962 // test relative indent is preserved when tab
23963 // for `try`, `except`, `else`, `finally`, `match` and `def`
23964 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
23965 cx.assert_editor_state(indoc! {"
23966 def main():
23967 ˇtry:
23968 ˇfetch()
23969 ˇexcept ValueError:
23970 ˇhandle_error()
23971 ˇelse:
23972 ˇmatch value:
23973 ˇcase _:
23974 ˇfinally:
23975 ˇdef status():
23976 ˇreturn 0
23977 "});
23978}
23979
23980#[gpui::test]
23981async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
23982 init_test(cx, |_| {});
23983
23984 let mut cx = EditorTestContext::new(cx).await;
23985 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
23986 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23987
23988 // test `else` auto outdents when typed inside `if` block
23989 cx.set_state(indoc! {"
23990 def main():
23991 if i == 2:
23992 return
23993 ˇ
23994 "});
23995 cx.update_editor(|editor, window, cx| {
23996 editor.handle_input("else:", window, cx);
23997 });
23998 cx.assert_editor_state(indoc! {"
23999 def main():
24000 if i == 2:
24001 return
24002 else:ˇ
24003 "});
24004
24005 // test `except` auto outdents when typed inside `try` block
24006 cx.set_state(indoc! {"
24007 def main():
24008 try:
24009 i = 2
24010 ˇ
24011 "});
24012 cx.update_editor(|editor, window, cx| {
24013 editor.handle_input("except:", window, cx);
24014 });
24015 cx.assert_editor_state(indoc! {"
24016 def main():
24017 try:
24018 i = 2
24019 except:ˇ
24020 "});
24021
24022 // test `else` auto outdents when typed inside `except` block
24023 cx.set_state(indoc! {"
24024 def main():
24025 try:
24026 i = 2
24027 except:
24028 j = 2
24029 ˇ
24030 "});
24031 cx.update_editor(|editor, window, cx| {
24032 editor.handle_input("else:", window, cx);
24033 });
24034 cx.assert_editor_state(indoc! {"
24035 def main():
24036 try:
24037 i = 2
24038 except:
24039 j = 2
24040 else:ˇ
24041 "});
24042
24043 // test `finally` auto outdents when typed inside `else` block
24044 cx.set_state(indoc! {"
24045 def main():
24046 try:
24047 i = 2
24048 except:
24049 j = 2
24050 else:
24051 k = 2
24052 ˇ
24053 "});
24054 cx.update_editor(|editor, window, cx| {
24055 editor.handle_input("finally:", window, cx);
24056 });
24057 cx.assert_editor_state(indoc! {"
24058 def main():
24059 try:
24060 i = 2
24061 except:
24062 j = 2
24063 else:
24064 k = 2
24065 finally:ˇ
24066 "});
24067
24068 // test `else` does not outdents when typed inside `except` block right after for block
24069 cx.set_state(indoc! {"
24070 def main():
24071 try:
24072 i = 2
24073 except:
24074 for i in range(n):
24075 pass
24076 ˇ
24077 "});
24078 cx.update_editor(|editor, window, cx| {
24079 editor.handle_input("else:", window, cx);
24080 });
24081 cx.assert_editor_state(indoc! {"
24082 def main():
24083 try:
24084 i = 2
24085 except:
24086 for i in range(n):
24087 pass
24088 else:ˇ
24089 "});
24090
24091 // test `finally` auto outdents when typed inside `else` block right after for block
24092 cx.set_state(indoc! {"
24093 def main():
24094 try:
24095 i = 2
24096 except:
24097 j = 2
24098 else:
24099 for i in range(n):
24100 pass
24101 ˇ
24102 "});
24103 cx.update_editor(|editor, window, cx| {
24104 editor.handle_input("finally:", window, cx);
24105 });
24106 cx.assert_editor_state(indoc! {"
24107 def main():
24108 try:
24109 i = 2
24110 except:
24111 j = 2
24112 else:
24113 for i in range(n):
24114 pass
24115 finally:ˇ
24116 "});
24117
24118 // test `except` outdents to inner "try" block
24119 cx.set_state(indoc! {"
24120 def main():
24121 try:
24122 i = 2
24123 if i == 2:
24124 try:
24125 i = 3
24126 ˇ
24127 "});
24128 cx.update_editor(|editor, window, cx| {
24129 editor.handle_input("except:", window, cx);
24130 });
24131 cx.assert_editor_state(indoc! {"
24132 def main():
24133 try:
24134 i = 2
24135 if i == 2:
24136 try:
24137 i = 3
24138 except:ˇ
24139 "});
24140
24141 // test `except` outdents to outer "try" block
24142 cx.set_state(indoc! {"
24143 def main():
24144 try:
24145 i = 2
24146 if i == 2:
24147 try:
24148 i = 3
24149 ˇ
24150 "});
24151 cx.update_editor(|editor, window, cx| {
24152 editor.handle_input("except:", window, cx);
24153 });
24154 cx.assert_editor_state(indoc! {"
24155 def main():
24156 try:
24157 i = 2
24158 if i == 2:
24159 try:
24160 i = 3
24161 except:ˇ
24162 "});
24163
24164 // test `else` stays at correct indent when typed after `for` block
24165 cx.set_state(indoc! {"
24166 def main():
24167 for i in range(10):
24168 if i == 3:
24169 break
24170 ˇ
24171 "});
24172 cx.update_editor(|editor, window, cx| {
24173 editor.handle_input("else:", window, cx);
24174 });
24175 cx.assert_editor_state(indoc! {"
24176 def main():
24177 for i in range(10):
24178 if i == 3:
24179 break
24180 else:ˇ
24181 "});
24182
24183 // test does not outdent on typing after line with square brackets
24184 cx.set_state(indoc! {"
24185 def f() -> list[str]:
24186 ˇ
24187 "});
24188 cx.update_editor(|editor, window, cx| {
24189 editor.handle_input("a", window, cx);
24190 });
24191 cx.assert_editor_state(indoc! {"
24192 def f() -> list[str]:
24193 aˇ
24194 "});
24195
24196 // test does not outdent on typing : after case keyword
24197 cx.set_state(indoc! {"
24198 match 1:
24199 caseˇ
24200 "});
24201 cx.update_editor(|editor, window, cx| {
24202 editor.handle_input(":", window, cx);
24203 });
24204 cx.assert_editor_state(indoc! {"
24205 match 1:
24206 case:ˇ
24207 "});
24208}
24209
24210#[gpui::test]
24211async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
24212 init_test(cx, |_| {});
24213 update_test_language_settings(cx, |settings| {
24214 settings.defaults.extend_comment_on_newline = Some(false);
24215 });
24216 let mut cx = EditorTestContext::new(cx).await;
24217 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24218 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24219
24220 // test correct indent after newline on comment
24221 cx.set_state(indoc! {"
24222 # COMMENT:ˇ
24223 "});
24224 cx.update_editor(|editor, window, cx| {
24225 editor.newline(&Newline, window, cx);
24226 });
24227 cx.assert_editor_state(indoc! {"
24228 # COMMENT:
24229 ˇ
24230 "});
24231
24232 // test correct indent after newline in brackets
24233 cx.set_state(indoc! {"
24234 {ˇ}
24235 "});
24236 cx.update_editor(|editor, window, cx| {
24237 editor.newline(&Newline, window, cx);
24238 });
24239 cx.run_until_parked();
24240 cx.assert_editor_state(indoc! {"
24241 {
24242 ˇ
24243 }
24244 "});
24245
24246 cx.set_state(indoc! {"
24247 (ˇ)
24248 "});
24249 cx.update_editor(|editor, window, cx| {
24250 editor.newline(&Newline, window, cx);
24251 });
24252 cx.run_until_parked();
24253 cx.assert_editor_state(indoc! {"
24254 (
24255 ˇ
24256 )
24257 "});
24258
24259 // do not indent after empty lists or dictionaries
24260 cx.set_state(indoc! {"
24261 a = []ˇ
24262 "});
24263 cx.update_editor(|editor, window, cx| {
24264 editor.newline(&Newline, window, cx);
24265 });
24266 cx.run_until_parked();
24267 cx.assert_editor_state(indoc! {"
24268 a = []
24269 ˇ
24270 "});
24271}
24272
24273#[gpui::test]
24274async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
24275 init_test(cx, |_| {});
24276
24277 let mut cx = EditorTestContext::new(cx).await;
24278 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24279 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24280
24281 // test cursor move to start of each line on tab
24282 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
24283 cx.set_state(indoc! {"
24284 function main() {
24285 ˇ for item in $items; do
24286 ˇ while [ -n \"$item\" ]; do
24287 ˇ if [ \"$value\" -gt 10 ]; then
24288 ˇ continue
24289 ˇ elif [ \"$value\" -lt 0 ]; then
24290 ˇ break
24291 ˇ else
24292 ˇ echo \"$item\"
24293 ˇ fi
24294 ˇ done
24295 ˇ done
24296 ˇ}
24297 "});
24298 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24299 cx.assert_editor_state(indoc! {"
24300 function main() {
24301 ˇfor item in $items; do
24302 ˇwhile [ -n \"$item\" ]; do
24303 ˇif [ \"$value\" -gt 10 ]; then
24304 ˇcontinue
24305 ˇelif [ \"$value\" -lt 0 ]; then
24306 ˇbreak
24307 ˇelse
24308 ˇecho \"$item\"
24309 ˇfi
24310 ˇdone
24311 ˇdone
24312 ˇ}
24313 "});
24314 // test relative indent is preserved when tab
24315 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24316 cx.assert_editor_state(indoc! {"
24317 function main() {
24318 ˇfor item in $items; do
24319 ˇwhile [ -n \"$item\" ]; do
24320 ˇif [ \"$value\" -gt 10 ]; then
24321 ˇcontinue
24322 ˇelif [ \"$value\" -lt 0 ]; then
24323 ˇbreak
24324 ˇelse
24325 ˇecho \"$item\"
24326 ˇfi
24327 ˇdone
24328 ˇdone
24329 ˇ}
24330 "});
24331
24332 // test cursor move to start of each line on tab
24333 // for `case` statement with patterns
24334 cx.set_state(indoc! {"
24335 function handle() {
24336 ˇ case \"$1\" in
24337 ˇ start)
24338 ˇ echo \"a\"
24339 ˇ ;;
24340 ˇ stop)
24341 ˇ echo \"b\"
24342 ˇ ;;
24343 ˇ *)
24344 ˇ echo \"c\"
24345 ˇ ;;
24346 ˇ esac
24347 ˇ}
24348 "});
24349 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24350 cx.assert_editor_state(indoc! {"
24351 function handle() {
24352 ˇcase \"$1\" in
24353 ˇstart)
24354 ˇecho \"a\"
24355 ˇ;;
24356 ˇstop)
24357 ˇecho \"b\"
24358 ˇ;;
24359 ˇ*)
24360 ˇecho \"c\"
24361 ˇ;;
24362 ˇesac
24363 ˇ}
24364 "});
24365}
24366
24367#[gpui::test]
24368async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
24369 init_test(cx, |_| {});
24370
24371 let mut cx = EditorTestContext::new(cx).await;
24372 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24373 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24374
24375 // test indents on comment insert
24376 cx.set_state(indoc! {"
24377 function main() {
24378 ˇ for item in $items; do
24379 ˇ while [ -n \"$item\" ]; do
24380 ˇ if [ \"$value\" -gt 10 ]; then
24381 ˇ continue
24382 ˇ elif [ \"$value\" -lt 0 ]; then
24383 ˇ break
24384 ˇ else
24385 ˇ echo \"$item\"
24386 ˇ fi
24387 ˇ done
24388 ˇ done
24389 ˇ}
24390 "});
24391 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
24392 cx.assert_editor_state(indoc! {"
24393 function main() {
24394 #ˇ for item in $items; do
24395 #ˇ while [ -n \"$item\" ]; do
24396 #ˇ if [ \"$value\" -gt 10 ]; then
24397 #ˇ continue
24398 #ˇ elif [ \"$value\" -lt 0 ]; then
24399 #ˇ break
24400 #ˇ else
24401 #ˇ echo \"$item\"
24402 #ˇ fi
24403 #ˇ done
24404 #ˇ done
24405 #ˇ}
24406 "});
24407}
24408
24409#[gpui::test]
24410async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
24411 init_test(cx, |_| {});
24412
24413 let mut cx = EditorTestContext::new(cx).await;
24414 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24415 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24416
24417 // test `else` auto outdents when typed inside `if` block
24418 cx.set_state(indoc! {"
24419 if [ \"$1\" = \"test\" ]; then
24420 echo \"foo bar\"
24421 ˇ
24422 "});
24423 cx.update_editor(|editor, window, cx| {
24424 editor.handle_input("else", window, cx);
24425 });
24426 cx.assert_editor_state(indoc! {"
24427 if [ \"$1\" = \"test\" ]; then
24428 echo \"foo bar\"
24429 elseˇ
24430 "});
24431
24432 // test `elif` auto outdents when typed inside `if` block
24433 cx.set_state(indoc! {"
24434 if [ \"$1\" = \"test\" ]; then
24435 echo \"foo bar\"
24436 ˇ
24437 "});
24438 cx.update_editor(|editor, window, cx| {
24439 editor.handle_input("elif", window, cx);
24440 });
24441 cx.assert_editor_state(indoc! {"
24442 if [ \"$1\" = \"test\" ]; then
24443 echo \"foo bar\"
24444 elifˇ
24445 "});
24446
24447 // test `fi` auto outdents when typed inside `else` block
24448 cx.set_state(indoc! {"
24449 if [ \"$1\" = \"test\" ]; then
24450 echo \"foo bar\"
24451 else
24452 echo \"bar baz\"
24453 ˇ
24454 "});
24455 cx.update_editor(|editor, window, cx| {
24456 editor.handle_input("fi", window, cx);
24457 });
24458 cx.assert_editor_state(indoc! {"
24459 if [ \"$1\" = \"test\" ]; then
24460 echo \"foo bar\"
24461 else
24462 echo \"bar baz\"
24463 fiˇ
24464 "});
24465
24466 // test `done` auto outdents when typed inside `while` block
24467 cx.set_state(indoc! {"
24468 while read line; do
24469 echo \"$line\"
24470 ˇ
24471 "});
24472 cx.update_editor(|editor, window, cx| {
24473 editor.handle_input("done", window, cx);
24474 });
24475 cx.assert_editor_state(indoc! {"
24476 while read line; do
24477 echo \"$line\"
24478 doneˇ
24479 "});
24480
24481 // test `done` auto outdents when typed inside `for` block
24482 cx.set_state(indoc! {"
24483 for file in *.txt; do
24484 cat \"$file\"
24485 ˇ
24486 "});
24487 cx.update_editor(|editor, window, cx| {
24488 editor.handle_input("done", window, cx);
24489 });
24490 cx.assert_editor_state(indoc! {"
24491 for file in *.txt; do
24492 cat \"$file\"
24493 doneˇ
24494 "});
24495
24496 // test `esac` auto outdents when typed inside `case` block
24497 cx.set_state(indoc! {"
24498 case \"$1\" in
24499 start)
24500 echo \"foo bar\"
24501 ;;
24502 stop)
24503 echo \"bar baz\"
24504 ;;
24505 ˇ
24506 "});
24507 cx.update_editor(|editor, window, cx| {
24508 editor.handle_input("esac", window, cx);
24509 });
24510 cx.assert_editor_state(indoc! {"
24511 case \"$1\" in
24512 start)
24513 echo \"foo bar\"
24514 ;;
24515 stop)
24516 echo \"bar baz\"
24517 ;;
24518 esacˇ
24519 "});
24520
24521 // test `*)` auto outdents when typed inside `case` block
24522 cx.set_state(indoc! {"
24523 case \"$1\" in
24524 start)
24525 echo \"foo bar\"
24526 ;;
24527 ˇ
24528 "});
24529 cx.update_editor(|editor, window, cx| {
24530 editor.handle_input("*)", window, cx);
24531 });
24532 cx.assert_editor_state(indoc! {"
24533 case \"$1\" in
24534 start)
24535 echo \"foo bar\"
24536 ;;
24537 *)ˇ
24538 "});
24539
24540 // test `fi` outdents to correct level with nested if blocks
24541 cx.set_state(indoc! {"
24542 if [ \"$1\" = \"test\" ]; then
24543 echo \"outer if\"
24544 if [ \"$2\" = \"debug\" ]; then
24545 echo \"inner if\"
24546 ˇ
24547 "});
24548 cx.update_editor(|editor, window, cx| {
24549 editor.handle_input("fi", window, cx);
24550 });
24551 cx.assert_editor_state(indoc! {"
24552 if [ \"$1\" = \"test\" ]; then
24553 echo \"outer if\"
24554 if [ \"$2\" = \"debug\" ]; then
24555 echo \"inner if\"
24556 fiˇ
24557 "});
24558}
24559
24560#[gpui::test]
24561async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
24562 init_test(cx, |_| {});
24563 update_test_language_settings(cx, |settings| {
24564 settings.defaults.extend_comment_on_newline = Some(false);
24565 });
24566 let mut cx = EditorTestContext::new(cx).await;
24567 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24568 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24569
24570 // test correct indent after newline on comment
24571 cx.set_state(indoc! {"
24572 # COMMENT:ˇ
24573 "});
24574 cx.update_editor(|editor, window, cx| {
24575 editor.newline(&Newline, window, cx);
24576 });
24577 cx.assert_editor_state(indoc! {"
24578 # COMMENT:
24579 ˇ
24580 "});
24581
24582 // test correct indent after newline after `then`
24583 cx.set_state(indoc! {"
24584
24585 if [ \"$1\" = \"test\" ]; thenˇ
24586 "});
24587 cx.update_editor(|editor, window, cx| {
24588 editor.newline(&Newline, window, cx);
24589 });
24590 cx.run_until_parked();
24591 cx.assert_editor_state(indoc! {"
24592
24593 if [ \"$1\" = \"test\" ]; then
24594 ˇ
24595 "});
24596
24597 // test correct indent after newline after `else`
24598 cx.set_state(indoc! {"
24599 if [ \"$1\" = \"test\" ]; then
24600 elseˇ
24601 "});
24602 cx.update_editor(|editor, window, cx| {
24603 editor.newline(&Newline, window, cx);
24604 });
24605 cx.run_until_parked();
24606 cx.assert_editor_state(indoc! {"
24607 if [ \"$1\" = \"test\" ]; then
24608 else
24609 ˇ
24610 "});
24611
24612 // test correct indent after newline after `elif`
24613 cx.set_state(indoc! {"
24614 if [ \"$1\" = \"test\" ]; then
24615 elifˇ
24616 "});
24617 cx.update_editor(|editor, window, cx| {
24618 editor.newline(&Newline, window, cx);
24619 });
24620 cx.run_until_parked();
24621 cx.assert_editor_state(indoc! {"
24622 if [ \"$1\" = \"test\" ]; then
24623 elif
24624 ˇ
24625 "});
24626
24627 // test correct indent after newline after `do`
24628 cx.set_state(indoc! {"
24629 for file in *.txt; doˇ
24630 "});
24631 cx.update_editor(|editor, window, cx| {
24632 editor.newline(&Newline, window, cx);
24633 });
24634 cx.run_until_parked();
24635 cx.assert_editor_state(indoc! {"
24636 for file in *.txt; do
24637 ˇ
24638 "});
24639
24640 // test correct indent after newline after case pattern
24641 cx.set_state(indoc! {"
24642 case \"$1\" in
24643 start)ˇ
24644 "});
24645 cx.update_editor(|editor, window, cx| {
24646 editor.newline(&Newline, window, cx);
24647 });
24648 cx.run_until_parked();
24649 cx.assert_editor_state(indoc! {"
24650 case \"$1\" in
24651 start)
24652 ˇ
24653 "});
24654
24655 // test correct indent after newline after case pattern
24656 cx.set_state(indoc! {"
24657 case \"$1\" in
24658 start)
24659 ;;
24660 *)ˇ
24661 "});
24662 cx.update_editor(|editor, window, cx| {
24663 editor.newline(&Newline, window, cx);
24664 });
24665 cx.run_until_parked();
24666 cx.assert_editor_state(indoc! {"
24667 case \"$1\" in
24668 start)
24669 ;;
24670 *)
24671 ˇ
24672 "});
24673
24674 // test correct indent after newline after function opening brace
24675 cx.set_state(indoc! {"
24676 function test() {ˇ}
24677 "});
24678 cx.update_editor(|editor, window, cx| {
24679 editor.newline(&Newline, window, cx);
24680 });
24681 cx.run_until_parked();
24682 cx.assert_editor_state(indoc! {"
24683 function test() {
24684 ˇ
24685 }
24686 "});
24687
24688 // test no extra indent after semicolon on same line
24689 cx.set_state(indoc! {"
24690 echo \"test\";ˇ
24691 "});
24692 cx.update_editor(|editor, window, cx| {
24693 editor.newline(&Newline, window, cx);
24694 });
24695 cx.run_until_parked();
24696 cx.assert_editor_state(indoc! {"
24697 echo \"test\";
24698 ˇ
24699 "});
24700}
24701
24702fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
24703 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
24704 point..point
24705}
24706
24707#[track_caller]
24708fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
24709 let (text, ranges) = marked_text_ranges(marked_text, true);
24710 assert_eq!(editor.text(cx), text);
24711 assert_eq!(
24712 editor.selections.ranges(cx),
24713 ranges,
24714 "Assert selections are {}",
24715 marked_text
24716 );
24717}
24718
24719pub fn handle_signature_help_request(
24720 cx: &mut EditorLspTestContext,
24721 mocked_response: lsp::SignatureHelp,
24722) -> impl Future<Output = ()> + use<> {
24723 let mut request =
24724 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
24725 let mocked_response = mocked_response.clone();
24726 async move { Ok(Some(mocked_response)) }
24727 });
24728
24729 async move {
24730 request.next().await;
24731 }
24732}
24733
24734#[track_caller]
24735pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
24736 cx.update_editor(|editor, _, _| {
24737 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
24738 let entries = menu.entries.borrow();
24739 let entries = entries
24740 .iter()
24741 .map(|entry| entry.string.as_str())
24742 .collect::<Vec<_>>();
24743 assert_eq!(entries, expected);
24744 } else {
24745 panic!("Expected completions menu");
24746 }
24747 });
24748}
24749
24750/// Handle completion request passing a marked string specifying where the completion
24751/// should be triggered from using '|' character, what range should be replaced, and what completions
24752/// should be returned using '<' and '>' to delimit the range.
24753///
24754/// Also see `handle_completion_request_with_insert_and_replace`.
24755#[track_caller]
24756pub fn handle_completion_request(
24757 marked_string: &str,
24758 completions: Vec<&'static str>,
24759 is_incomplete: bool,
24760 counter: Arc<AtomicUsize>,
24761 cx: &mut EditorLspTestContext,
24762) -> impl Future<Output = ()> {
24763 let complete_from_marker: TextRangeMarker = '|'.into();
24764 let replace_range_marker: TextRangeMarker = ('<', '>').into();
24765 let (_, mut marked_ranges) = marked_text_ranges_by(
24766 marked_string,
24767 vec![complete_from_marker.clone(), replace_range_marker.clone()],
24768 );
24769
24770 let complete_from_position =
24771 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
24772 let replace_range =
24773 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
24774
24775 let mut request =
24776 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
24777 let completions = completions.clone();
24778 counter.fetch_add(1, atomic::Ordering::Release);
24779 async move {
24780 assert_eq!(params.text_document_position.text_document.uri, url.clone());
24781 assert_eq!(
24782 params.text_document_position.position,
24783 complete_from_position
24784 );
24785 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
24786 is_incomplete,
24787 item_defaults: None,
24788 items: completions
24789 .iter()
24790 .map(|completion_text| lsp::CompletionItem {
24791 label: completion_text.to_string(),
24792 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
24793 range: replace_range,
24794 new_text: completion_text.to_string(),
24795 })),
24796 ..Default::default()
24797 })
24798 .collect(),
24799 })))
24800 }
24801 });
24802
24803 async move {
24804 request.next().await;
24805 }
24806}
24807
24808/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
24809/// given instead, which also contains an `insert` range.
24810///
24811/// This function uses markers to define ranges:
24812/// - `|` marks the cursor position
24813/// - `<>` marks the replace range
24814/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
24815pub fn handle_completion_request_with_insert_and_replace(
24816 cx: &mut EditorLspTestContext,
24817 marked_string: &str,
24818 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
24819 counter: Arc<AtomicUsize>,
24820) -> impl Future<Output = ()> {
24821 let complete_from_marker: TextRangeMarker = '|'.into();
24822 let replace_range_marker: TextRangeMarker = ('<', '>').into();
24823 let insert_range_marker: TextRangeMarker = ('{', '}').into();
24824
24825 let (_, mut marked_ranges) = marked_text_ranges_by(
24826 marked_string,
24827 vec![
24828 complete_from_marker.clone(),
24829 replace_range_marker.clone(),
24830 insert_range_marker.clone(),
24831 ],
24832 );
24833
24834 let complete_from_position =
24835 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
24836 let replace_range =
24837 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
24838
24839 let insert_range = match marked_ranges.remove(&insert_range_marker) {
24840 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
24841 _ => lsp::Range {
24842 start: replace_range.start,
24843 end: complete_from_position,
24844 },
24845 };
24846
24847 let mut request =
24848 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
24849 let completions = completions.clone();
24850 counter.fetch_add(1, atomic::Ordering::Release);
24851 async move {
24852 assert_eq!(params.text_document_position.text_document.uri, url.clone());
24853 assert_eq!(
24854 params.text_document_position.position, complete_from_position,
24855 "marker `|` position doesn't match",
24856 );
24857 Ok(Some(lsp::CompletionResponse::Array(
24858 completions
24859 .iter()
24860 .map(|(label, new_text)| lsp::CompletionItem {
24861 label: label.to_string(),
24862 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
24863 lsp::InsertReplaceEdit {
24864 insert: insert_range,
24865 replace: replace_range,
24866 new_text: new_text.to_string(),
24867 },
24868 )),
24869 ..Default::default()
24870 })
24871 .collect(),
24872 )))
24873 }
24874 });
24875
24876 async move {
24877 request.next().await;
24878 }
24879}
24880
24881fn handle_resolve_completion_request(
24882 cx: &mut EditorLspTestContext,
24883 edits: Option<Vec<(&'static str, &'static str)>>,
24884) -> impl Future<Output = ()> {
24885 let edits = edits.map(|edits| {
24886 edits
24887 .iter()
24888 .map(|(marked_string, new_text)| {
24889 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
24890 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
24891 lsp::TextEdit::new(replace_range, new_text.to_string())
24892 })
24893 .collect::<Vec<_>>()
24894 });
24895
24896 let mut request =
24897 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
24898 let edits = edits.clone();
24899 async move {
24900 Ok(lsp::CompletionItem {
24901 additional_text_edits: edits,
24902 ..Default::default()
24903 })
24904 }
24905 });
24906
24907 async move {
24908 request.next().await;
24909 }
24910}
24911
24912pub(crate) fn update_test_language_settings(
24913 cx: &mut TestAppContext,
24914 f: impl Fn(&mut AllLanguageSettingsContent),
24915) {
24916 cx.update(|cx| {
24917 SettingsStore::update_global(cx, |store, cx| {
24918 store.update_user_settings::<AllLanguageSettings>(cx, f);
24919 });
24920 });
24921}
24922
24923pub(crate) fn update_test_project_settings(
24924 cx: &mut TestAppContext,
24925 f: impl Fn(&mut ProjectSettings),
24926) {
24927 cx.update(|cx| {
24928 SettingsStore::update_global(cx, |store, cx| {
24929 store.update_user_settings::<ProjectSettings>(cx, f);
24930 });
24931 });
24932}
24933
24934pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
24935 cx.update(|cx| {
24936 assets::Assets.load_test_fonts(cx);
24937 let store = SettingsStore::test(cx);
24938 cx.set_global(store);
24939 theme::init(theme::LoadThemes::JustBase, cx);
24940 release_channel::init(SemanticVersion::default(), cx);
24941 client::init_settings(cx);
24942 language::init(cx);
24943 Project::init_settings(cx);
24944 workspace::init_settings(cx);
24945 crate::init(cx);
24946 });
24947 zlog::init_test();
24948 update_test_language_settings(cx, f);
24949}
24950
24951#[track_caller]
24952fn assert_hunk_revert(
24953 not_reverted_text_with_selections: &str,
24954 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
24955 expected_reverted_text_with_selections: &str,
24956 base_text: &str,
24957 cx: &mut EditorLspTestContext,
24958) {
24959 cx.set_state(not_reverted_text_with_selections);
24960 cx.set_head_text(base_text);
24961 cx.executor().run_until_parked();
24962
24963 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
24964 let snapshot = editor.snapshot(window, cx);
24965 let reverted_hunk_statuses = snapshot
24966 .buffer_snapshot
24967 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
24968 .map(|hunk| hunk.status().kind)
24969 .collect::<Vec<_>>();
24970
24971 editor.git_restore(&Default::default(), window, cx);
24972 reverted_hunk_statuses
24973 });
24974 cx.executor().run_until_parked();
24975 cx.assert_editor_state(expected_reverted_text_with_selections);
24976 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
24977}
24978
24979#[gpui::test(iterations = 10)]
24980async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
24981 init_test(cx, |_| {});
24982
24983 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
24984 let counter = diagnostic_requests.clone();
24985
24986 let fs = FakeFs::new(cx.executor());
24987 fs.insert_tree(
24988 path!("/a"),
24989 json!({
24990 "first.rs": "fn main() { let a = 5; }",
24991 "second.rs": "// Test file",
24992 }),
24993 )
24994 .await;
24995
24996 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24997 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24998 let cx = &mut VisualTestContext::from_window(*workspace, cx);
24999
25000 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25001 language_registry.add(rust_lang());
25002 let mut fake_servers = language_registry.register_fake_lsp(
25003 "Rust",
25004 FakeLspAdapter {
25005 capabilities: lsp::ServerCapabilities {
25006 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
25007 lsp::DiagnosticOptions {
25008 identifier: None,
25009 inter_file_dependencies: true,
25010 workspace_diagnostics: true,
25011 work_done_progress_options: Default::default(),
25012 },
25013 )),
25014 ..Default::default()
25015 },
25016 ..Default::default()
25017 },
25018 );
25019
25020 let editor = workspace
25021 .update(cx, |workspace, window, cx| {
25022 workspace.open_abs_path(
25023 PathBuf::from(path!("/a/first.rs")),
25024 OpenOptions::default(),
25025 window,
25026 cx,
25027 )
25028 })
25029 .unwrap()
25030 .await
25031 .unwrap()
25032 .downcast::<Editor>()
25033 .unwrap();
25034 let fake_server = fake_servers.next().await.unwrap();
25035 let server_id = fake_server.server.server_id();
25036 let mut first_request = fake_server
25037 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
25038 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
25039 let result_id = Some(new_result_id.to_string());
25040 assert_eq!(
25041 params.text_document.uri,
25042 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25043 );
25044 async move {
25045 Ok(lsp::DocumentDiagnosticReportResult::Report(
25046 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
25047 related_documents: None,
25048 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
25049 items: Vec::new(),
25050 result_id,
25051 },
25052 }),
25053 ))
25054 }
25055 });
25056
25057 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
25058 project.update(cx, |project, cx| {
25059 let buffer_id = editor
25060 .read(cx)
25061 .buffer()
25062 .read(cx)
25063 .as_singleton()
25064 .expect("created a singleton buffer")
25065 .read(cx)
25066 .remote_id();
25067 let buffer_result_id = project
25068 .lsp_store()
25069 .read(cx)
25070 .result_id(server_id, buffer_id, cx);
25071 assert_eq!(expected, buffer_result_id);
25072 });
25073 };
25074
25075 ensure_result_id(None, cx);
25076 cx.executor().advance_clock(Duration::from_millis(60));
25077 cx.executor().run_until_parked();
25078 assert_eq!(
25079 diagnostic_requests.load(atomic::Ordering::Acquire),
25080 1,
25081 "Opening file should trigger diagnostic request"
25082 );
25083 first_request
25084 .next()
25085 .await
25086 .expect("should have sent the first diagnostics pull request");
25087 ensure_result_id(Some("1".to_string()), cx);
25088
25089 // Editing should trigger diagnostics
25090 editor.update_in(cx, |editor, window, cx| {
25091 editor.handle_input("2", window, cx)
25092 });
25093 cx.executor().advance_clock(Duration::from_millis(60));
25094 cx.executor().run_until_parked();
25095 assert_eq!(
25096 diagnostic_requests.load(atomic::Ordering::Acquire),
25097 2,
25098 "Editing should trigger diagnostic request"
25099 );
25100 ensure_result_id(Some("2".to_string()), cx);
25101
25102 // Moving cursor should not trigger diagnostic request
25103 editor.update_in(cx, |editor, window, cx| {
25104 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25105 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
25106 });
25107 });
25108 cx.executor().advance_clock(Duration::from_millis(60));
25109 cx.executor().run_until_parked();
25110 assert_eq!(
25111 diagnostic_requests.load(atomic::Ordering::Acquire),
25112 2,
25113 "Cursor movement should not trigger diagnostic request"
25114 );
25115 ensure_result_id(Some("2".to_string()), cx);
25116 // Multiple rapid edits should be debounced
25117 for _ in 0..5 {
25118 editor.update_in(cx, |editor, window, cx| {
25119 editor.handle_input("x", window, cx)
25120 });
25121 }
25122 cx.executor().advance_clock(Duration::from_millis(60));
25123 cx.executor().run_until_parked();
25124
25125 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
25126 assert!(
25127 final_requests <= 4,
25128 "Multiple rapid edits should be debounced (got {final_requests} requests)",
25129 );
25130 ensure_result_id(Some(final_requests.to_string()), cx);
25131}
25132
25133#[gpui::test]
25134async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
25135 // Regression test for issue #11671
25136 // Previously, adding a cursor after moving multiple cursors would reset
25137 // the cursor count instead of adding to the existing cursors.
25138 init_test(cx, |_| {});
25139 let mut cx = EditorTestContext::new(cx).await;
25140
25141 // Create a simple buffer with cursor at start
25142 cx.set_state(indoc! {"
25143 ˇaaaa
25144 bbbb
25145 cccc
25146 dddd
25147 eeee
25148 ffff
25149 gggg
25150 hhhh"});
25151
25152 // Add 2 cursors below (so we have 3 total)
25153 cx.update_editor(|editor, window, cx| {
25154 editor.add_selection_below(&Default::default(), window, cx);
25155 editor.add_selection_below(&Default::default(), window, cx);
25156 });
25157
25158 // Verify we have 3 cursors
25159 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
25160 assert_eq!(
25161 initial_count, 3,
25162 "Should have 3 cursors after adding 2 below"
25163 );
25164
25165 // Move down one line
25166 cx.update_editor(|editor, window, cx| {
25167 editor.move_down(&MoveDown, window, cx);
25168 });
25169
25170 // Add another cursor below
25171 cx.update_editor(|editor, window, cx| {
25172 editor.add_selection_below(&Default::default(), window, cx);
25173 });
25174
25175 // Should now have 4 cursors (3 original + 1 new)
25176 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
25177 assert_eq!(
25178 final_count, 4,
25179 "Should have 4 cursors after moving and adding another"
25180 );
25181}
25182
25183#[gpui::test(iterations = 10)]
25184async fn test_document_colors(cx: &mut TestAppContext) {
25185 let expected_color = Rgba {
25186 r: 0.33,
25187 g: 0.33,
25188 b: 0.33,
25189 a: 0.33,
25190 };
25191
25192 init_test(cx, |_| {});
25193
25194 let fs = FakeFs::new(cx.executor());
25195 fs.insert_tree(
25196 path!("/a"),
25197 json!({
25198 "first.rs": "fn main() { let a = 5; }",
25199 }),
25200 )
25201 .await;
25202
25203 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25204 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25205 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25206
25207 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25208 language_registry.add(rust_lang());
25209 let mut fake_servers = language_registry.register_fake_lsp(
25210 "Rust",
25211 FakeLspAdapter {
25212 capabilities: lsp::ServerCapabilities {
25213 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
25214 ..lsp::ServerCapabilities::default()
25215 },
25216 name: "rust-analyzer",
25217 ..FakeLspAdapter::default()
25218 },
25219 );
25220 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
25221 "Rust",
25222 FakeLspAdapter {
25223 capabilities: lsp::ServerCapabilities {
25224 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
25225 ..lsp::ServerCapabilities::default()
25226 },
25227 name: "not-rust-analyzer",
25228 ..FakeLspAdapter::default()
25229 },
25230 );
25231
25232 let editor = workspace
25233 .update(cx, |workspace, window, cx| {
25234 workspace.open_abs_path(
25235 PathBuf::from(path!("/a/first.rs")),
25236 OpenOptions::default(),
25237 window,
25238 cx,
25239 )
25240 })
25241 .unwrap()
25242 .await
25243 .unwrap()
25244 .downcast::<Editor>()
25245 .unwrap();
25246 let fake_language_server = fake_servers.next().await.unwrap();
25247 let fake_language_server_without_capabilities =
25248 fake_servers_without_capabilities.next().await.unwrap();
25249 let requests_made = Arc::new(AtomicUsize::new(0));
25250 let closure_requests_made = Arc::clone(&requests_made);
25251 let mut color_request_handle = fake_language_server
25252 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
25253 let requests_made = Arc::clone(&closure_requests_made);
25254 async move {
25255 assert_eq!(
25256 params.text_document.uri,
25257 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25258 );
25259 requests_made.fetch_add(1, atomic::Ordering::Release);
25260 Ok(vec![
25261 lsp::ColorInformation {
25262 range: lsp::Range {
25263 start: lsp::Position {
25264 line: 0,
25265 character: 0,
25266 },
25267 end: lsp::Position {
25268 line: 0,
25269 character: 1,
25270 },
25271 },
25272 color: lsp::Color {
25273 red: 0.33,
25274 green: 0.33,
25275 blue: 0.33,
25276 alpha: 0.33,
25277 },
25278 },
25279 lsp::ColorInformation {
25280 range: lsp::Range {
25281 start: lsp::Position {
25282 line: 0,
25283 character: 0,
25284 },
25285 end: lsp::Position {
25286 line: 0,
25287 character: 1,
25288 },
25289 },
25290 color: lsp::Color {
25291 red: 0.33,
25292 green: 0.33,
25293 blue: 0.33,
25294 alpha: 0.33,
25295 },
25296 },
25297 ])
25298 }
25299 });
25300
25301 let _handle = fake_language_server_without_capabilities
25302 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
25303 panic!("Should not be called");
25304 });
25305 cx.executor().advance_clock(Duration::from_millis(100));
25306 color_request_handle.next().await.unwrap();
25307 cx.run_until_parked();
25308 assert_eq!(
25309 1,
25310 requests_made.load(atomic::Ordering::Acquire),
25311 "Should query for colors once per editor open"
25312 );
25313 editor.update_in(cx, |editor, _, cx| {
25314 assert_eq!(
25315 vec![expected_color],
25316 extract_color_inlays(editor, cx),
25317 "Should have an initial inlay"
25318 );
25319 });
25320
25321 // opening another file in a split should not influence the LSP query counter
25322 workspace
25323 .update(cx, |workspace, window, cx| {
25324 assert_eq!(
25325 workspace.panes().len(),
25326 1,
25327 "Should have one pane with one editor"
25328 );
25329 workspace.move_item_to_pane_in_direction(
25330 &MoveItemToPaneInDirection {
25331 direction: SplitDirection::Right,
25332 focus: false,
25333 clone: true,
25334 },
25335 window,
25336 cx,
25337 );
25338 })
25339 .unwrap();
25340 cx.run_until_parked();
25341 workspace
25342 .update(cx, |workspace, _, cx| {
25343 let panes = workspace.panes();
25344 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
25345 for pane in panes {
25346 let editor = pane
25347 .read(cx)
25348 .active_item()
25349 .and_then(|item| item.downcast::<Editor>())
25350 .expect("Should have opened an editor in each split");
25351 let editor_file = editor
25352 .read(cx)
25353 .buffer()
25354 .read(cx)
25355 .as_singleton()
25356 .expect("test deals with singleton buffers")
25357 .read(cx)
25358 .file()
25359 .expect("test buffese should have a file")
25360 .path();
25361 assert_eq!(
25362 editor_file.as_ref(),
25363 Path::new("first.rs"),
25364 "Both editors should be opened for the same file"
25365 )
25366 }
25367 })
25368 .unwrap();
25369
25370 cx.executor().advance_clock(Duration::from_millis(500));
25371 let save = editor.update_in(cx, |editor, window, cx| {
25372 editor.move_to_end(&MoveToEnd, window, cx);
25373 editor.handle_input("dirty", window, cx);
25374 editor.save(
25375 SaveOptions {
25376 format: true,
25377 autosave: true,
25378 },
25379 project.clone(),
25380 window,
25381 cx,
25382 )
25383 });
25384 save.await.unwrap();
25385
25386 color_request_handle.next().await.unwrap();
25387 cx.run_until_parked();
25388 assert_eq!(
25389 3,
25390 requests_made.load(atomic::Ordering::Acquire),
25391 "Should query for colors once per save and once per formatting after save"
25392 );
25393
25394 drop(editor);
25395 let close = workspace
25396 .update(cx, |workspace, window, cx| {
25397 workspace.active_pane().update(cx, |pane, cx| {
25398 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25399 })
25400 })
25401 .unwrap();
25402 close.await.unwrap();
25403 let close = workspace
25404 .update(cx, |workspace, window, cx| {
25405 workspace.active_pane().update(cx, |pane, cx| {
25406 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25407 })
25408 })
25409 .unwrap();
25410 close.await.unwrap();
25411 assert_eq!(
25412 3,
25413 requests_made.load(atomic::Ordering::Acquire),
25414 "After saving and closing all editors, no extra requests should be made"
25415 );
25416 workspace
25417 .update(cx, |workspace, _, cx| {
25418 assert!(
25419 workspace.active_item(cx).is_none(),
25420 "Should close all editors"
25421 )
25422 })
25423 .unwrap();
25424
25425 workspace
25426 .update(cx, |workspace, window, cx| {
25427 workspace.active_pane().update(cx, |pane, cx| {
25428 pane.navigate_backward(&Default::default(), window, cx);
25429 })
25430 })
25431 .unwrap();
25432 cx.executor().advance_clock(Duration::from_millis(100));
25433 cx.run_until_parked();
25434 let editor = workspace
25435 .update(cx, |workspace, _, cx| {
25436 workspace
25437 .active_item(cx)
25438 .expect("Should have reopened the editor again after navigating back")
25439 .downcast::<Editor>()
25440 .expect("Should be an editor")
25441 })
25442 .unwrap();
25443 color_request_handle.next().await.unwrap();
25444 assert_eq!(
25445 3,
25446 requests_made.load(atomic::Ordering::Acquire),
25447 "Cache should be reused on buffer close and reopen"
25448 );
25449 editor.update(cx, |editor, cx| {
25450 assert_eq!(
25451 vec![expected_color],
25452 extract_color_inlays(editor, cx),
25453 "Should have an initial inlay"
25454 );
25455 });
25456}
25457
25458#[gpui::test]
25459async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
25460 init_test(cx, |_| {});
25461 let (editor, cx) = cx.add_window_view(Editor::single_line);
25462 editor.update_in(cx, |editor, window, cx| {
25463 editor.set_text("oops\n\nwow\n", window, cx)
25464 });
25465 cx.run_until_parked();
25466 editor.update(cx, |editor, cx| {
25467 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
25468 });
25469 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
25470 cx.run_until_parked();
25471 editor.update(cx, |editor, cx| {
25472 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
25473 });
25474}
25475
25476#[gpui::test]
25477async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
25478 init_test(cx, |_| {});
25479
25480 cx.update(|cx| {
25481 register_project_item::<Editor>(cx);
25482 });
25483
25484 let fs = FakeFs::new(cx.executor());
25485 fs.insert_tree("/root1", json!({})).await;
25486 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
25487 .await;
25488
25489 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
25490 let (workspace, cx) =
25491 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
25492
25493 let worktree_id = project.update(cx, |project, cx| {
25494 project.worktrees(cx).next().unwrap().read(cx).id()
25495 });
25496
25497 let handle = workspace
25498 .update_in(cx, |workspace, window, cx| {
25499 let project_path = (worktree_id, "one.pdf");
25500 workspace.open_path(project_path, None, true, window, cx)
25501 })
25502 .await
25503 .unwrap();
25504
25505 assert_eq!(
25506 handle.to_any().entity_type(),
25507 TypeId::of::<InvalidBufferView>()
25508 );
25509}
25510
25511#[gpui::test]
25512async fn test_select_next_prev_syntax_node(cx: &mut TestAppContext) {
25513 init_test(cx, |_| {});
25514
25515 let language = Arc::new(Language::new(
25516 LanguageConfig::default(),
25517 Some(tree_sitter_rust::LANGUAGE.into()),
25518 ));
25519
25520 // Test hierarchical sibling navigation
25521 let text = r#"
25522 fn outer() {
25523 if condition {
25524 let a = 1;
25525 }
25526 let b = 2;
25527 }
25528
25529 fn another() {
25530 let c = 3;
25531 }
25532 "#;
25533
25534 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
25535 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
25536 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
25537
25538 // Wait for parsing to complete
25539 editor
25540 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
25541 .await;
25542
25543 editor.update_in(cx, |editor, window, cx| {
25544 // Start by selecting "let a = 1;" inside the if block
25545 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25546 s.select_display_ranges([
25547 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 26)
25548 ]);
25549 });
25550
25551 let initial_selection = editor.selections.display_ranges(cx);
25552 assert_eq!(initial_selection.len(), 1, "Should have one selection");
25553
25554 // Test select next sibling - should move up levels to find the next sibling
25555 // Since "let a = 1;" has no siblings in the if block, it should move up
25556 // to find "let b = 2;" which is a sibling of the if block
25557 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
25558 let next_selection = editor.selections.display_ranges(cx);
25559
25560 // Should have a selection and it should be different from the initial
25561 assert_eq!(
25562 next_selection.len(),
25563 1,
25564 "Should have one selection after next"
25565 );
25566 assert_ne!(
25567 next_selection[0], initial_selection[0],
25568 "Next sibling selection should be different"
25569 );
25570
25571 // Test hierarchical navigation by going to the end of the current function
25572 // and trying to navigate to the next function
25573 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25574 s.select_display_ranges([
25575 DisplayPoint::new(DisplayRow(5), 12)..DisplayPoint::new(DisplayRow(5), 22)
25576 ]);
25577 });
25578
25579 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
25580 let function_next_selection = editor.selections.display_ranges(cx);
25581
25582 // Should move to the next function
25583 assert_eq!(
25584 function_next_selection.len(),
25585 1,
25586 "Should have one selection after function next"
25587 );
25588
25589 // Test select previous sibling navigation
25590 editor.select_prev_syntax_node(&SelectPreviousSyntaxNode, window, cx);
25591 let prev_selection = editor.selections.display_ranges(cx);
25592
25593 // Should have a selection and it should be different
25594 assert_eq!(
25595 prev_selection.len(),
25596 1,
25597 "Should have one selection after prev"
25598 );
25599 assert_ne!(
25600 prev_selection[0], function_next_selection[0],
25601 "Previous sibling selection should be different from next"
25602 );
25603 });
25604}
25605
25606#[gpui::test]
25607async fn test_next_prev_document_highlight(cx: &mut TestAppContext) {
25608 init_test(cx, |_| {});
25609
25610 let mut cx = EditorTestContext::new(cx).await;
25611 cx.set_state(
25612 "let ˇvariable = 42;
25613let another = variable + 1;
25614let result = variable * 2;",
25615 );
25616
25617 // Set up document highlights manually (simulating LSP response)
25618 cx.update_editor(|editor, _window, cx| {
25619 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
25620
25621 // Create highlights for "variable" occurrences
25622 let highlight_ranges = [
25623 Point::new(0, 4)..Point::new(0, 12), // First "variable"
25624 Point::new(1, 14)..Point::new(1, 22), // Second "variable"
25625 Point::new(2, 13)..Point::new(2, 21), // Third "variable"
25626 ];
25627
25628 let anchor_ranges: Vec<_> = highlight_ranges
25629 .iter()
25630 .map(|range| range.clone().to_anchors(&buffer_snapshot))
25631 .collect();
25632
25633 editor.highlight_background::<DocumentHighlightRead>(
25634 &anchor_ranges,
25635 |theme| theme.colors().editor_document_highlight_read_background,
25636 cx,
25637 );
25638 });
25639
25640 // Go to next highlight - should move to second "variable"
25641 cx.update_editor(|editor, window, cx| {
25642 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
25643 });
25644 cx.assert_editor_state(
25645 "let variable = 42;
25646let another = ˇvariable + 1;
25647let result = variable * 2;",
25648 );
25649
25650 // Go to next highlight - should move to third "variable"
25651 cx.update_editor(|editor, window, cx| {
25652 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
25653 });
25654 cx.assert_editor_state(
25655 "let variable = 42;
25656let another = variable + 1;
25657let result = ˇvariable * 2;",
25658 );
25659
25660 // Go to next highlight - should stay at third "variable" (no wrap-around)
25661 cx.update_editor(|editor, window, cx| {
25662 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
25663 });
25664 cx.assert_editor_state(
25665 "let variable = 42;
25666let another = variable + 1;
25667let result = ˇvariable * 2;",
25668 );
25669
25670 // Now test going backwards from third position
25671 cx.update_editor(|editor, window, cx| {
25672 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
25673 });
25674 cx.assert_editor_state(
25675 "let variable = 42;
25676let another = ˇvariable + 1;
25677let result = variable * 2;",
25678 );
25679
25680 // Go to previous highlight - should move to first "variable"
25681 cx.update_editor(|editor, window, cx| {
25682 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
25683 });
25684 cx.assert_editor_state(
25685 "let ˇvariable = 42;
25686let another = variable + 1;
25687let result = variable * 2;",
25688 );
25689
25690 // Go to previous highlight - should stay on first "variable"
25691 cx.update_editor(|editor, window, cx| {
25692 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
25693 });
25694 cx.assert_editor_state(
25695 "let ˇvariable = 42;
25696let another = variable + 1;
25697let result = variable * 2;",
25698 );
25699}
25700
25701#[track_caller]
25702fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
25703 editor
25704 .all_inlays(cx)
25705 .into_iter()
25706 .filter_map(|inlay| inlay.get_color())
25707 .map(Rgba::from)
25708 .collect()
25709}