1use super::*;
2use crate::{
3 JoinLines,
4 code_context_menus::CodeContextMenu,
5 edit_prediction_tests::FakeEditPredictionProvider,
6 linked_editing_ranges::LinkedEditingRanges,
7 scroll::scroll_amount::ScrollAmount,
8 test::{
9 assert_text_with_selections, build_editor,
10 editor_lsp_test_context::{EditorLspTestContext, git_commit_lang},
11 editor_test_context::EditorTestContext,
12 select_ranges,
13 },
14};
15use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
16use collections::HashMap;
17use futures::{StreamExt, channel::oneshot};
18use gpui::{
19 BackgroundExecutor, DismissEvent, Rgba, SemanticVersion, TestAppContext, UpdateGlobal,
20 VisualTestContext, WindowBounds, WindowOptions, div,
21};
22use indoc::indoc;
23use language::{
24 BracketPairConfig,
25 Capability::ReadWrite,
26 DiagnosticSourceKind, FakeLspAdapter, IndentGuideSettings, LanguageConfig,
27 LanguageConfigOverride, LanguageMatcher, LanguageName, Override, Point,
28 language_settings::{
29 CompletionSettingsContent, FormatterList, LanguageSettingsContent, LspInsertMode,
30 SelectedFormatter,
31 },
32 tree_sitter_python,
33};
34use language_settings::Formatter;
35use lsp::CompletionParams;
36use multi_buffer::{IndentGuide, PathKey};
37use parking_lot::Mutex;
38use pretty_assertions::{assert_eq, assert_ne};
39use project::{
40 FakeFs,
41 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
42 project_settings::LspSettings,
43};
44use serde_json::{self, json};
45use settings::{
46 AllLanguageSettingsContent, IndentGuideBackgroundColoring, IndentGuideColoring,
47 ProjectSettingsContent,
48};
49use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
50use std::{
51 iter,
52 sync::atomic::{self, AtomicUsize},
53};
54use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
55use text::ToPoint as _;
56use unindent::Unindent;
57use util::{
58 assert_set_eq, path,
59 rel_path::rel_path,
60 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
61 uri,
62};
63use workspace::{
64 CloseActiveItem, CloseAllItems, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry,
65 OpenOptions, ViewId,
66 invalid_buffer_view::InvalidBufferView,
67 item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
68 register_project_item,
69};
70
71#[gpui::test]
72fn test_edit_events(cx: &mut TestAppContext) {
73 init_test(cx, |_| {});
74
75 let buffer = cx.new(|cx| {
76 let mut buffer = language::Buffer::local("123456", cx);
77 buffer.set_group_interval(Duration::from_secs(1));
78 buffer
79 });
80
81 let events = Rc::new(RefCell::new(Vec::new()));
82 let editor1 = cx.add_window({
83 let events = events.clone();
84 |window, cx| {
85 let entity = cx.entity();
86 cx.subscribe_in(
87 &entity,
88 window,
89 move |_, _, event: &EditorEvent, _, _| match event {
90 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
91 EditorEvent::BufferEdited => {
92 events.borrow_mut().push(("editor1", "buffer edited"))
93 }
94 _ => {}
95 },
96 )
97 .detach();
98 Editor::for_buffer(buffer.clone(), None, window, cx)
99 }
100 });
101
102 let editor2 = cx.add_window({
103 let events = events.clone();
104 |window, cx| {
105 cx.subscribe_in(
106 &cx.entity(),
107 window,
108 move |_, _, event: &EditorEvent, _, _| match event {
109 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
110 EditorEvent::BufferEdited => {
111 events.borrow_mut().push(("editor2", "buffer edited"))
112 }
113 _ => {}
114 },
115 )
116 .detach();
117 Editor::for_buffer(buffer.clone(), None, window, cx)
118 }
119 });
120
121 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
122
123 // Mutating editor 1 will emit an `Edited` event only for that editor.
124 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
125 assert_eq!(
126 mem::take(&mut *events.borrow_mut()),
127 [
128 ("editor1", "edited"),
129 ("editor1", "buffer edited"),
130 ("editor2", "buffer edited"),
131 ]
132 );
133
134 // Mutating editor 2 will emit an `Edited` event only for that editor.
135 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
136 assert_eq!(
137 mem::take(&mut *events.borrow_mut()),
138 [
139 ("editor2", "edited"),
140 ("editor1", "buffer edited"),
141 ("editor2", "buffer edited"),
142 ]
143 );
144
145 // Undoing on editor 1 will emit an `Edited` event only for that editor.
146 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
147 assert_eq!(
148 mem::take(&mut *events.borrow_mut()),
149 [
150 ("editor1", "edited"),
151 ("editor1", "buffer edited"),
152 ("editor2", "buffer edited"),
153 ]
154 );
155
156 // Redoing on editor 1 will emit an `Edited` event only for that editor.
157 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
158 assert_eq!(
159 mem::take(&mut *events.borrow_mut()),
160 [
161 ("editor1", "edited"),
162 ("editor1", "buffer edited"),
163 ("editor2", "buffer edited"),
164 ]
165 );
166
167 // Undoing on editor 2 will emit an `Edited` event only for that editor.
168 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
169 assert_eq!(
170 mem::take(&mut *events.borrow_mut()),
171 [
172 ("editor2", "edited"),
173 ("editor1", "buffer edited"),
174 ("editor2", "buffer edited"),
175 ]
176 );
177
178 // Redoing on editor 2 will emit an `Edited` event only for that editor.
179 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
180 assert_eq!(
181 mem::take(&mut *events.borrow_mut()),
182 [
183 ("editor2", "edited"),
184 ("editor1", "buffer edited"),
185 ("editor2", "buffer edited"),
186 ]
187 );
188
189 // No event is emitted when the mutation is a no-op.
190 _ = editor2.update(cx, |editor, window, cx| {
191 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
192 s.select_ranges([0..0])
193 });
194
195 editor.backspace(&Backspace, window, cx);
196 });
197 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
198}
199
200#[gpui::test]
201fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
202 init_test(cx, |_| {});
203
204 let mut now = Instant::now();
205 let group_interval = Duration::from_millis(1);
206 let buffer = cx.new(|cx| {
207 let mut buf = language::Buffer::local("123456", cx);
208 buf.set_group_interval(group_interval);
209 buf
210 });
211 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
212 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
213
214 _ = editor.update(cx, |editor, window, cx| {
215 editor.start_transaction_at(now, window, cx);
216 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
217 s.select_ranges([2..4])
218 });
219
220 editor.insert("cd", window, cx);
221 editor.end_transaction_at(now, cx);
222 assert_eq!(editor.text(cx), "12cd56");
223 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
224
225 editor.start_transaction_at(now, window, cx);
226 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
227 s.select_ranges([4..5])
228 });
229 editor.insert("e", window, cx);
230 editor.end_transaction_at(now, cx);
231 assert_eq!(editor.text(cx), "12cde6");
232 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
233
234 now += group_interval + Duration::from_millis(1);
235 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
236 s.select_ranges([2..2])
237 });
238
239 // Simulate an edit in another editor
240 buffer.update(cx, |buffer, cx| {
241 buffer.start_transaction_at(now, cx);
242 buffer.edit([(0..1, "a")], None, cx);
243 buffer.edit([(1..1, "b")], None, cx);
244 buffer.end_transaction_at(now, cx);
245 });
246
247 assert_eq!(editor.text(cx), "ab2cde6");
248 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
249
250 // Last transaction happened past the group interval in a different editor.
251 // Undo it individually and don't restore selections.
252 editor.undo(&Undo, window, cx);
253 assert_eq!(editor.text(cx), "12cde6");
254 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
255
256 // First two transactions happened within the group interval in this editor.
257 // Undo them together and restore selections.
258 editor.undo(&Undo, window, cx);
259 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
260 assert_eq!(editor.text(cx), "123456");
261 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
262
263 // Redo the first two transactions together.
264 editor.redo(&Redo, window, cx);
265 assert_eq!(editor.text(cx), "12cde6");
266 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
267
268 // Redo the last transaction on its own.
269 editor.redo(&Redo, window, cx);
270 assert_eq!(editor.text(cx), "ab2cde6");
271 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
272
273 // Test empty transactions.
274 editor.start_transaction_at(now, window, cx);
275 editor.end_transaction_at(now, cx);
276 editor.undo(&Undo, window, cx);
277 assert_eq!(editor.text(cx), "12cde6");
278 });
279}
280
281#[gpui::test]
282fn test_ime_composition(cx: &mut TestAppContext) {
283 init_test(cx, |_| {});
284
285 let buffer = cx.new(|cx| {
286 let mut buffer = language::Buffer::local("abcde", cx);
287 // Ensure automatic grouping doesn't occur.
288 buffer.set_group_interval(Duration::ZERO);
289 buffer
290 });
291
292 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
293 cx.add_window(|window, cx| {
294 let mut editor = build_editor(buffer.clone(), window, cx);
295
296 // Start a new IME composition.
297 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
298 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
299 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
300 assert_eq!(editor.text(cx), "äbcde");
301 assert_eq!(
302 editor.marked_text_ranges(cx),
303 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
304 );
305
306 // Finalize IME composition.
307 editor.replace_text_in_range(None, "ā", window, cx);
308 assert_eq!(editor.text(cx), "ābcde");
309 assert_eq!(editor.marked_text_ranges(cx), None);
310
311 // IME composition edits are grouped and are undone/redone at once.
312 editor.undo(&Default::default(), window, cx);
313 assert_eq!(editor.text(cx), "abcde");
314 assert_eq!(editor.marked_text_ranges(cx), None);
315 editor.redo(&Default::default(), window, cx);
316 assert_eq!(editor.text(cx), "ābcde");
317 assert_eq!(editor.marked_text_ranges(cx), None);
318
319 // Start a new IME composition.
320 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
321 assert_eq!(
322 editor.marked_text_ranges(cx),
323 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
324 );
325
326 // Undoing during an IME composition cancels it.
327 editor.undo(&Default::default(), window, cx);
328 assert_eq!(editor.text(cx), "ābcde");
329 assert_eq!(editor.marked_text_ranges(cx), None);
330
331 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
332 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
333 assert_eq!(editor.text(cx), "ābcdè");
334 assert_eq!(
335 editor.marked_text_ranges(cx),
336 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
337 );
338
339 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
340 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
341 assert_eq!(editor.text(cx), "ābcdę");
342 assert_eq!(editor.marked_text_ranges(cx), None);
343
344 // Start a new IME composition with multiple cursors.
345 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
346 s.select_ranges([
347 OffsetUtf16(1)..OffsetUtf16(1),
348 OffsetUtf16(3)..OffsetUtf16(3),
349 OffsetUtf16(5)..OffsetUtf16(5),
350 ])
351 });
352 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
353 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
354 assert_eq!(
355 editor.marked_text_ranges(cx),
356 Some(vec![
357 OffsetUtf16(0)..OffsetUtf16(3),
358 OffsetUtf16(4)..OffsetUtf16(7),
359 OffsetUtf16(8)..OffsetUtf16(11)
360 ])
361 );
362
363 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
364 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
365 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
366 assert_eq!(
367 editor.marked_text_ranges(cx),
368 Some(vec![
369 OffsetUtf16(1)..OffsetUtf16(2),
370 OffsetUtf16(5)..OffsetUtf16(6),
371 OffsetUtf16(9)..OffsetUtf16(10)
372 ])
373 );
374
375 // Finalize IME composition with multiple cursors.
376 editor.replace_text_in_range(Some(9..10), "2", window, cx);
377 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
378 assert_eq!(editor.marked_text_ranges(cx), None);
379
380 editor
381 });
382}
383
384#[gpui::test]
385fn test_selection_with_mouse(cx: &mut TestAppContext) {
386 init_test(cx, |_| {});
387
388 let editor = cx.add_window(|window, cx| {
389 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
390 build_editor(buffer, window, cx)
391 });
392
393 _ = editor.update(cx, |editor, window, cx| {
394 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
395 });
396 assert_eq!(
397 editor
398 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
399 .unwrap(),
400 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
401 );
402
403 _ = editor.update(cx, |editor, window, cx| {
404 editor.update_selection(
405 DisplayPoint::new(DisplayRow(3), 3),
406 0,
407 gpui::Point::<f32>::default(),
408 window,
409 cx,
410 );
411 });
412
413 assert_eq!(
414 editor
415 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
416 .unwrap(),
417 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
418 );
419
420 _ = editor.update(cx, |editor, window, cx| {
421 editor.update_selection(
422 DisplayPoint::new(DisplayRow(1), 1),
423 0,
424 gpui::Point::<f32>::default(),
425 window,
426 cx,
427 );
428 });
429
430 assert_eq!(
431 editor
432 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
433 .unwrap(),
434 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
435 );
436
437 _ = editor.update(cx, |editor, window, cx| {
438 editor.end_selection(window, cx);
439 editor.update_selection(
440 DisplayPoint::new(DisplayRow(3), 3),
441 0,
442 gpui::Point::<f32>::default(),
443 window,
444 cx,
445 );
446 });
447
448 assert_eq!(
449 editor
450 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
451 .unwrap(),
452 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
453 );
454
455 _ = editor.update(cx, |editor, window, cx| {
456 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
457 editor.update_selection(
458 DisplayPoint::new(DisplayRow(0), 0),
459 0,
460 gpui::Point::<f32>::default(),
461 window,
462 cx,
463 );
464 });
465
466 assert_eq!(
467 editor
468 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
469 .unwrap(),
470 [
471 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
472 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
473 ]
474 );
475
476 _ = editor.update(cx, |editor, window, cx| {
477 editor.end_selection(window, cx);
478 });
479
480 assert_eq!(
481 editor
482 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
483 .unwrap(),
484 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
485 );
486}
487
488#[gpui::test]
489fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
490 init_test(cx, |_| {});
491
492 let editor = cx.add_window(|window, cx| {
493 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
494 build_editor(buffer, window, cx)
495 });
496
497 _ = editor.update(cx, |editor, window, cx| {
498 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
499 });
500
501 _ = editor.update(cx, |editor, window, cx| {
502 editor.end_selection(window, cx);
503 });
504
505 _ = editor.update(cx, |editor, window, cx| {
506 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
507 });
508
509 _ = editor.update(cx, |editor, window, cx| {
510 editor.end_selection(window, cx);
511 });
512
513 assert_eq!(
514 editor
515 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
516 .unwrap(),
517 [
518 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
519 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
520 ]
521 );
522
523 _ = editor.update(cx, |editor, window, cx| {
524 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
525 });
526
527 _ = editor.update(cx, |editor, window, cx| {
528 editor.end_selection(window, cx);
529 });
530
531 assert_eq!(
532 editor
533 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
534 .unwrap(),
535 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
536 );
537}
538
539#[gpui::test]
540fn test_canceling_pending_selection(cx: &mut TestAppContext) {
541 init_test(cx, |_| {});
542
543 let editor = cx.add_window(|window, cx| {
544 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
545 build_editor(buffer, window, cx)
546 });
547
548 _ = editor.update(cx, |editor, window, cx| {
549 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
550 assert_eq!(
551 editor.selections.display_ranges(cx),
552 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
553 );
554 });
555
556 _ = editor.update(cx, |editor, window, cx| {
557 editor.update_selection(
558 DisplayPoint::new(DisplayRow(3), 3),
559 0,
560 gpui::Point::<f32>::default(),
561 window,
562 cx,
563 );
564 assert_eq!(
565 editor.selections.display_ranges(cx),
566 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
567 );
568 });
569
570 _ = editor.update(cx, |editor, window, cx| {
571 editor.cancel(&Cancel, window, cx);
572 editor.update_selection(
573 DisplayPoint::new(DisplayRow(1), 1),
574 0,
575 gpui::Point::<f32>::default(),
576 window,
577 cx,
578 );
579 assert_eq!(
580 editor.selections.display_ranges(cx),
581 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
582 );
583 });
584}
585
586#[gpui::test]
587fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
588 init_test(cx, |_| {});
589
590 let editor = cx.add_window(|window, cx| {
591 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
592 build_editor(buffer, window, cx)
593 });
594
595 _ = editor.update(cx, |editor, window, cx| {
596 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
597 assert_eq!(
598 editor.selections.display_ranges(cx),
599 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
600 );
601
602 editor.move_down(&Default::default(), window, cx);
603 assert_eq!(
604 editor.selections.display_ranges(cx),
605 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
606 );
607
608 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
609 assert_eq!(
610 editor.selections.display_ranges(cx),
611 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
612 );
613
614 editor.move_up(&Default::default(), window, cx);
615 assert_eq!(
616 editor.selections.display_ranges(cx),
617 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
618 );
619 });
620}
621
622#[gpui::test]
623fn test_clone(cx: &mut TestAppContext) {
624 init_test(cx, |_| {});
625
626 let (text, selection_ranges) = marked_text_ranges(
627 indoc! {"
628 one
629 two
630 threeˇ
631 four
632 fiveˇ
633 "},
634 true,
635 );
636
637 let editor = cx.add_window(|window, cx| {
638 let buffer = MultiBuffer::build_simple(&text, cx);
639 build_editor(buffer, window, cx)
640 });
641
642 _ = editor.update(cx, |editor, window, cx| {
643 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
644 s.select_ranges(selection_ranges.clone())
645 });
646 editor.fold_creases(
647 vec![
648 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
649 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
650 ],
651 true,
652 window,
653 cx,
654 );
655 });
656
657 let cloned_editor = editor
658 .update(cx, |editor, _, cx| {
659 cx.open_window(Default::default(), |window, cx| {
660 cx.new(|cx| editor.clone(window, cx))
661 })
662 })
663 .unwrap()
664 .unwrap();
665
666 let snapshot = editor
667 .update(cx, |e, window, cx| e.snapshot(window, cx))
668 .unwrap();
669 let cloned_snapshot = cloned_editor
670 .update(cx, |e, window, cx| e.snapshot(window, cx))
671 .unwrap();
672
673 assert_eq!(
674 cloned_editor
675 .update(cx, |e, _, cx| e.display_text(cx))
676 .unwrap(),
677 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
678 );
679 assert_eq!(
680 cloned_snapshot
681 .folds_in_range(0..text.len())
682 .collect::<Vec<_>>(),
683 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
684 );
685 assert_set_eq!(
686 cloned_editor
687 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
688 .unwrap(),
689 editor
690 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
691 .unwrap()
692 );
693 assert_set_eq!(
694 cloned_editor
695 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
696 .unwrap(),
697 editor
698 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
699 .unwrap()
700 );
701}
702
703#[gpui::test]
704async fn test_navigation_history(cx: &mut TestAppContext) {
705 init_test(cx, |_| {});
706
707 use workspace::item::Item;
708
709 let fs = FakeFs::new(cx.executor());
710 let project = Project::test(fs, [], cx).await;
711 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
712 let pane = workspace
713 .update(cx, |workspace, _, _| workspace.active_pane().clone())
714 .unwrap();
715
716 _ = workspace.update(cx, |_v, window, cx| {
717 cx.new(|cx| {
718 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
719 let mut editor = build_editor(buffer, window, cx);
720 let handle = cx.entity();
721 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
722
723 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
724 editor.nav_history.as_mut().unwrap().pop_backward(cx)
725 }
726
727 // Move the cursor a small distance.
728 // Nothing is added to the navigation history.
729 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
730 s.select_display_ranges([
731 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
732 ])
733 });
734 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
735 s.select_display_ranges([
736 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
737 ])
738 });
739 assert!(pop_history(&mut editor, cx).is_none());
740
741 // Move the cursor a large distance.
742 // The history can jump back to the previous position.
743 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
744 s.select_display_ranges([
745 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
746 ])
747 });
748 let nav_entry = pop_history(&mut editor, cx).unwrap();
749 editor.navigate(nav_entry.data.unwrap(), window, cx);
750 assert_eq!(nav_entry.item.id(), cx.entity_id());
751 assert_eq!(
752 editor.selections.display_ranges(cx),
753 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
754 );
755 assert!(pop_history(&mut editor, cx).is_none());
756
757 // Move the cursor a small distance via the mouse.
758 // Nothing is added to the navigation history.
759 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
760 editor.end_selection(window, cx);
761 assert_eq!(
762 editor.selections.display_ranges(cx),
763 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
764 );
765 assert!(pop_history(&mut editor, cx).is_none());
766
767 // Move the cursor a large distance via the mouse.
768 // The history can jump back to the previous position.
769 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
770 editor.end_selection(window, cx);
771 assert_eq!(
772 editor.selections.display_ranges(cx),
773 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
774 );
775 let nav_entry = pop_history(&mut editor, cx).unwrap();
776 editor.navigate(nav_entry.data.unwrap(), window, cx);
777 assert_eq!(nav_entry.item.id(), cx.entity_id());
778 assert_eq!(
779 editor.selections.display_ranges(cx),
780 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
781 );
782 assert!(pop_history(&mut editor, cx).is_none());
783
784 // Set scroll position to check later
785 editor.set_scroll_position(gpui::Point::<f64>::new(5.5, 5.5), window, cx);
786 let original_scroll_position = editor.scroll_manager.anchor();
787
788 // Jump to the end of the document and adjust scroll
789 editor.move_to_end(&MoveToEnd, window, cx);
790 editor.set_scroll_position(gpui::Point::<f64>::new(-2.5, -0.5), window, cx);
791 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
792
793 let nav_entry = pop_history(&mut editor, cx).unwrap();
794 editor.navigate(nav_entry.data.unwrap(), window, cx);
795 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
796
797 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
798 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
799 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
800 let invalid_point = Point::new(9999, 0);
801 editor.navigate(
802 Box::new(NavigationData {
803 cursor_anchor: invalid_anchor,
804 cursor_position: invalid_point,
805 scroll_anchor: ScrollAnchor {
806 anchor: invalid_anchor,
807 offset: Default::default(),
808 },
809 scroll_top_row: invalid_point.row,
810 }),
811 window,
812 cx,
813 );
814 assert_eq!(
815 editor.selections.display_ranges(cx),
816 &[editor.max_point(cx)..editor.max_point(cx)]
817 );
818 assert_eq!(
819 editor.scroll_position(cx),
820 gpui::Point::new(0., editor.max_point(cx).row().as_f64())
821 );
822
823 editor
824 })
825 });
826}
827
828#[gpui::test]
829fn test_cancel(cx: &mut TestAppContext) {
830 init_test(cx, |_| {});
831
832 let editor = cx.add_window(|window, cx| {
833 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
834 build_editor(buffer, window, cx)
835 });
836
837 _ = editor.update(cx, |editor, window, cx| {
838 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
839 editor.update_selection(
840 DisplayPoint::new(DisplayRow(1), 1),
841 0,
842 gpui::Point::<f32>::default(),
843 window,
844 cx,
845 );
846 editor.end_selection(window, cx);
847
848 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
849 editor.update_selection(
850 DisplayPoint::new(DisplayRow(0), 3),
851 0,
852 gpui::Point::<f32>::default(),
853 window,
854 cx,
855 );
856 editor.end_selection(window, cx);
857 assert_eq!(
858 editor.selections.display_ranges(cx),
859 [
860 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
861 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
862 ]
863 );
864 });
865
866 _ = editor.update(cx, |editor, window, cx| {
867 editor.cancel(&Cancel, window, cx);
868 assert_eq!(
869 editor.selections.display_ranges(cx),
870 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
871 );
872 });
873
874 _ = editor.update(cx, |editor, window, cx| {
875 editor.cancel(&Cancel, window, cx);
876 assert_eq!(
877 editor.selections.display_ranges(cx),
878 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
879 );
880 });
881}
882
883#[gpui::test]
884fn test_fold_action(cx: &mut TestAppContext) {
885 init_test(cx, |_| {});
886
887 let editor = cx.add_window(|window, cx| {
888 let buffer = MultiBuffer::build_simple(
889 &"
890 impl Foo {
891 // Hello!
892
893 fn a() {
894 1
895 }
896
897 fn b() {
898 2
899 }
900
901 fn c() {
902 3
903 }
904 }
905 "
906 .unindent(),
907 cx,
908 );
909 build_editor(buffer, window, cx)
910 });
911
912 _ = editor.update(cx, |editor, window, cx| {
913 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
914 s.select_display_ranges([
915 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
916 ]);
917 });
918 editor.fold(&Fold, window, cx);
919 assert_eq!(
920 editor.display_text(cx),
921 "
922 impl Foo {
923 // Hello!
924
925 fn a() {
926 1
927 }
928
929 fn b() {⋯
930 }
931
932 fn c() {⋯
933 }
934 }
935 "
936 .unindent(),
937 );
938
939 editor.fold(&Fold, window, cx);
940 assert_eq!(
941 editor.display_text(cx),
942 "
943 impl Foo {⋯
944 }
945 "
946 .unindent(),
947 );
948
949 editor.unfold_lines(&UnfoldLines, window, cx);
950 assert_eq!(
951 editor.display_text(cx),
952 "
953 impl Foo {
954 // Hello!
955
956 fn a() {
957 1
958 }
959
960 fn b() {⋯
961 }
962
963 fn c() {⋯
964 }
965 }
966 "
967 .unindent(),
968 );
969
970 editor.unfold_lines(&UnfoldLines, window, cx);
971 assert_eq!(
972 editor.display_text(cx),
973 editor.buffer.read(cx).read(cx).text()
974 );
975 });
976}
977
978#[gpui::test]
979fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
980 init_test(cx, |_| {});
981
982 let editor = cx.add_window(|window, cx| {
983 let buffer = MultiBuffer::build_simple(
984 &"
985 class Foo:
986 # Hello!
987
988 def a():
989 print(1)
990
991 def b():
992 print(2)
993
994 def c():
995 print(3)
996 "
997 .unindent(),
998 cx,
999 );
1000 build_editor(buffer, window, cx)
1001 });
1002
1003 _ = editor.update(cx, |editor, window, cx| {
1004 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1005 s.select_display_ranges([
1006 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
1007 ]);
1008 });
1009 editor.fold(&Fold, window, cx);
1010 assert_eq!(
1011 editor.display_text(cx),
1012 "
1013 class Foo:
1014 # Hello!
1015
1016 def a():
1017 print(1)
1018
1019 def b():⋯
1020
1021 def c():⋯
1022 "
1023 .unindent(),
1024 );
1025
1026 editor.fold(&Fold, window, cx);
1027 assert_eq!(
1028 editor.display_text(cx),
1029 "
1030 class Foo:⋯
1031 "
1032 .unindent(),
1033 );
1034
1035 editor.unfold_lines(&UnfoldLines, window, cx);
1036 assert_eq!(
1037 editor.display_text(cx),
1038 "
1039 class Foo:
1040 # Hello!
1041
1042 def a():
1043 print(1)
1044
1045 def b():⋯
1046
1047 def c():⋯
1048 "
1049 .unindent(),
1050 );
1051
1052 editor.unfold_lines(&UnfoldLines, window, cx);
1053 assert_eq!(
1054 editor.display_text(cx),
1055 editor.buffer.read(cx).read(cx).text()
1056 );
1057 });
1058}
1059
1060#[gpui::test]
1061fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1062 init_test(cx, |_| {});
1063
1064 let editor = cx.add_window(|window, cx| {
1065 let buffer = MultiBuffer::build_simple(
1066 &"
1067 class Foo:
1068 # Hello!
1069
1070 def a():
1071 print(1)
1072
1073 def b():
1074 print(2)
1075
1076
1077 def c():
1078 print(3)
1079
1080
1081 "
1082 .unindent(),
1083 cx,
1084 );
1085 build_editor(buffer, window, cx)
1086 });
1087
1088 _ = editor.update(cx, |editor, window, cx| {
1089 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1090 s.select_display_ranges([
1091 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1092 ]);
1093 });
1094 editor.fold(&Fold, window, cx);
1095 assert_eq!(
1096 editor.display_text(cx),
1097 "
1098 class Foo:
1099 # Hello!
1100
1101 def a():
1102 print(1)
1103
1104 def b():⋯
1105
1106
1107 def c():⋯
1108
1109
1110 "
1111 .unindent(),
1112 );
1113
1114 editor.fold(&Fold, window, cx);
1115 assert_eq!(
1116 editor.display_text(cx),
1117 "
1118 class Foo:⋯
1119
1120
1121 "
1122 .unindent(),
1123 );
1124
1125 editor.unfold_lines(&UnfoldLines, window, cx);
1126 assert_eq!(
1127 editor.display_text(cx),
1128 "
1129 class Foo:
1130 # Hello!
1131
1132 def a():
1133 print(1)
1134
1135 def b():⋯
1136
1137
1138 def c():⋯
1139
1140
1141 "
1142 .unindent(),
1143 );
1144
1145 editor.unfold_lines(&UnfoldLines, window, cx);
1146 assert_eq!(
1147 editor.display_text(cx),
1148 editor.buffer.read(cx).read(cx).text()
1149 );
1150 });
1151}
1152
1153#[gpui::test]
1154fn test_fold_at_level(cx: &mut TestAppContext) {
1155 init_test(cx, |_| {});
1156
1157 let editor = cx.add_window(|window, cx| {
1158 let buffer = MultiBuffer::build_simple(
1159 &"
1160 class Foo:
1161 # Hello!
1162
1163 def a():
1164 print(1)
1165
1166 def b():
1167 print(2)
1168
1169
1170 class Bar:
1171 # World!
1172
1173 def a():
1174 print(1)
1175
1176 def b():
1177 print(2)
1178
1179
1180 "
1181 .unindent(),
1182 cx,
1183 );
1184 build_editor(buffer, window, cx)
1185 });
1186
1187 _ = editor.update(cx, |editor, window, cx| {
1188 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1189 assert_eq!(
1190 editor.display_text(cx),
1191 "
1192 class Foo:
1193 # Hello!
1194
1195 def a():⋯
1196
1197 def b():⋯
1198
1199
1200 class Bar:
1201 # World!
1202
1203 def a():⋯
1204
1205 def b():⋯
1206
1207
1208 "
1209 .unindent(),
1210 );
1211
1212 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1213 assert_eq!(
1214 editor.display_text(cx),
1215 "
1216 class Foo:⋯
1217
1218
1219 class Bar:⋯
1220
1221
1222 "
1223 .unindent(),
1224 );
1225
1226 editor.unfold_all(&UnfoldAll, window, cx);
1227 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1228 assert_eq!(
1229 editor.display_text(cx),
1230 "
1231 class Foo:
1232 # Hello!
1233
1234 def a():
1235 print(1)
1236
1237 def b():
1238 print(2)
1239
1240
1241 class Bar:
1242 # World!
1243
1244 def a():
1245 print(1)
1246
1247 def b():
1248 print(2)
1249
1250
1251 "
1252 .unindent(),
1253 );
1254
1255 assert_eq!(
1256 editor.display_text(cx),
1257 editor.buffer.read(cx).read(cx).text()
1258 );
1259 });
1260}
1261
1262#[gpui::test]
1263fn test_move_cursor(cx: &mut TestAppContext) {
1264 init_test(cx, |_| {});
1265
1266 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1267 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1268
1269 buffer.update(cx, |buffer, cx| {
1270 buffer.edit(
1271 vec![
1272 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1273 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1274 ],
1275 None,
1276 cx,
1277 );
1278 });
1279 _ = editor.update(cx, |editor, window, cx| {
1280 assert_eq!(
1281 editor.selections.display_ranges(cx),
1282 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1283 );
1284
1285 editor.move_down(&MoveDown, window, cx);
1286 assert_eq!(
1287 editor.selections.display_ranges(cx),
1288 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1289 );
1290
1291 editor.move_right(&MoveRight, window, cx);
1292 assert_eq!(
1293 editor.selections.display_ranges(cx),
1294 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1295 );
1296
1297 editor.move_left(&MoveLeft, window, cx);
1298 assert_eq!(
1299 editor.selections.display_ranges(cx),
1300 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1301 );
1302
1303 editor.move_up(&MoveUp, window, cx);
1304 assert_eq!(
1305 editor.selections.display_ranges(cx),
1306 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1307 );
1308
1309 editor.move_to_end(&MoveToEnd, window, cx);
1310 assert_eq!(
1311 editor.selections.display_ranges(cx),
1312 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1313 );
1314
1315 editor.move_to_beginning(&MoveToBeginning, window, cx);
1316 assert_eq!(
1317 editor.selections.display_ranges(cx),
1318 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1319 );
1320
1321 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1322 s.select_display_ranges([
1323 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1324 ]);
1325 });
1326 editor.select_to_beginning(&SelectToBeginning, window, cx);
1327 assert_eq!(
1328 editor.selections.display_ranges(cx),
1329 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1330 );
1331
1332 editor.select_to_end(&SelectToEnd, window, cx);
1333 assert_eq!(
1334 editor.selections.display_ranges(cx),
1335 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1336 );
1337 });
1338}
1339
1340#[gpui::test]
1341fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1342 init_test(cx, |_| {});
1343
1344 let editor = cx.add_window(|window, cx| {
1345 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1346 build_editor(buffer, window, cx)
1347 });
1348
1349 assert_eq!('🟥'.len_utf8(), 4);
1350 assert_eq!('α'.len_utf8(), 2);
1351
1352 _ = editor.update(cx, |editor, window, cx| {
1353 editor.fold_creases(
1354 vec![
1355 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1356 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1357 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1358 ],
1359 true,
1360 window,
1361 cx,
1362 );
1363 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1364
1365 editor.move_right(&MoveRight, window, cx);
1366 assert_eq!(
1367 editor.selections.display_ranges(cx),
1368 &[empty_range(0, "🟥".len())]
1369 );
1370 editor.move_right(&MoveRight, window, cx);
1371 assert_eq!(
1372 editor.selections.display_ranges(cx),
1373 &[empty_range(0, "🟥🟧".len())]
1374 );
1375 editor.move_right(&MoveRight, window, cx);
1376 assert_eq!(
1377 editor.selections.display_ranges(cx),
1378 &[empty_range(0, "🟥🟧⋯".len())]
1379 );
1380
1381 editor.move_down(&MoveDown, window, cx);
1382 assert_eq!(
1383 editor.selections.display_ranges(cx),
1384 &[empty_range(1, "ab⋯e".len())]
1385 );
1386 editor.move_left(&MoveLeft, window, cx);
1387 assert_eq!(
1388 editor.selections.display_ranges(cx),
1389 &[empty_range(1, "ab⋯".len())]
1390 );
1391 editor.move_left(&MoveLeft, window, cx);
1392 assert_eq!(
1393 editor.selections.display_ranges(cx),
1394 &[empty_range(1, "ab".len())]
1395 );
1396 editor.move_left(&MoveLeft, window, cx);
1397 assert_eq!(
1398 editor.selections.display_ranges(cx),
1399 &[empty_range(1, "a".len())]
1400 );
1401
1402 editor.move_down(&MoveDown, window, cx);
1403 assert_eq!(
1404 editor.selections.display_ranges(cx),
1405 &[empty_range(2, "α".len())]
1406 );
1407 editor.move_right(&MoveRight, window, cx);
1408 assert_eq!(
1409 editor.selections.display_ranges(cx),
1410 &[empty_range(2, "αβ".len())]
1411 );
1412 editor.move_right(&MoveRight, window, cx);
1413 assert_eq!(
1414 editor.selections.display_ranges(cx),
1415 &[empty_range(2, "αβ⋯".len())]
1416 );
1417 editor.move_right(&MoveRight, window, cx);
1418 assert_eq!(
1419 editor.selections.display_ranges(cx),
1420 &[empty_range(2, "αβ⋯ε".len())]
1421 );
1422
1423 editor.move_up(&MoveUp, window, cx);
1424 assert_eq!(
1425 editor.selections.display_ranges(cx),
1426 &[empty_range(1, "ab⋯e".len())]
1427 );
1428 editor.move_down(&MoveDown, window, cx);
1429 assert_eq!(
1430 editor.selections.display_ranges(cx),
1431 &[empty_range(2, "αβ⋯ε".len())]
1432 );
1433 editor.move_up(&MoveUp, window, cx);
1434 assert_eq!(
1435 editor.selections.display_ranges(cx),
1436 &[empty_range(1, "ab⋯e".len())]
1437 );
1438
1439 editor.move_up(&MoveUp, window, cx);
1440 assert_eq!(
1441 editor.selections.display_ranges(cx),
1442 &[empty_range(0, "🟥🟧".len())]
1443 );
1444 editor.move_left(&MoveLeft, window, cx);
1445 assert_eq!(
1446 editor.selections.display_ranges(cx),
1447 &[empty_range(0, "🟥".len())]
1448 );
1449 editor.move_left(&MoveLeft, window, cx);
1450 assert_eq!(
1451 editor.selections.display_ranges(cx),
1452 &[empty_range(0, "".len())]
1453 );
1454 });
1455}
1456
1457#[gpui::test]
1458fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1459 init_test(cx, |_| {});
1460
1461 let editor = cx.add_window(|window, cx| {
1462 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1463 build_editor(buffer, window, cx)
1464 });
1465 _ = editor.update(cx, |editor, window, cx| {
1466 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1467 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1468 });
1469
1470 // moving above start of document should move selection to start of document,
1471 // but the next move down should still be at the original goal_x
1472 editor.move_up(&MoveUp, window, cx);
1473 assert_eq!(
1474 editor.selections.display_ranges(cx),
1475 &[empty_range(0, "".len())]
1476 );
1477
1478 editor.move_down(&MoveDown, window, cx);
1479 assert_eq!(
1480 editor.selections.display_ranges(cx),
1481 &[empty_range(1, "abcd".len())]
1482 );
1483
1484 editor.move_down(&MoveDown, window, cx);
1485 assert_eq!(
1486 editor.selections.display_ranges(cx),
1487 &[empty_range(2, "αβγ".len())]
1488 );
1489
1490 editor.move_down(&MoveDown, window, cx);
1491 assert_eq!(
1492 editor.selections.display_ranges(cx),
1493 &[empty_range(3, "abcd".len())]
1494 );
1495
1496 editor.move_down(&MoveDown, window, cx);
1497 assert_eq!(
1498 editor.selections.display_ranges(cx),
1499 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1500 );
1501
1502 // moving past end of document should not change goal_x
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_down(&MoveDown, window, cx);
1510 assert_eq!(
1511 editor.selections.display_ranges(cx),
1512 &[empty_range(5, "".len())]
1513 );
1514
1515 editor.move_up(&MoveUp, window, cx);
1516 assert_eq!(
1517 editor.selections.display_ranges(cx),
1518 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1519 );
1520
1521 editor.move_up(&MoveUp, window, cx);
1522 assert_eq!(
1523 editor.selections.display_ranges(cx),
1524 &[empty_range(3, "abcd".len())]
1525 );
1526
1527 editor.move_up(&MoveUp, window, cx);
1528 assert_eq!(
1529 editor.selections.display_ranges(cx),
1530 &[empty_range(2, "αβγ".len())]
1531 );
1532 });
1533}
1534
1535#[gpui::test]
1536fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1537 init_test(cx, |_| {});
1538 let move_to_beg = MoveToBeginningOfLine {
1539 stop_at_soft_wraps: true,
1540 stop_at_indent: true,
1541 };
1542
1543 let delete_to_beg = DeleteToBeginningOfLine {
1544 stop_at_indent: false,
1545 };
1546
1547 let move_to_end = MoveToEndOfLine {
1548 stop_at_soft_wraps: true,
1549 };
1550
1551 let editor = cx.add_window(|window, cx| {
1552 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1553 build_editor(buffer, window, cx)
1554 });
1555 _ = editor.update(cx, |editor, window, cx| {
1556 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1557 s.select_display_ranges([
1558 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1559 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1560 ]);
1561 });
1562 });
1563
1564 _ = editor.update(cx, |editor, window, cx| {
1565 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1566 assert_eq!(
1567 editor.selections.display_ranges(cx),
1568 &[
1569 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1570 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1571 ]
1572 );
1573 });
1574
1575 _ = editor.update(cx, |editor, window, cx| {
1576 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1577 assert_eq!(
1578 editor.selections.display_ranges(cx),
1579 &[
1580 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1581 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1582 ]
1583 );
1584 });
1585
1586 _ = editor.update(cx, |editor, window, cx| {
1587 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1588 assert_eq!(
1589 editor.selections.display_ranges(cx),
1590 &[
1591 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1592 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1593 ]
1594 );
1595 });
1596
1597 _ = editor.update(cx, |editor, window, cx| {
1598 editor.move_to_end_of_line(&move_to_end, window, cx);
1599 assert_eq!(
1600 editor.selections.display_ranges(cx),
1601 &[
1602 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1603 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1604 ]
1605 );
1606 });
1607
1608 // Moving to the end of line again is a no-op.
1609 _ = editor.update(cx, |editor, window, cx| {
1610 editor.move_to_end_of_line(&move_to_end, window, cx);
1611 assert_eq!(
1612 editor.selections.display_ranges(cx),
1613 &[
1614 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1615 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1616 ]
1617 );
1618 });
1619
1620 _ = editor.update(cx, |editor, window, cx| {
1621 editor.move_left(&MoveLeft, window, cx);
1622 editor.select_to_beginning_of_line(
1623 &SelectToBeginningOfLine {
1624 stop_at_soft_wraps: true,
1625 stop_at_indent: true,
1626 },
1627 window,
1628 cx,
1629 );
1630 assert_eq!(
1631 editor.selections.display_ranges(cx),
1632 &[
1633 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1634 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1635 ]
1636 );
1637 });
1638
1639 _ = editor.update(cx, |editor, window, cx| {
1640 editor.select_to_beginning_of_line(
1641 &SelectToBeginningOfLine {
1642 stop_at_soft_wraps: true,
1643 stop_at_indent: true,
1644 },
1645 window,
1646 cx,
1647 );
1648 assert_eq!(
1649 editor.selections.display_ranges(cx),
1650 &[
1651 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1652 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1653 ]
1654 );
1655 });
1656
1657 _ = editor.update(cx, |editor, window, cx| {
1658 editor.select_to_beginning_of_line(
1659 &SelectToBeginningOfLine {
1660 stop_at_soft_wraps: true,
1661 stop_at_indent: true,
1662 },
1663 window,
1664 cx,
1665 );
1666 assert_eq!(
1667 editor.selections.display_ranges(cx),
1668 &[
1669 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1670 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1671 ]
1672 );
1673 });
1674
1675 _ = editor.update(cx, |editor, window, cx| {
1676 editor.select_to_end_of_line(
1677 &SelectToEndOfLine {
1678 stop_at_soft_wraps: true,
1679 },
1680 window,
1681 cx,
1682 );
1683 assert_eq!(
1684 editor.selections.display_ranges(cx),
1685 &[
1686 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1687 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1688 ]
1689 );
1690 });
1691
1692 _ = editor.update(cx, |editor, window, cx| {
1693 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1694 assert_eq!(editor.display_text(cx), "ab\n de");
1695 assert_eq!(
1696 editor.selections.display_ranges(cx),
1697 &[
1698 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1699 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1700 ]
1701 );
1702 });
1703
1704 _ = editor.update(cx, |editor, window, cx| {
1705 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1706 assert_eq!(editor.display_text(cx), "\n");
1707 assert_eq!(
1708 editor.selections.display_ranges(cx),
1709 &[
1710 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1711 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1712 ]
1713 );
1714 });
1715}
1716
1717#[gpui::test]
1718fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1719 init_test(cx, |_| {});
1720 let move_to_beg = MoveToBeginningOfLine {
1721 stop_at_soft_wraps: false,
1722 stop_at_indent: false,
1723 };
1724
1725 let move_to_end = MoveToEndOfLine {
1726 stop_at_soft_wraps: false,
1727 };
1728
1729 let editor = cx.add_window(|window, cx| {
1730 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1731 build_editor(buffer, window, cx)
1732 });
1733
1734 _ = editor.update(cx, |editor, window, cx| {
1735 editor.set_wrap_width(Some(140.0.into()), cx);
1736
1737 // We expect the following lines after wrapping
1738 // ```
1739 // thequickbrownfox
1740 // jumpedoverthelazydo
1741 // gs
1742 // ```
1743 // The final `gs` was soft-wrapped onto a new line.
1744 assert_eq!(
1745 "thequickbrownfox\njumpedoverthelaz\nydogs",
1746 editor.display_text(cx),
1747 );
1748
1749 // First, let's assert behavior on the first line, that was not soft-wrapped.
1750 // Start the cursor at the `k` on the first line
1751 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1752 s.select_display_ranges([
1753 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1754 ]);
1755 });
1756
1757 // Moving to the beginning of the line should put us at the beginning of the line.
1758 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1759 assert_eq!(
1760 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1761 editor.selections.display_ranges(cx)
1762 );
1763
1764 // Moving to the end of the line should put us at the end of the line.
1765 editor.move_to_end_of_line(&move_to_end, window, cx);
1766 assert_eq!(
1767 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1768 editor.selections.display_ranges(cx)
1769 );
1770
1771 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1772 // Start the cursor at the last line (`y` that was wrapped to a new line)
1773 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1774 s.select_display_ranges([
1775 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1776 ]);
1777 });
1778
1779 // Moving to the beginning of the line should put us at the start of the second line of
1780 // display text, i.e., the `j`.
1781 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1782 assert_eq!(
1783 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1784 editor.selections.display_ranges(cx)
1785 );
1786
1787 // Moving to the beginning of the line again should be a no-op.
1788 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1789 assert_eq!(
1790 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1791 editor.selections.display_ranges(cx)
1792 );
1793
1794 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1795 // next display line.
1796 editor.move_to_end_of_line(&move_to_end, window, cx);
1797 assert_eq!(
1798 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1799 editor.selections.display_ranges(cx)
1800 );
1801
1802 // Moving to the end of the line again should be a no-op.
1803 editor.move_to_end_of_line(&move_to_end, window, cx);
1804 assert_eq!(
1805 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1806 editor.selections.display_ranges(cx)
1807 );
1808 });
1809}
1810
1811#[gpui::test]
1812fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1813 init_test(cx, |_| {});
1814
1815 let move_to_beg = MoveToBeginningOfLine {
1816 stop_at_soft_wraps: true,
1817 stop_at_indent: true,
1818 };
1819
1820 let select_to_beg = SelectToBeginningOfLine {
1821 stop_at_soft_wraps: true,
1822 stop_at_indent: true,
1823 };
1824
1825 let delete_to_beg = DeleteToBeginningOfLine {
1826 stop_at_indent: true,
1827 };
1828
1829 let move_to_end = MoveToEndOfLine {
1830 stop_at_soft_wraps: false,
1831 };
1832
1833 let editor = cx.add_window(|window, cx| {
1834 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1835 build_editor(buffer, window, cx)
1836 });
1837
1838 _ = editor.update(cx, |editor, window, cx| {
1839 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1840 s.select_display_ranges([
1841 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1842 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1843 ]);
1844 });
1845
1846 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1847 // and the second cursor at the first non-whitespace character in the line.
1848 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1849 assert_eq!(
1850 editor.selections.display_ranges(cx),
1851 &[
1852 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1853 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1854 ]
1855 );
1856
1857 // Moving to the beginning of the line again should be a no-op for the first cursor,
1858 // and should move the second cursor to the beginning of the line.
1859 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1860 assert_eq!(
1861 editor.selections.display_ranges(cx),
1862 &[
1863 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1864 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1865 ]
1866 );
1867
1868 // Moving to the beginning of the line again should still be a no-op for the first cursor,
1869 // and should move the second cursor back to the first non-whitespace character in the line.
1870 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1871 assert_eq!(
1872 editor.selections.display_ranges(cx),
1873 &[
1874 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1875 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1876 ]
1877 );
1878
1879 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
1880 // and to the first non-whitespace character in the line for the second cursor.
1881 editor.move_to_end_of_line(&move_to_end, window, cx);
1882 editor.move_left(&MoveLeft, window, cx);
1883 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1884 assert_eq!(
1885 editor.selections.display_ranges(cx),
1886 &[
1887 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1888 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1889 ]
1890 );
1891
1892 // Selecting to the beginning of the line again should be a no-op for the first cursor,
1893 // and should select to the beginning of the line for the second cursor.
1894 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1895 assert_eq!(
1896 editor.selections.display_ranges(cx),
1897 &[
1898 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1899 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1900 ]
1901 );
1902
1903 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
1904 // and should delete to the first non-whitespace character in the line for the second cursor.
1905 editor.move_to_end_of_line(&move_to_end, window, cx);
1906 editor.move_left(&MoveLeft, window, cx);
1907 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1908 assert_eq!(editor.text(cx), "c\n f");
1909 });
1910}
1911
1912#[gpui::test]
1913fn test_beginning_of_line_with_cursor_between_line_start_and_indent(cx: &mut TestAppContext) {
1914 init_test(cx, |_| {});
1915
1916 let move_to_beg = MoveToBeginningOfLine {
1917 stop_at_soft_wraps: true,
1918 stop_at_indent: true,
1919 };
1920
1921 let editor = cx.add_window(|window, cx| {
1922 let buffer = MultiBuffer::build_simple(" hello\nworld", cx);
1923 build_editor(buffer, window, cx)
1924 });
1925
1926 _ = editor.update(cx, |editor, window, cx| {
1927 // test cursor between line_start and indent_start
1928 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1929 s.select_display_ranges([
1930 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3)
1931 ]);
1932 });
1933
1934 // cursor should move to line_start
1935 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1936 assert_eq!(
1937 editor.selections.display_ranges(cx),
1938 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1939 );
1940
1941 // cursor should move to indent_start
1942 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1943 assert_eq!(
1944 editor.selections.display_ranges(cx),
1945 &[DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 4)]
1946 );
1947
1948 // cursor should move to back to line_start
1949 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1950 assert_eq!(
1951 editor.selections.display_ranges(cx),
1952 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1953 );
1954 });
1955}
1956
1957#[gpui::test]
1958fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1959 init_test(cx, |_| {});
1960
1961 let editor = cx.add_window(|window, cx| {
1962 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1963 build_editor(buffer, window, cx)
1964 });
1965 _ = editor.update(cx, |editor, window, cx| {
1966 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1967 s.select_display_ranges([
1968 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
1969 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
1970 ])
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_previous_word_start(&MoveToPreviousWordStart, window, cx);
1982 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1983
1984 editor.move_to_previous_word_start(&MoveToPreviousWordStart, 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_to_next_word_end(&MoveToNextWordEnd, window, cx);
1991 assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
1992
1993 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1994 assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1995
1996 editor.move_right(&MoveRight, window, cx);
1997 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1998 assert_selection_ranges(
1999 "use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}",
2000 editor,
2001 cx,
2002 );
2003
2004 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2005 assert_selection_ranges(
2006 "use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
2007 editor,
2008 cx,
2009 );
2010
2011 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
2012 assert_selection_ranges(
2013 "use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
2014 editor,
2015 cx,
2016 );
2017 });
2018}
2019
2020#[gpui::test]
2021fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
2022 init_test(cx, |_| {});
2023
2024 let editor = cx.add_window(|window, cx| {
2025 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
2026 build_editor(buffer, window, cx)
2027 });
2028
2029 _ = editor.update(cx, |editor, window, cx| {
2030 editor.set_wrap_width(Some(140.0.into()), cx);
2031 assert_eq!(
2032 editor.display_text(cx),
2033 "use one::{\n two::three::\n four::five\n};"
2034 );
2035
2036 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2037 s.select_display_ranges([
2038 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
2039 ]);
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), 9)..DisplayPoint::new(DisplayRow(1), 9)]
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(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
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), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2058 );
2059
2060 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2061 assert_eq!(
2062 editor.selections.display_ranges(cx),
2063 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
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(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2070 );
2071
2072 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2073 assert_eq!(
2074 editor.selections.display_ranges(cx),
2075 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2076 );
2077 });
2078}
2079
2080#[gpui::test]
2081async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2082 init_test(cx, |_| {});
2083 let mut cx = EditorTestContext::new(cx).await;
2084
2085 let line_height = cx.editor(|editor, window, _| {
2086 editor
2087 .style()
2088 .unwrap()
2089 .text
2090 .line_height_in_pixels(window.rem_size())
2091 });
2092 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2093
2094 cx.set_state(
2095 &r#"ˇone
2096 two
2097
2098 three
2099 fourˇ
2100 five
2101
2102 six"#
2103 .unindent(),
2104 );
2105
2106 cx.update_editor(|editor, window, cx| {
2107 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2108 });
2109 cx.assert_editor_state(
2110 &r#"one
2111 two
2112 ˇ
2113 three
2114 four
2115 five
2116 ˇ
2117 six"#
2118 .unindent(),
2119 );
2120
2121 cx.update_editor(|editor, window, cx| {
2122 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2123 });
2124 cx.assert_editor_state(
2125 &r#"one
2126 two
2127
2128 three
2129 four
2130 five
2131 ˇ
2132 sixˇ"#
2133 .unindent(),
2134 );
2135
2136 cx.update_editor(|editor, window, cx| {
2137 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2138 });
2139 cx.assert_editor_state(
2140 &r#"one
2141 two
2142
2143 three
2144 four
2145 five
2146
2147 sixˇ"#
2148 .unindent(),
2149 );
2150
2151 cx.update_editor(|editor, window, cx| {
2152 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2153 });
2154 cx.assert_editor_state(
2155 &r#"one
2156 two
2157
2158 three
2159 four
2160 five
2161 ˇ
2162 six"#
2163 .unindent(),
2164 );
2165
2166 cx.update_editor(|editor, window, cx| {
2167 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2168 });
2169 cx.assert_editor_state(
2170 &r#"one
2171 two
2172 ˇ
2173 three
2174 four
2175 five
2176
2177 six"#
2178 .unindent(),
2179 );
2180
2181 cx.update_editor(|editor, window, cx| {
2182 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2183 });
2184 cx.assert_editor_state(
2185 &r#"ˇone
2186 two
2187
2188 three
2189 four
2190 five
2191
2192 six"#
2193 .unindent(),
2194 );
2195}
2196
2197#[gpui::test]
2198async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2199 init_test(cx, |_| {});
2200 let mut cx = EditorTestContext::new(cx).await;
2201 let line_height = cx.editor(|editor, window, _| {
2202 editor
2203 .style()
2204 .unwrap()
2205 .text
2206 .line_height_in_pixels(window.rem_size())
2207 });
2208 let window = cx.window;
2209 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2210
2211 cx.set_state(
2212 r#"ˇone
2213 two
2214 three
2215 four
2216 five
2217 six
2218 seven
2219 eight
2220 nine
2221 ten
2222 "#,
2223 );
2224
2225 cx.update_editor(|editor, window, cx| {
2226 assert_eq!(
2227 editor.snapshot(window, cx).scroll_position(),
2228 gpui::Point::new(0., 0.)
2229 );
2230 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2231 assert_eq!(
2232 editor.snapshot(window, cx).scroll_position(),
2233 gpui::Point::new(0., 3.)
2234 );
2235 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2236 assert_eq!(
2237 editor.snapshot(window, cx).scroll_position(),
2238 gpui::Point::new(0., 6.)
2239 );
2240 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2241 assert_eq!(
2242 editor.snapshot(window, cx).scroll_position(),
2243 gpui::Point::new(0., 3.)
2244 );
2245
2246 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2247 assert_eq!(
2248 editor.snapshot(window, cx).scroll_position(),
2249 gpui::Point::new(0., 1.)
2250 );
2251 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2252 assert_eq!(
2253 editor.snapshot(window, cx).scroll_position(),
2254 gpui::Point::new(0., 3.)
2255 );
2256 });
2257}
2258
2259#[gpui::test]
2260async fn test_autoscroll(cx: &mut TestAppContext) {
2261 init_test(cx, |_| {});
2262 let mut cx = EditorTestContext::new(cx).await;
2263
2264 let line_height = cx.update_editor(|editor, window, cx| {
2265 editor.set_vertical_scroll_margin(2, cx);
2266 editor
2267 .style()
2268 .unwrap()
2269 .text
2270 .line_height_in_pixels(window.rem_size())
2271 });
2272 let window = cx.window;
2273 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2274
2275 cx.set_state(
2276 r#"ˇone
2277 two
2278 three
2279 four
2280 five
2281 six
2282 seven
2283 eight
2284 nine
2285 ten
2286 "#,
2287 );
2288 cx.update_editor(|editor, window, cx| {
2289 assert_eq!(
2290 editor.snapshot(window, cx).scroll_position(),
2291 gpui::Point::new(0., 0.0)
2292 );
2293 });
2294
2295 // Add a cursor below the visible area. Since both cursors cannot fit
2296 // on screen, the editor autoscrolls to reveal the newest cursor, and
2297 // allows the vertical scroll margin below that cursor.
2298 cx.update_editor(|editor, window, cx| {
2299 editor.change_selections(Default::default(), window, cx, |selections| {
2300 selections.select_ranges([
2301 Point::new(0, 0)..Point::new(0, 0),
2302 Point::new(6, 0)..Point::new(6, 0),
2303 ]);
2304 })
2305 });
2306 cx.update_editor(|editor, window, cx| {
2307 assert_eq!(
2308 editor.snapshot(window, cx).scroll_position(),
2309 gpui::Point::new(0., 3.0)
2310 );
2311 });
2312
2313 // Move down. The editor cursor scrolls down to track the newest cursor.
2314 cx.update_editor(|editor, window, cx| {
2315 editor.move_down(&Default::default(), window, cx);
2316 });
2317 cx.update_editor(|editor, window, cx| {
2318 assert_eq!(
2319 editor.snapshot(window, cx).scroll_position(),
2320 gpui::Point::new(0., 4.0)
2321 );
2322 });
2323
2324 // Add a cursor above the visible area. Since both cursors fit on screen,
2325 // the editor scrolls to show both.
2326 cx.update_editor(|editor, window, cx| {
2327 editor.change_selections(Default::default(), window, cx, |selections| {
2328 selections.select_ranges([
2329 Point::new(1, 0)..Point::new(1, 0),
2330 Point::new(6, 0)..Point::new(6, 0),
2331 ]);
2332 })
2333 });
2334 cx.update_editor(|editor, window, cx| {
2335 assert_eq!(
2336 editor.snapshot(window, cx).scroll_position(),
2337 gpui::Point::new(0., 1.0)
2338 );
2339 });
2340}
2341
2342#[gpui::test]
2343async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2344 init_test(cx, |_| {});
2345 let mut cx = EditorTestContext::new(cx).await;
2346
2347 let line_height = cx.editor(|editor, window, _cx| {
2348 editor
2349 .style()
2350 .unwrap()
2351 .text
2352 .line_height_in_pixels(window.rem_size())
2353 });
2354 let window = cx.window;
2355 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2356 cx.set_state(
2357 &r#"
2358 ˇone
2359 two
2360 threeˇ
2361 four
2362 five
2363 six
2364 seven
2365 eight
2366 nine
2367 ten
2368 "#
2369 .unindent(),
2370 );
2371
2372 cx.update_editor(|editor, window, cx| {
2373 editor.move_page_down(&MovePageDown::default(), window, cx)
2374 });
2375 cx.assert_editor_state(
2376 &r#"
2377 one
2378 two
2379 three
2380 ˇfour
2381 five
2382 sixˇ
2383 seven
2384 eight
2385 nine
2386 ten
2387 "#
2388 .unindent(),
2389 );
2390
2391 cx.update_editor(|editor, window, cx| {
2392 editor.move_page_down(&MovePageDown::default(), window, cx)
2393 });
2394 cx.assert_editor_state(
2395 &r#"
2396 one
2397 two
2398 three
2399 four
2400 five
2401 six
2402 ˇseven
2403 eight
2404 nineˇ
2405 ten
2406 "#
2407 .unindent(),
2408 );
2409
2410 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2411 cx.assert_editor_state(
2412 &r#"
2413 one
2414 two
2415 three
2416 ˇfour
2417 five
2418 sixˇ
2419 seven
2420 eight
2421 nine
2422 ten
2423 "#
2424 .unindent(),
2425 );
2426
2427 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2428 cx.assert_editor_state(
2429 &r#"
2430 ˇone
2431 two
2432 threeˇ
2433 four
2434 five
2435 six
2436 seven
2437 eight
2438 nine
2439 ten
2440 "#
2441 .unindent(),
2442 );
2443
2444 // Test select collapsing
2445 cx.update_editor(|editor, window, cx| {
2446 editor.move_page_down(&MovePageDown::default(), window, cx);
2447 editor.move_page_down(&MovePageDown::default(), window, cx);
2448 editor.move_page_down(&MovePageDown::default(), window, cx);
2449 });
2450 cx.assert_editor_state(
2451 &r#"
2452 one
2453 two
2454 three
2455 four
2456 five
2457 six
2458 seven
2459 eight
2460 nine
2461 ˇten
2462 ˇ"#
2463 .unindent(),
2464 );
2465}
2466
2467#[gpui::test]
2468async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2469 init_test(cx, |_| {});
2470 let mut cx = EditorTestContext::new(cx).await;
2471 cx.set_state("one «two threeˇ» four");
2472 cx.update_editor(|editor, window, cx| {
2473 editor.delete_to_beginning_of_line(
2474 &DeleteToBeginningOfLine {
2475 stop_at_indent: false,
2476 },
2477 window,
2478 cx,
2479 );
2480 assert_eq!(editor.text(cx), " four");
2481 });
2482}
2483
2484#[gpui::test]
2485async fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2486 init_test(cx, |_| {});
2487
2488 let mut cx = EditorTestContext::new(cx).await;
2489
2490 // For an empty selection, the preceding word fragment is deleted.
2491 // For non-empty selections, only selected characters are deleted.
2492 cx.set_state("onˇe two t«hreˇ»e four");
2493 cx.update_editor(|editor, window, cx| {
2494 editor.delete_to_previous_word_start(
2495 &DeleteToPreviousWordStart {
2496 ignore_newlines: false,
2497 ignore_brackets: false,
2498 },
2499 window,
2500 cx,
2501 );
2502 });
2503 cx.assert_editor_state("ˇe two tˇe four");
2504
2505 cx.set_state("e tˇwo te «fˇ»our");
2506 cx.update_editor(|editor, window, cx| {
2507 editor.delete_to_next_word_end(
2508 &DeleteToNextWordEnd {
2509 ignore_newlines: false,
2510 ignore_brackets: false,
2511 },
2512 window,
2513 cx,
2514 );
2515 });
2516 cx.assert_editor_state("e tˇ te ˇour");
2517}
2518
2519#[gpui::test]
2520async fn test_delete_whitespaces(cx: &mut TestAppContext) {
2521 init_test(cx, |_| {});
2522
2523 let mut cx = EditorTestContext::new(cx).await;
2524
2525 cx.set_state("here is some text ˇwith a space");
2526 cx.update_editor(|editor, window, cx| {
2527 editor.delete_to_previous_word_start(
2528 &DeleteToPreviousWordStart {
2529 ignore_newlines: false,
2530 ignore_brackets: true,
2531 },
2532 window,
2533 cx,
2534 );
2535 });
2536 // Continuous whitespace sequences are removed entirely, words behind them are not affected by the deletion action.
2537 cx.assert_editor_state("here is some textˇwith a space");
2538
2539 cx.set_state("here is some text ˇwith a space");
2540 cx.update_editor(|editor, window, cx| {
2541 editor.delete_to_previous_word_start(
2542 &DeleteToPreviousWordStart {
2543 ignore_newlines: false,
2544 ignore_brackets: false,
2545 },
2546 window,
2547 cx,
2548 );
2549 });
2550 cx.assert_editor_state("here is some textˇwith a space");
2551
2552 cx.set_state("here is some textˇ with a space");
2553 cx.update_editor(|editor, window, cx| {
2554 editor.delete_to_next_word_end(
2555 &DeleteToNextWordEnd {
2556 ignore_newlines: false,
2557 ignore_brackets: true,
2558 },
2559 window,
2560 cx,
2561 );
2562 });
2563 // Same happens in the other direction.
2564 cx.assert_editor_state("here is some textˇwith a space");
2565
2566 cx.set_state("here is some textˇ with a space");
2567 cx.update_editor(|editor, window, cx| {
2568 editor.delete_to_next_word_end(
2569 &DeleteToNextWordEnd {
2570 ignore_newlines: false,
2571 ignore_brackets: false,
2572 },
2573 window,
2574 cx,
2575 );
2576 });
2577 cx.assert_editor_state("here is some textˇwith a space");
2578
2579 cx.set_state("here is some textˇ with a space");
2580 cx.update_editor(|editor, window, cx| {
2581 editor.delete_to_next_word_end(
2582 &DeleteToNextWordEnd {
2583 ignore_newlines: true,
2584 ignore_brackets: false,
2585 },
2586 window,
2587 cx,
2588 );
2589 });
2590 cx.assert_editor_state("here is some textˇwith a space");
2591 cx.update_editor(|editor, window, cx| {
2592 editor.delete_to_previous_word_start(
2593 &DeleteToPreviousWordStart {
2594 ignore_newlines: true,
2595 ignore_brackets: false,
2596 },
2597 window,
2598 cx,
2599 );
2600 });
2601 cx.assert_editor_state("here is some ˇwith a space");
2602 cx.update_editor(|editor, window, cx| {
2603 editor.delete_to_previous_word_start(
2604 &DeleteToPreviousWordStart {
2605 ignore_newlines: true,
2606 ignore_brackets: false,
2607 },
2608 window,
2609 cx,
2610 );
2611 });
2612 // Single whitespaces are removed with the word behind them.
2613 cx.assert_editor_state("here is ˇwith a space");
2614 cx.update_editor(|editor, window, cx| {
2615 editor.delete_to_previous_word_start(
2616 &DeleteToPreviousWordStart {
2617 ignore_newlines: true,
2618 ignore_brackets: false,
2619 },
2620 window,
2621 cx,
2622 );
2623 });
2624 cx.assert_editor_state("here ˇwith a space");
2625 cx.update_editor(|editor, window, cx| {
2626 editor.delete_to_previous_word_start(
2627 &DeleteToPreviousWordStart {
2628 ignore_newlines: true,
2629 ignore_brackets: false,
2630 },
2631 window,
2632 cx,
2633 );
2634 });
2635 cx.assert_editor_state("ˇwith a space");
2636 cx.update_editor(|editor, window, cx| {
2637 editor.delete_to_previous_word_start(
2638 &DeleteToPreviousWordStart {
2639 ignore_newlines: true,
2640 ignore_brackets: false,
2641 },
2642 window,
2643 cx,
2644 );
2645 });
2646 cx.assert_editor_state("ˇwith a space");
2647 cx.update_editor(|editor, window, cx| {
2648 editor.delete_to_next_word_end(
2649 &DeleteToNextWordEnd {
2650 ignore_newlines: true,
2651 ignore_brackets: false,
2652 },
2653 window,
2654 cx,
2655 );
2656 });
2657 // Same happens in the other direction.
2658 cx.assert_editor_state("ˇ a space");
2659 cx.update_editor(|editor, window, cx| {
2660 editor.delete_to_next_word_end(
2661 &DeleteToNextWordEnd {
2662 ignore_newlines: true,
2663 ignore_brackets: false,
2664 },
2665 window,
2666 cx,
2667 );
2668 });
2669 cx.assert_editor_state("ˇ space");
2670 cx.update_editor(|editor, window, cx| {
2671 editor.delete_to_next_word_end(
2672 &DeleteToNextWordEnd {
2673 ignore_newlines: true,
2674 ignore_brackets: false,
2675 },
2676 window,
2677 cx,
2678 );
2679 });
2680 cx.assert_editor_state("ˇ");
2681 cx.update_editor(|editor, window, cx| {
2682 editor.delete_to_next_word_end(
2683 &DeleteToNextWordEnd {
2684 ignore_newlines: true,
2685 ignore_brackets: false,
2686 },
2687 window,
2688 cx,
2689 );
2690 });
2691 cx.assert_editor_state("ˇ");
2692 cx.update_editor(|editor, window, cx| {
2693 editor.delete_to_previous_word_start(
2694 &DeleteToPreviousWordStart {
2695 ignore_newlines: true,
2696 ignore_brackets: false,
2697 },
2698 window,
2699 cx,
2700 );
2701 });
2702 cx.assert_editor_state("ˇ");
2703}
2704
2705#[gpui::test]
2706async fn test_delete_to_bracket(cx: &mut TestAppContext) {
2707 init_test(cx, |_| {});
2708
2709 let language = Arc::new(
2710 Language::new(
2711 LanguageConfig {
2712 brackets: BracketPairConfig {
2713 pairs: vec![
2714 BracketPair {
2715 start: "\"".to_string(),
2716 end: "\"".to_string(),
2717 close: true,
2718 surround: true,
2719 newline: false,
2720 },
2721 BracketPair {
2722 start: "(".to_string(),
2723 end: ")".to_string(),
2724 close: true,
2725 surround: true,
2726 newline: true,
2727 },
2728 ],
2729 ..BracketPairConfig::default()
2730 },
2731 ..LanguageConfig::default()
2732 },
2733 Some(tree_sitter_rust::LANGUAGE.into()),
2734 )
2735 .with_brackets_query(
2736 r#"
2737 ("(" @open ")" @close)
2738 ("\"" @open "\"" @close)
2739 "#,
2740 )
2741 .unwrap(),
2742 );
2743
2744 let mut cx = EditorTestContext::new(cx).await;
2745 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2746
2747 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2748 cx.update_editor(|editor, window, cx| {
2749 editor.delete_to_previous_word_start(
2750 &DeleteToPreviousWordStart {
2751 ignore_newlines: true,
2752 ignore_brackets: false,
2753 },
2754 window,
2755 cx,
2756 );
2757 });
2758 // Deletion stops before brackets if asked to not ignore them.
2759 cx.assert_editor_state(r#"macro!("ˇCOMMENT");"#);
2760 cx.update_editor(|editor, window, cx| {
2761 editor.delete_to_previous_word_start(
2762 &DeleteToPreviousWordStart {
2763 ignore_newlines: true,
2764 ignore_brackets: false,
2765 },
2766 window,
2767 cx,
2768 );
2769 });
2770 // Deletion has to remove a single bracket and then stop again.
2771 cx.assert_editor_state(r#"macro!(ˇCOMMENT");"#);
2772
2773 cx.update_editor(|editor, window, cx| {
2774 editor.delete_to_previous_word_start(
2775 &DeleteToPreviousWordStart {
2776 ignore_newlines: true,
2777 ignore_brackets: false,
2778 },
2779 window,
2780 cx,
2781 );
2782 });
2783 cx.assert_editor_state(r#"macro!ˇCOMMENT");"#);
2784
2785 cx.update_editor(|editor, window, cx| {
2786 editor.delete_to_previous_word_start(
2787 &DeleteToPreviousWordStart {
2788 ignore_newlines: true,
2789 ignore_brackets: false,
2790 },
2791 window,
2792 cx,
2793 );
2794 });
2795 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2796
2797 cx.update_editor(|editor, window, cx| {
2798 editor.delete_to_previous_word_start(
2799 &DeleteToPreviousWordStart {
2800 ignore_newlines: true,
2801 ignore_brackets: false,
2802 },
2803 window,
2804 cx,
2805 );
2806 });
2807 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2808
2809 cx.update_editor(|editor, window, cx| {
2810 editor.delete_to_next_word_end(
2811 &DeleteToNextWordEnd {
2812 ignore_newlines: true,
2813 ignore_brackets: false,
2814 },
2815 window,
2816 cx,
2817 );
2818 });
2819 // Brackets on the right are not paired anymore, hence deletion does not stop at them
2820 cx.assert_editor_state(r#"ˇ");"#);
2821
2822 cx.update_editor(|editor, window, cx| {
2823 editor.delete_to_next_word_end(
2824 &DeleteToNextWordEnd {
2825 ignore_newlines: true,
2826 ignore_brackets: false,
2827 },
2828 window,
2829 cx,
2830 );
2831 });
2832 cx.assert_editor_state(r#"ˇ"#);
2833
2834 cx.update_editor(|editor, window, cx| {
2835 editor.delete_to_next_word_end(
2836 &DeleteToNextWordEnd {
2837 ignore_newlines: true,
2838 ignore_brackets: false,
2839 },
2840 window,
2841 cx,
2842 );
2843 });
2844 cx.assert_editor_state(r#"ˇ"#);
2845
2846 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2847 cx.update_editor(|editor, window, cx| {
2848 editor.delete_to_previous_word_start(
2849 &DeleteToPreviousWordStart {
2850 ignore_newlines: true,
2851 ignore_brackets: true,
2852 },
2853 window,
2854 cx,
2855 );
2856 });
2857 cx.assert_editor_state(r#"macroˇCOMMENT");"#);
2858}
2859
2860#[gpui::test]
2861fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2862 init_test(cx, |_| {});
2863
2864 let editor = cx.add_window(|window, cx| {
2865 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2866 build_editor(buffer, window, cx)
2867 });
2868 let del_to_prev_word_start = DeleteToPreviousWordStart {
2869 ignore_newlines: false,
2870 ignore_brackets: false,
2871 };
2872 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2873 ignore_newlines: true,
2874 ignore_brackets: false,
2875 };
2876
2877 _ = editor.update(cx, |editor, window, cx| {
2878 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2879 s.select_display_ranges([
2880 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2881 ])
2882 });
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\nthree\n");
2885 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2886 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2887 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2888 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2889 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2890 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
2891 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2892 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
2893 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2894 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2895 });
2896}
2897
2898#[gpui::test]
2899fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2900 init_test(cx, |_| {});
2901
2902 let editor = cx.add_window(|window, cx| {
2903 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2904 build_editor(buffer, window, cx)
2905 });
2906 let del_to_next_word_end = DeleteToNextWordEnd {
2907 ignore_newlines: false,
2908 ignore_brackets: false,
2909 };
2910 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2911 ignore_newlines: true,
2912 ignore_brackets: false,
2913 };
2914
2915 _ = editor.update(cx, |editor, window, cx| {
2916 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2917 s.select_display_ranges([
2918 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2919 ])
2920 });
2921 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2922 assert_eq!(
2923 editor.buffer.read(cx).read(cx).text(),
2924 "one\n two\nthree\n four"
2925 );
2926 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2927 assert_eq!(
2928 editor.buffer.read(cx).read(cx).text(),
2929 "\n two\nthree\n four"
2930 );
2931 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2932 assert_eq!(
2933 editor.buffer.read(cx).read(cx).text(),
2934 "two\nthree\n four"
2935 );
2936 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2937 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2938 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2939 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
2940 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2941 assert_eq!(editor.buffer.read(cx).read(cx).text(), "four");
2942 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2943 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2944 });
2945}
2946
2947#[gpui::test]
2948fn test_newline(cx: &mut TestAppContext) {
2949 init_test(cx, |_| {});
2950
2951 let editor = cx.add_window(|window, cx| {
2952 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2953 build_editor(buffer, window, cx)
2954 });
2955
2956 _ = editor.update(cx, |editor, window, cx| {
2957 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2958 s.select_display_ranges([
2959 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2960 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2961 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2962 ])
2963 });
2964
2965 editor.newline(&Newline, window, cx);
2966 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2967 });
2968}
2969
2970#[gpui::test]
2971fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2972 init_test(cx, |_| {});
2973
2974 let editor = cx.add_window(|window, cx| {
2975 let buffer = MultiBuffer::build_simple(
2976 "
2977 a
2978 b(
2979 X
2980 )
2981 c(
2982 X
2983 )
2984 "
2985 .unindent()
2986 .as_str(),
2987 cx,
2988 );
2989 let mut editor = build_editor(buffer, window, cx);
2990 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2991 s.select_ranges([
2992 Point::new(2, 4)..Point::new(2, 5),
2993 Point::new(5, 4)..Point::new(5, 5),
2994 ])
2995 });
2996 editor
2997 });
2998
2999 _ = editor.update(cx, |editor, window, cx| {
3000 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3001 editor.buffer.update(cx, |buffer, cx| {
3002 buffer.edit(
3003 [
3004 (Point::new(1, 2)..Point::new(3, 0), ""),
3005 (Point::new(4, 2)..Point::new(6, 0), ""),
3006 ],
3007 None,
3008 cx,
3009 );
3010 assert_eq!(
3011 buffer.read(cx).text(),
3012 "
3013 a
3014 b()
3015 c()
3016 "
3017 .unindent()
3018 );
3019 });
3020 assert_eq!(
3021 editor.selections.ranges(cx),
3022 &[
3023 Point::new(1, 2)..Point::new(1, 2),
3024 Point::new(2, 2)..Point::new(2, 2),
3025 ],
3026 );
3027
3028 editor.newline(&Newline, window, cx);
3029 assert_eq!(
3030 editor.text(cx),
3031 "
3032 a
3033 b(
3034 )
3035 c(
3036 )
3037 "
3038 .unindent()
3039 );
3040
3041 // The selections are moved after the inserted newlines
3042 assert_eq!(
3043 editor.selections.ranges(cx),
3044 &[
3045 Point::new(2, 0)..Point::new(2, 0),
3046 Point::new(4, 0)..Point::new(4, 0),
3047 ],
3048 );
3049 });
3050}
3051
3052#[gpui::test]
3053async fn test_newline_above(cx: &mut TestAppContext) {
3054 init_test(cx, |settings| {
3055 settings.defaults.tab_size = NonZeroU32::new(4)
3056 });
3057
3058 let language = Arc::new(
3059 Language::new(
3060 LanguageConfig::default(),
3061 Some(tree_sitter_rust::LANGUAGE.into()),
3062 )
3063 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3064 .unwrap(),
3065 );
3066
3067 let mut cx = EditorTestContext::new(cx).await;
3068 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3069 cx.set_state(indoc! {"
3070 const a: ˇA = (
3071 (ˇ
3072 «const_functionˇ»(ˇ),
3073 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3074 )ˇ
3075 ˇ);ˇ
3076 "});
3077
3078 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
3079 cx.assert_editor_state(indoc! {"
3080 ˇ
3081 const a: A = (
3082 ˇ
3083 (
3084 ˇ
3085 ˇ
3086 const_function(),
3087 ˇ
3088 ˇ
3089 ˇ
3090 ˇ
3091 something_else,
3092 ˇ
3093 )
3094 ˇ
3095 ˇ
3096 );
3097 "});
3098}
3099
3100#[gpui::test]
3101async fn test_newline_below(cx: &mut TestAppContext) {
3102 init_test(cx, |settings| {
3103 settings.defaults.tab_size = NonZeroU32::new(4)
3104 });
3105
3106 let language = Arc::new(
3107 Language::new(
3108 LanguageConfig::default(),
3109 Some(tree_sitter_rust::LANGUAGE.into()),
3110 )
3111 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3112 .unwrap(),
3113 );
3114
3115 let mut cx = EditorTestContext::new(cx).await;
3116 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3117 cx.set_state(indoc! {"
3118 const a: ˇA = (
3119 (ˇ
3120 «const_functionˇ»(ˇ),
3121 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3122 )ˇ
3123 ˇ);ˇ
3124 "});
3125
3126 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
3127 cx.assert_editor_state(indoc! {"
3128 const a: A = (
3129 ˇ
3130 (
3131 ˇ
3132 const_function(),
3133 ˇ
3134 ˇ
3135 something_else,
3136 ˇ
3137 ˇ
3138 ˇ
3139 ˇ
3140 )
3141 ˇ
3142 );
3143 ˇ
3144 ˇ
3145 "});
3146}
3147
3148#[gpui::test]
3149async fn test_newline_comments(cx: &mut TestAppContext) {
3150 init_test(cx, |settings| {
3151 settings.defaults.tab_size = NonZeroU32::new(4)
3152 });
3153
3154 let language = Arc::new(Language::new(
3155 LanguageConfig {
3156 line_comments: vec!["// ".into()],
3157 ..LanguageConfig::default()
3158 },
3159 None,
3160 ));
3161 {
3162 let mut cx = EditorTestContext::new(cx).await;
3163 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3164 cx.set_state(indoc! {"
3165 // Fooˇ
3166 "});
3167
3168 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3169 cx.assert_editor_state(indoc! {"
3170 // Foo
3171 // ˇ
3172 "});
3173 // Ensure that we add comment prefix when existing line contains space
3174 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3175 cx.assert_editor_state(
3176 indoc! {"
3177 // Foo
3178 //s
3179 // ˇ
3180 "}
3181 .replace("s", " ") // s is used as space placeholder to prevent format on save
3182 .as_str(),
3183 );
3184 // Ensure that we add comment prefix when existing line does not contain space
3185 cx.set_state(indoc! {"
3186 // Foo
3187 //ˇ
3188 "});
3189 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3190 cx.assert_editor_state(indoc! {"
3191 // Foo
3192 //
3193 // ˇ
3194 "});
3195 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
3196 cx.set_state(indoc! {"
3197 ˇ// Foo
3198 "});
3199 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3200 cx.assert_editor_state(indoc! {"
3201
3202 ˇ// Foo
3203 "});
3204 }
3205 // Ensure that comment continuations can be disabled.
3206 update_test_language_settings(cx, |settings| {
3207 settings.defaults.extend_comment_on_newline = Some(false);
3208 });
3209 let mut cx = EditorTestContext::new(cx).await;
3210 cx.set_state(indoc! {"
3211 // Fooˇ
3212 "});
3213 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3214 cx.assert_editor_state(indoc! {"
3215 // Foo
3216 ˇ
3217 "});
3218}
3219
3220#[gpui::test]
3221async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
3222 init_test(cx, |settings| {
3223 settings.defaults.tab_size = NonZeroU32::new(4)
3224 });
3225
3226 let language = Arc::new(Language::new(
3227 LanguageConfig {
3228 line_comments: vec!["// ".into(), "/// ".into()],
3229 ..LanguageConfig::default()
3230 },
3231 None,
3232 ));
3233 {
3234 let mut cx = EditorTestContext::new(cx).await;
3235 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3236 cx.set_state(indoc! {"
3237 //ˇ
3238 "});
3239 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3240 cx.assert_editor_state(indoc! {"
3241 //
3242 // ˇ
3243 "});
3244
3245 cx.set_state(indoc! {"
3246 ///ˇ
3247 "});
3248 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3249 cx.assert_editor_state(indoc! {"
3250 ///
3251 /// ˇ
3252 "});
3253 }
3254}
3255
3256#[gpui::test]
3257async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
3258 init_test(cx, |settings| {
3259 settings.defaults.tab_size = NonZeroU32::new(4)
3260 });
3261
3262 let language = Arc::new(
3263 Language::new(
3264 LanguageConfig {
3265 documentation_comment: Some(language::BlockCommentConfig {
3266 start: "/**".into(),
3267 end: "*/".into(),
3268 prefix: "* ".into(),
3269 tab_size: 1,
3270 }),
3271
3272 ..LanguageConfig::default()
3273 },
3274 Some(tree_sitter_rust::LANGUAGE.into()),
3275 )
3276 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
3277 .unwrap(),
3278 );
3279
3280 {
3281 let mut cx = EditorTestContext::new(cx).await;
3282 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3283 cx.set_state(indoc! {"
3284 /**ˇ
3285 "});
3286
3287 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3288 cx.assert_editor_state(indoc! {"
3289 /**
3290 * ˇ
3291 "});
3292 // Ensure that if cursor is before the comment start,
3293 // we do not actually insert a comment prefix.
3294 cx.set_state(indoc! {"
3295 ˇ/**
3296 "});
3297 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3298 cx.assert_editor_state(indoc! {"
3299
3300 ˇ/**
3301 "});
3302 // Ensure that if cursor is between it doesn't add comment prefix.
3303 cx.set_state(indoc! {"
3304 /*ˇ*
3305 "});
3306 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3307 cx.assert_editor_state(indoc! {"
3308 /*
3309 ˇ*
3310 "});
3311 // Ensure that if suffix exists on same line after cursor it adds new line.
3312 cx.set_state(indoc! {"
3313 /**ˇ*/
3314 "});
3315 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3316 cx.assert_editor_state(indoc! {"
3317 /**
3318 * ˇ
3319 */
3320 "});
3321 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3322 cx.set_state(indoc! {"
3323 /**ˇ */
3324 "});
3325 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3326 cx.assert_editor_state(indoc! {"
3327 /**
3328 * ˇ
3329 */
3330 "});
3331 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3332 cx.set_state(indoc! {"
3333 /** ˇ*/
3334 "});
3335 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3336 cx.assert_editor_state(
3337 indoc! {"
3338 /**s
3339 * ˇ
3340 */
3341 "}
3342 .replace("s", " ") // s is used as space placeholder to prevent format on save
3343 .as_str(),
3344 );
3345 // Ensure that delimiter space is preserved when newline on already
3346 // spaced delimiter.
3347 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3348 cx.assert_editor_state(
3349 indoc! {"
3350 /**s
3351 *s
3352 * ˇ
3353 */
3354 "}
3355 .replace("s", " ") // s is used as space placeholder to prevent format on save
3356 .as_str(),
3357 );
3358 // Ensure that delimiter space is preserved when space is not
3359 // on existing delimiter.
3360 cx.set_state(indoc! {"
3361 /**
3362 *ˇ
3363 */
3364 "});
3365 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3366 cx.assert_editor_state(indoc! {"
3367 /**
3368 *
3369 * ˇ
3370 */
3371 "});
3372 // Ensure that if suffix exists on same line after cursor it
3373 // doesn't add extra new line if prefix is not on same line.
3374 cx.set_state(indoc! {"
3375 /**
3376 ˇ*/
3377 "});
3378 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3379 cx.assert_editor_state(indoc! {"
3380 /**
3381
3382 ˇ*/
3383 "});
3384 // Ensure that it detects suffix after existing prefix.
3385 cx.set_state(indoc! {"
3386 /**ˇ/
3387 "});
3388 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3389 cx.assert_editor_state(indoc! {"
3390 /**
3391 ˇ/
3392 "});
3393 // Ensure that if suffix exists on same line before
3394 // cursor it does not add comment prefix.
3395 cx.set_state(indoc! {"
3396 /** */ˇ
3397 "});
3398 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3399 cx.assert_editor_state(indoc! {"
3400 /** */
3401 ˇ
3402 "});
3403 // Ensure that if suffix exists on same line before
3404 // cursor it does not add comment prefix.
3405 cx.set_state(indoc! {"
3406 /**
3407 *
3408 */ˇ
3409 "});
3410 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3411 cx.assert_editor_state(indoc! {"
3412 /**
3413 *
3414 */
3415 ˇ
3416 "});
3417
3418 // Ensure that inline comment followed by code
3419 // doesn't add comment prefix on newline
3420 cx.set_state(indoc! {"
3421 /** */ textˇ
3422 "});
3423 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3424 cx.assert_editor_state(indoc! {"
3425 /** */ text
3426 ˇ
3427 "});
3428
3429 // Ensure that text after comment end tag
3430 // doesn't add comment prefix on newline
3431 cx.set_state(indoc! {"
3432 /**
3433 *
3434 */ˇtext
3435 "});
3436 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3437 cx.assert_editor_state(indoc! {"
3438 /**
3439 *
3440 */
3441 ˇtext
3442 "});
3443
3444 // Ensure if not comment block it doesn't
3445 // add comment prefix on newline
3446 cx.set_state(indoc! {"
3447 * textˇ
3448 "});
3449 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3450 cx.assert_editor_state(indoc! {"
3451 * text
3452 ˇ
3453 "});
3454 }
3455 // Ensure that comment continuations can be disabled.
3456 update_test_language_settings(cx, |settings| {
3457 settings.defaults.extend_comment_on_newline = Some(false);
3458 });
3459 let mut cx = EditorTestContext::new(cx).await;
3460 cx.set_state(indoc! {"
3461 /**ˇ
3462 "});
3463 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3464 cx.assert_editor_state(indoc! {"
3465 /**
3466 ˇ
3467 "});
3468}
3469
3470#[gpui::test]
3471async fn test_newline_comments_with_block_comment(cx: &mut TestAppContext) {
3472 init_test(cx, |settings| {
3473 settings.defaults.tab_size = NonZeroU32::new(4)
3474 });
3475
3476 let lua_language = Arc::new(Language::new(
3477 LanguageConfig {
3478 line_comments: vec!["--".into()],
3479 block_comment: Some(language::BlockCommentConfig {
3480 start: "--[[".into(),
3481 prefix: "".into(),
3482 end: "]]".into(),
3483 tab_size: 0,
3484 }),
3485 ..LanguageConfig::default()
3486 },
3487 None,
3488 ));
3489
3490 let mut cx = EditorTestContext::new(cx).await;
3491 cx.update_buffer(|buffer, cx| buffer.set_language(Some(lua_language), cx));
3492
3493 // Line with line comment should extend
3494 cx.set_state(indoc! {"
3495 --ˇ
3496 "});
3497 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3498 cx.assert_editor_state(indoc! {"
3499 --
3500 --ˇ
3501 "});
3502
3503 // Line with block comment that matches line comment should not extend
3504 cx.set_state(indoc! {"
3505 --[[ˇ
3506 "});
3507 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3508 cx.assert_editor_state(indoc! {"
3509 --[[
3510 ˇ
3511 "});
3512}
3513
3514#[gpui::test]
3515fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3516 init_test(cx, |_| {});
3517
3518 let editor = cx.add_window(|window, cx| {
3519 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3520 let mut editor = build_editor(buffer, window, cx);
3521 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3522 s.select_ranges([3..4, 11..12, 19..20])
3523 });
3524 editor
3525 });
3526
3527 _ = editor.update(cx, |editor, window, cx| {
3528 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3529 editor.buffer.update(cx, |buffer, cx| {
3530 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3531 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3532 });
3533 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
3534
3535 editor.insert("Z", window, cx);
3536 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3537
3538 // The selections are moved after the inserted characters
3539 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
3540 });
3541}
3542
3543#[gpui::test]
3544async fn test_tab(cx: &mut TestAppContext) {
3545 init_test(cx, |settings| {
3546 settings.defaults.tab_size = NonZeroU32::new(3)
3547 });
3548
3549 let mut cx = EditorTestContext::new(cx).await;
3550 cx.set_state(indoc! {"
3551 ˇabˇc
3552 ˇ🏀ˇ🏀ˇefg
3553 dˇ
3554 "});
3555 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3556 cx.assert_editor_state(indoc! {"
3557 ˇab ˇc
3558 ˇ🏀 ˇ🏀 ˇefg
3559 d ˇ
3560 "});
3561
3562 cx.set_state(indoc! {"
3563 a
3564 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3565 "});
3566 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3567 cx.assert_editor_state(indoc! {"
3568 a
3569 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3570 "});
3571}
3572
3573#[gpui::test]
3574async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3575 init_test(cx, |_| {});
3576
3577 let mut cx = EditorTestContext::new(cx).await;
3578 let language = Arc::new(
3579 Language::new(
3580 LanguageConfig::default(),
3581 Some(tree_sitter_rust::LANGUAGE.into()),
3582 )
3583 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3584 .unwrap(),
3585 );
3586 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3587
3588 // test when all cursors are not at suggested indent
3589 // then simply move to their suggested indent location
3590 cx.set_state(indoc! {"
3591 const a: B = (
3592 c(
3593 ˇ
3594 ˇ )
3595 );
3596 "});
3597 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3598 cx.assert_editor_state(indoc! {"
3599 const a: B = (
3600 c(
3601 ˇ
3602 ˇ)
3603 );
3604 "});
3605
3606 // test cursor already at suggested indent not moving when
3607 // other cursors are yet to reach their suggested indents
3608 cx.set_state(indoc! {"
3609 ˇ
3610 const a: B = (
3611 c(
3612 d(
3613 ˇ
3614 )
3615 ˇ
3616 ˇ )
3617 );
3618 "});
3619 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3620 cx.assert_editor_state(indoc! {"
3621 ˇ
3622 const a: B = (
3623 c(
3624 d(
3625 ˇ
3626 )
3627 ˇ
3628 ˇ)
3629 );
3630 "});
3631 // test when all cursors are at suggested indent then tab is inserted
3632 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3633 cx.assert_editor_state(indoc! {"
3634 ˇ
3635 const a: B = (
3636 c(
3637 d(
3638 ˇ
3639 )
3640 ˇ
3641 ˇ)
3642 );
3643 "});
3644
3645 // test when current indent is less than suggested indent,
3646 // we adjust line to match suggested indent and move cursor to it
3647 //
3648 // when no other cursor is at word boundary, all of them should move
3649 cx.set_state(indoc! {"
3650 const a: B = (
3651 c(
3652 d(
3653 ˇ
3654 ˇ )
3655 ˇ )
3656 );
3657 "});
3658 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3659 cx.assert_editor_state(indoc! {"
3660 const a: B = (
3661 c(
3662 d(
3663 ˇ
3664 ˇ)
3665 ˇ)
3666 );
3667 "});
3668
3669 // test when current indent is less than suggested indent,
3670 // we adjust line to match suggested indent and move cursor to it
3671 //
3672 // when some other cursor is at word boundary, it should not move
3673 cx.set_state(indoc! {"
3674 const a: B = (
3675 c(
3676 d(
3677 ˇ
3678 ˇ )
3679 ˇ)
3680 );
3681 "});
3682 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3683 cx.assert_editor_state(indoc! {"
3684 const a: B = (
3685 c(
3686 d(
3687 ˇ
3688 ˇ)
3689 ˇ)
3690 );
3691 "});
3692
3693 // test when current indent is more than suggested indent,
3694 // we just move cursor to current indent instead of suggested indent
3695 //
3696 // when no other cursor is at word boundary, all of them should move
3697 cx.set_state(indoc! {"
3698 const a: B = (
3699 c(
3700 d(
3701 ˇ
3702 ˇ )
3703 ˇ )
3704 );
3705 "});
3706 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3707 cx.assert_editor_state(indoc! {"
3708 const a: B = (
3709 c(
3710 d(
3711 ˇ
3712 ˇ)
3713 ˇ)
3714 );
3715 "});
3716 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3717 cx.assert_editor_state(indoc! {"
3718 const a: B = (
3719 c(
3720 d(
3721 ˇ
3722 ˇ)
3723 ˇ)
3724 );
3725 "});
3726
3727 // test when current indent is more than suggested indent,
3728 // we just move cursor to current indent instead of suggested indent
3729 //
3730 // when some other cursor is at word boundary, it doesn't move
3731 cx.set_state(indoc! {"
3732 const a: B = (
3733 c(
3734 d(
3735 ˇ
3736 ˇ )
3737 ˇ)
3738 );
3739 "});
3740 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3741 cx.assert_editor_state(indoc! {"
3742 const a: B = (
3743 c(
3744 d(
3745 ˇ
3746 ˇ)
3747 ˇ)
3748 );
3749 "});
3750
3751 // handle auto-indent when there are multiple cursors on the same line
3752 cx.set_state(indoc! {"
3753 const a: B = (
3754 c(
3755 ˇ ˇ
3756 ˇ )
3757 );
3758 "});
3759 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3760 cx.assert_editor_state(indoc! {"
3761 const a: B = (
3762 c(
3763 ˇ
3764 ˇ)
3765 );
3766 "});
3767}
3768
3769#[gpui::test]
3770async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3771 init_test(cx, |settings| {
3772 settings.defaults.tab_size = NonZeroU32::new(3)
3773 });
3774
3775 let mut cx = EditorTestContext::new(cx).await;
3776 cx.set_state(indoc! {"
3777 ˇ
3778 \t ˇ
3779 \t ˇ
3780 \t ˇ
3781 \t \t\t \t \t\t \t\t \t \t ˇ
3782 "});
3783
3784 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3785 cx.assert_editor_state(indoc! {"
3786 ˇ
3787 \t ˇ
3788 \t ˇ
3789 \t ˇ
3790 \t \t\t \t \t\t \t\t \t \t ˇ
3791 "});
3792}
3793
3794#[gpui::test]
3795async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3796 init_test(cx, |settings| {
3797 settings.defaults.tab_size = NonZeroU32::new(4)
3798 });
3799
3800 let language = Arc::new(
3801 Language::new(
3802 LanguageConfig::default(),
3803 Some(tree_sitter_rust::LANGUAGE.into()),
3804 )
3805 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3806 .unwrap(),
3807 );
3808
3809 let mut cx = EditorTestContext::new(cx).await;
3810 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3811 cx.set_state(indoc! {"
3812 fn a() {
3813 if b {
3814 \t ˇc
3815 }
3816 }
3817 "});
3818
3819 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3820 cx.assert_editor_state(indoc! {"
3821 fn a() {
3822 if b {
3823 ˇc
3824 }
3825 }
3826 "});
3827}
3828
3829#[gpui::test]
3830async fn test_indent_outdent(cx: &mut TestAppContext) {
3831 init_test(cx, |settings| {
3832 settings.defaults.tab_size = NonZeroU32::new(4);
3833 });
3834
3835 let mut cx = EditorTestContext::new(cx).await;
3836
3837 cx.set_state(indoc! {"
3838 «oneˇ» «twoˇ»
3839 three
3840 four
3841 "});
3842 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3843 cx.assert_editor_state(indoc! {"
3844 «oneˇ» «twoˇ»
3845 three
3846 four
3847 "});
3848
3849 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3850 cx.assert_editor_state(indoc! {"
3851 «oneˇ» «twoˇ»
3852 three
3853 four
3854 "});
3855
3856 // select across line ending
3857 cx.set_state(indoc! {"
3858 one two
3859 t«hree
3860 ˇ» four
3861 "});
3862 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3863 cx.assert_editor_state(indoc! {"
3864 one two
3865 t«hree
3866 ˇ» four
3867 "});
3868
3869 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3870 cx.assert_editor_state(indoc! {"
3871 one two
3872 t«hree
3873 ˇ» four
3874 "});
3875
3876 // Ensure that indenting/outdenting works when the cursor is at column 0.
3877 cx.set_state(indoc! {"
3878 one two
3879 ˇthree
3880 four
3881 "});
3882 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3883 cx.assert_editor_state(indoc! {"
3884 one two
3885 ˇthree
3886 four
3887 "});
3888
3889 cx.set_state(indoc! {"
3890 one two
3891 ˇ three
3892 four
3893 "});
3894 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3895 cx.assert_editor_state(indoc! {"
3896 one two
3897 ˇthree
3898 four
3899 "});
3900}
3901
3902#[gpui::test]
3903async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) {
3904 // This is a regression test for issue #33761
3905 init_test(cx, |_| {});
3906
3907 let mut cx = EditorTestContext::new(cx).await;
3908 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3909 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3910
3911 cx.set_state(
3912 r#"ˇ# ingress:
3913ˇ# api:
3914ˇ# enabled: false
3915ˇ# pathType: Prefix
3916ˇ# console:
3917ˇ# enabled: false
3918ˇ# pathType: Prefix
3919"#,
3920 );
3921
3922 // Press tab to indent all lines
3923 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3924
3925 cx.assert_editor_state(
3926 r#" ˇ# ingress:
3927 ˇ# api:
3928 ˇ# enabled: false
3929 ˇ# pathType: Prefix
3930 ˇ# console:
3931 ˇ# enabled: false
3932 ˇ# pathType: Prefix
3933"#,
3934 );
3935}
3936
3937#[gpui::test]
3938async fn test_indent_yaml_non_comments_with_multiple_cursors(cx: &mut TestAppContext) {
3939 // This is a test to make sure our fix for issue #33761 didn't break anything
3940 init_test(cx, |_| {});
3941
3942 let mut cx = EditorTestContext::new(cx).await;
3943 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3944 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3945
3946 cx.set_state(
3947 r#"ˇingress:
3948ˇ api:
3949ˇ enabled: false
3950ˇ pathType: Prefix
3951"#,
3952 );
3953
3954 // Press tab to indent all lines
3955 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3956
3957 cx.assert_editor_state(
3958 r#"ˇingress:
3959 ˇapi:
3960 ˇenabled: false
3961 ˇpathType: Prefix
3962"#,
3963 );
3964}
3965
3966#[gpui::test]
3967async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3968 init_test(cx, |settings| {
3969 settings.defaults.hard_tabs = Some(true);
3970 });
3971
3972 let mut cx = EditorTestContext::new(cx).await;
3973
3974 // select two ranges on one line
3975 cx.set_state(indoc! {"
3976 «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«oneˇ» «twoˇ»
3983 three
3984 four
3985 "});
3986 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3987 cx.assert_editor_state(indoc! {"
3988 \t\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 \t«oneˇ» «twoˇ»
3995 three
3996 four
3997 "});
3998 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3999 cx.assert_editor_state(indoc! {"
4000 «oneˇ» «twoˇ»
4001 three
4002 four
4003 "});
4004
4005 // select across a line ending
4006 cx.set_state(indoc! {"
4007 one two
4008 t«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 \tt«hree
4015 ˇ»four
4016 "});
4017 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4018 cx.assert_editor_state(indoc! {"
4019 one two
4020 \t\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 \tt«hree
4027 ˇ»four
4028 "});
4029 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4030 cx.assert_editor_state(indoc! {"
4031 one two
4032 t«hree
4033 ˇ»four
4034 "});
4035
4036 // Ensure that indenting/outdenting works when the cursor is at column 0.
4037 cx.set_state(indoc! {"
4038 one two
4039 ˇthree
4040 four
4041 "});
4042 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4043 cx.assert_editor_state(indoc! {"
4044 one two
4045 ˇthree
4046 four
4047 "});
4048 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4049 cx.assert_editor_state(indoc! {"
4050 one two
4051 \tˇthree
4052 four
4053 "});
4054 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4055 cx.assert_editor_state(indoc! {"
4056 one two
4057 ˇthree
4058 four
4059 "});
4060}
4061
4062#[gpui::test]
4063fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
4064 init_test(cx, |settings| {
4065 settings.languages.0.extend([
4066 (
4067 "TOML".into(),
4068 LanguageSettingsContent {
4069 tab_size: NonZeroU32::new(2),
4070 ..Default::default()
4071 },
4072 ),
4073 (
4074 "Rust".into(),
4075 LanguageSettingsContent {
4076 tab_size: NonZeroU32::new(4),
4077 ..Default::default()
4078 },
4079 ),
4080 ]);
4081 });
4082
4083 let toml_language = Arc::new(Language::new(
4084 LanguageConfig {
4085 name: "TOML".into(),
4086 ..Default::default()
4087 },
4088 None,
4089 ));
4090 let rust_language = Arc::new(Language::new(
4091 LanguageConfig {
4092 name: "Rust".into(),
4093 ..Default::default()
4094 },
4095 None,
4096 ));
4097
4098 let toml_buffer =
4099 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
4100 let rust_buffer =
4101 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
4102 let multibuffer = cx.new(|cx| {
4103 let mut multibuffer = MultiBuffer::new(ReadWrite);
4104 multibuffer.push_excerpts(
4105 toml_buffer.clone(),
4106 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
4107 cx,
4108 );
4109 multibuffer.push_excerpts(
4110 rust_buffer.clone(),
4111 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
4112 cx,
4113 );
4114 multibuffer
4115 });
4116
4117 cx.add_window(|window, cx| {
4118 let mut editor = build_editor(multibuffer, window, cx);
4119
4120 assert_eq!(
4121 editor.text(cx),
4122 indoc! {"
4123 a = 1
4124 b = 2
4125
4126 const c: usize = 3;
4127 "}
4128 );
4129
4130 select_ranges(
4131 &mut editor,
4132 indoc! {"
4133 «aˇ» = 1
4134 b = 2
4135
4136 «const c:ˇ» usize = 3;
4137 "},
4138 window,
4139 cx,
4140 );
4141
4142 editor.tab(&Tab, window, cx);
4143 assert_text_with_selections(
4144 &mut editor,
4145 indoc! {"
4146 «aˇ» = 1
4147 b = 2
4148
4149 «const c:ˇ» usize = 3;
4150 "},
4151 cx,
4152 );
4153 editor.backtab(&Backtab, window, cx);
4154 assert_text_with_selections(
4155 &mut editor,
4156 indoc! {"
4157 «aˇ» = 1
4158 b = 2
4159
4160 «const c:ˇ» usize = 3;
4161 "},
4162 cx,
4163 );
4164
4165 editor
4166 });
4167}
4168
4169#[gpui::test]
4170async fn test_backspace(cx: &mut TestAppContext) {
4171 init_test(cx, |_| {});
4172
4173 let mut cx = EditorTestContext::new(cx).await;
4174
4175 // Basic backspace
4176 cx.set_state(indoc! {"
4177 onˇe two three
4178 fou«rˇ» five six
4179 seven «ˇeight nine
4180 »ten
4181 "});
4182 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4183 cx.assert_editor_state(indoc! {"
4184 oˇe two three
4185 fouˇ five six
4186 seven ˇten
4187 "});
4188
4189 // Test backspace inside and around indents
4190 cx.set_state(indoc! {"
4191 zero
4192 ˇone
4193 ˇtwo
4194 ˇ ˇ ˇ three
4195 ˇ ˇ four
4196 "});
4197 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4198 cx.assert_editor_state(indoc! {"
4199 zero
4200 ˇone
4201 ˇtwo
4202 ˇ threeˇ four
4203 "});
4204}
4205
4206#[gpui::test]
4207async fn test_delete(cx: &mut TestAppContext) {
4208 init_test(cx, |_| {});
4209
4210 let mut cx = EditorTestContext::new(cx).await;
4211 cx.set_state(indoc! {"
4212 onˇe two three
4213 fou«rˇ» five six
4214 seven «ˇeight nine
4215 »ten
4216 "});
4217 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
4218 cx.assert_editor_state(indoc! {"
4219 onˇ two three
4220 fouˇ five six
4221 seven ˇten
4222 "});
4223}
4224
4225#[gpui::test]
4226fn test_delete_line(cx: &mut TestAppContext) {
4227 init_test(cx, |_| {});
4228
4229 let editor = cx.add_window(|window, cx| {
4230 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4231 build_editor(buffer, window, cx)
4232 });
4233 _ = editor.update(cx, |editor, window, cx| {
4234 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4235 s.select_display_ranges([
4236 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4237 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4238 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4239 ])
4240 });
4241 editor.delete_line(&DeleteLine, window, cx);
4242 assert_eq!(editor.display_text(cx), "ghi");
4243 assert_eq!(
4244 editor.selections.display_ranges(cx),
4245 vec![
4246 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
4247 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
4248 ]
4249 );
4250 });
4251
4252 let editor = cx.add_window(|window, cx| {
4253 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4254 build_editor(buffer, window, cx)
4255 });
4256 _ = editor.update(cx, |editor, window, cx| {
4257 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4258 s.select_display_ranges([
4259 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
4260 ])
4261 });
4262 editor.delete_line(&DeleteLine, window, cx);
4263 assert_eq!(editor.display_text(cx), "ghi\n");
4264 assert_eq!(
4265 editor.selections.display_ranges(cx),
4266 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
4267 );
4268 });
4269}
4270
4271#[gpui::test]
4272fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
4273 init_test(cx, |_| {});
4274
4275 cx.add_window(|window, cx| {
4276 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4277 let mut editor = build_editor(buffer.clone(), window, cx);
4278 let buffer = buffer.read(cx).as_singleton().unwrap();
4279
4280 assert_eq!(
4281 editor.selections.ranges::<Point>(cx),
4282 &[Point::new(0, 0)..Point::new(0, 0)]
4283 );
4284
4285 // When on single line, replace newline at end by space
4286 editor.join_lines(&JoinLines, window, cx);
4287 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4288 assert_eq!(
4289 editor.selections.ranges::<Point>(cx),
4290 &[Point::new(0, 3)..Point::new(0, 3)]
4291 );
4292
4293 // When multiple lines are selected, remove newlines that are spanned by the selection
4294 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4295 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
4296 });
4297 editor.join_lines(&JoinLines, window, cx);
4298 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
4299 assert_eq!(
4300 editor.selections.ranges::<Point>(cx),
4301 &[Point::new(0, 11)..Point::new(0, 11)]
4302 );
4303
4304 // Undo should be transactional
4305 editor.undo(&Undo, window, cx);
4306 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4307 assert_eq!(
4308 editor.selections.ranges::<Point>(cx),
4309 &[Point::new(0, 5)..Point::new(2, 2)]
4310 );
4311
4312 // When joining an empty line don't insert a space
4313 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4314 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
4315 });
4316 editor.join_lines(&JoinLines, window, cx);
4317 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
4318 assert_eq!(
4319 editor.selections.ranges::<Point>(cx),
4320 [Point::new(2, 3)..Point::new(2, 3)]
4321 );
4322
4323 // We can remove trailing newlines
4324 editor.join_lines(&JoinLines, window, cx);
4325 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4326 assert_eq!(
4327 editor.selections.ranges::<Point>(cx),
4328 [Point::new(2, 3)..Point::new(2, 3)]
4329 );
4330
4331 // We don't blow up on the last line
4332 editor.join_lines(&JoinLines, window, cx);
4333 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4334 assert_eq!(
4335 editor.selections.ranges::<Point>(cx),
4336 [Point::new(2, 3)..Point::new(2, 3)]
4337 );
4338
4339 // reset to test indentation
4340 editor.buffer.update(cx, |buffer, cx| {
4341 buffer.edit(
4342 [
4343 (Point::new(1, 0)..Point::new(1, 2), " "),
4344 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
4345 ],
4346 None,
4347 cx,
4348 )
4349 });
4350
4351 // We remove any leading spaces
4352 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
4353 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4354 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
4355 });
4356 editor.join_lines(&JoinLines, window, cx);
4357 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
4358
4359 // We don't insert a space for a line containing only spaces
4360 editor.join_lines(&JoinLines, window, cx);
4361 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
4362
4363 // We ignore any leading tabs
4364 editor.join_lines(&JoinLines, window, cx);
4365 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
4366
4367 editor
4368 });
4369}
4370
4371#[gpui::test]
4372fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
4373 init_test(cx, |_| {});
4374
4375 cx.add_window(|window, cx| {
4376 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4377 let mut editor = build_editor(buffer.clone(), window, cx);
4378 let buffer = buffer.read(cx).as_singleton().unwrap();
4379
4380 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4381 s.select_ranges([
4382 Point::new(0, 2)..Point::new(1, 1),
4383 Point::new(1, 2)..Point::new(1, 2),
4384 Point::new(3, 1)..Point::new(3, 2),
4385 ])
4386 });
4387
4388 editor.join_lines(&JoinLines, window, cx);
4389 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
4390
4391 assert_eq!(
4392 editor.selections.ranges::<Point>(cx),
4393 [
4394 Point::new(0, 7)..Point::new(0, 7),
4395 Point::new(1, 3)..Point::new(1, 3)
4396 ]
4397 );
4398 editor
4399 });
4400}
4401
4402#[gpui::test]
4403async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
4404 init_test(cx, |_| {});
4405
4406 let mut cx = EditorTestContext::new(cx).await;
4407
4408 let diff_base = r#"
4409 Line 0
4410 Line 1
4411 Line 2
4412 Line 3
4413 "#
4414 .unindent();
4415
4416 cx.set_state(
4417 &r#"
4418 ˇLine 0
4419 Line 1
4420 Line 2
4421 Line 3
4422 "#
4423 .unindent(),
4424 );
4425
4426 cx.set_head_text(&diff_base);
4427 executor.run_until_parked();
4428
4429 // Join lines
4430 cx.update_editor(|editor, window, cx| {
4431 editor.join_lines(&JoinLines, window, cx);
4432 });
4433 executor.run_until_parked();
4434
4435 cx.assert_editor_state(
4436 &r#"
4437 Line 0ˇ Line 1
4438 Line 2
4439 Line 3
4440 "#
4441 .unindent(),
4442 );
4443 // Join again
4444 cx.update_editor(|editor, window, cx| {
4445 editor.join_lines(&JoinLines, window, cx);
4446 });
4447 executor.run_until_parked();
4448
4449 cx.assert_editor_state(
4450 &r#"
4451 Line 0 Line 1ˇ Line 2
4452 Line 3
4453 "#
4454 .unindent(),
4455 );
4456}
4457
4458#[gpui::test]
4459async fn test_custom_newlines_cause_no_false_positive_diffs(
4460 executor: BackgroundExecutor,
4461 cx: &mut TestAppContext,
4462) {
4463 init_test(cx, |_| {});
4464 let mut cx = EditorTestContext::new(cx).await;
4465 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
4466 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
4467 executor.run_until_parked();
4468
4469 cx.update_editor(|editor, window, cx| {
4470 let snapshot = editor.snapshot(window, cx);
4471 assert_eq!(
4472 snapshot
4473 .buffer_snapshot
4474 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
4475 .collect::<Vec<_>>(),
4476 Vec::new(),
4477 "Should not have any diffs for files with custom newlines"
4478 );
4479 });
4480}
4481
4482#[gpui::test]
4483async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
4484 init_test(cx, |_| {});
4485
4486 let mut cx = EditorTestContext::new(cx).await;
4487
4488 // Test sort_lines_case_insensitive()
4489 cx.set_state(indoc! {"
4490 «z
4491 y
4492 x
4493 Z
4494 Y
4495 Xˇ»
4496 "});
4497 cx.update_editor(|e, window, cx| {
4498 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
4499 });
4500 cx.assert_editor_state(indoc! {"
4501 «x
4502 X
4503 y
4504 Y
4505 z
4506 Zˇ»
4507 "});
4508
4509 // Test sort_lines_by_length()
4510 //
4511 // Demonstrates:
4512 // - ∞ is 3 bytes UTF-8, but sorted by its char count (1)
4513 // - sort is stable
4514 cx.set_state(indoc! {"
4515 «123
4516 æ
4517 12
4518 ∞
4519 1
4520 æˇ»
4521 "});
4522 cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
4523 cx.assert_editor_state(indoc! {"
4524 «æ
4525 ∞
4526 1
4527 æ
4528 12
4529 123ˇ»
4530 "});
4531
4532 // Test reverse_lines()
4533 cx.set_state(indoc! {"
4534 «5
4535 4
4536 3
4537 2
4538 1ˇ»
4539 "});
4540 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4541 cx.assert_editor_state(indoc! {"
4542 «1
4543 2
4544 3
4545 4
4546 5ˇ»
4547 "});
4548
4549 // Skip testing shuffle_line()
4550
4551 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4552 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4553
4554 // Don't manipulate when cursor is on single line, but expand the selection
4555 cx.set_state(indoc! {"
4556 ddˇdd
4557 ccc
4558 bb
4559 a
4560 "});
4561 cx.update_editor(|e, window, cx| {
4562 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4563 });
4564 cx.assert_editor_state(indoc! {"
4565 «ddddˇ»
4566 ccc
4567 bb
4568 a
4569 "});
4570
4571 // Basic manipulate case
4572 // Start selection moves to column 0
4573 // End of selection shrinks to fit shorter line
4574 cx.set_state(indoc! {"
4575 dd«d
4576 ccc
4577 bb
4578 aaaaaˇ»
4579 "});
4580 cx.update_editor(|e, window, cx| {
4581 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4582 });
4583 cx.assert_editor_state(indoc! {"
4584 «aaaaa
4585 bb
4586 ccc
4587 dddˇ»
4588 "});
4589
4590 // Manipulate case with newlines
4591 cx.set_state(indoc! {"
4592 dd«d
4593 ccc
4594
4595 bb
4596 aaaaa
4597
4598 ˇ»
4599 "});
4600 cx.update_editor(|e, window, cx| {
4601 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4602 });
4603 cx.assert_editor_state(indoc! {"
4604 «
4605
4606 aaaaa
4607 bb
4608 ccc
4609 dddˇ»
4610
4611 "});
4612
4613 // Adding new line
4614 cx.set_state(indoc! {"
4615 aa«a
4616 bbˇ»b
4617 "});
4618 cx.update_editor(|e, window, cx| {
4619 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4620 });
4621 cx.assert_editor_state(indoc! {"
4622 «aaa
4623 bbb
4624 added_lineˇ»
4625 "});
4626
4627 // Removing line
4628 cx.set_state(indoc! {"
4629 aa«a
4630 bbbˇ»
4631 "});
4632 cx.update_editor(|e, window, cx| {
4633 e.manipulate_immutable_lines(window, cx, |lines| {
4634 lines.pop();
4635 })
4636 });
4637 cx.assert_editor_state(indoc! {"
4638 «aaaˇ»
4639 "});
4640
4641 // Removing all lines
4642 cx.set_state(indoc! {"
4643 aa«a
4644 bbbˇ»
4645 "});
4646 cx.update_editor(|e, window, cx| {
4647 e.manipulate_immutable_lines(window, cx, |lines| {
4648 lines.drain(..);
4649 })
4650 });
4651 cx.assert_editor_state(indoc! {"
4652 ˇ
4653 "});
4654}
4655
4656#[gpui::test]
4657async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4658 init_test(cx, |_| {});
4659
4660 let mut cx = EditorTestContext::new(cx).await;
4661
4662 // Consider continuous selection as single selection
4663 cx.set_state(indoc! {"
4664 Aaa«aa
4665 cˇ»c«c
4666 bb
4667 aaaˇ»aa
4668 "});
4669 cx.update_editor(|e, window, cx| {
4670 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4671 });
4672 cx.assert_editor_state(indoc! {"
4673 «Aaaaa
4674 ccc
4675 bb
4676 aaaaaˇ»
4677 "});
4678
4679 cx.set_state(indoc! {"
4680 Aaa«aa
4681 cˇ»c«c
4682 bb
4683 aaaˇ»aa
4684 "});
4685 cx.update_editor(|e, window, cx| {
4686 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4687 });
4688 cx.assert_editor_state(indoc! {"
4689 «Aaaaa
4690 ccc
4691 bbˇ»
4692 "});
4693
4694 // Consider non continuous selection as distinct dedup operations
4695 cx.set_state(indoc! {"
4696 «aaaaa
4697 bb
4698 aaaaa
4699 aaaaaˇ»
4700
4701 aaa«aaˇ»
4702 "});
4703 cx.update_editor(|e, window, cx| {
4704 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4705 });
4706 cx.assert_editor_state(indoc! {"
4707 «aaaaa
4708 bbˇ»
4709
4710 «aaaaaˇ»
4711 "});
4712}
4713
4714#[gpui::test]
4715async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4716 init_test(cx, |_| {});
4717
4718 let mut cx = EditorTestContext::new(cx).await;
4719
4720 cx.set_state(indoc! {"
4721 «Aaa
4722 aAa
4723 Aaaˇ»
4724 "});
4725 cx.update_editor(|e, window, cx| {
4726 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4727 });
4728 cx.assert_editor_state(indoc! {"
4729 «Aaa
4730 aAaˇ»
4731 "});
4732
4733 cx.set_state(indoc! {"
4734 «Aaa
4735 aAa
4736 aaAˇ»
4737 "});
4738 cx.update_editor(|e, window, cx| {
4739 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4740 });
4741 cx.assert_editor_state(indoc! {"
4742 «Aaaˇ»
4743 "});
4744}
4745
4746#[gpui::test]
4747async fn test_wrap_in_tag_single_selection(cx: &mut TestAppContext) {
4748 init_test(cx, |_| {});
4749
4750 let mut cx = EditorTestContext::new(cx).await;
4751
4752 let js_language = Arc::new(Language::new(
4753 LanguageConfig {
4754 name: "JavaScript".into(),
4755 wrap_characters: Some(language::WrapCharactersConfig {
4756 start_prefix: "<".into(),
4757 start_suffix: ">".into(),
4758 end_prefix: "</".into(),
4759 end_suffix: ">".into(),
4760 }),
4761 ..LanguageConfig::default()
4762 },
4763 None,
4764 ));
4765
4766 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
4767
4768 cx.set_state(indoc! {"
4769 «testˇ»
4770 "});
4771 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4772 cx.assert_editor_state(indoc! {"
4773 <«ˇ»>test</«ˇ»>
4774 "});
4775
4776 cx.set_state(indoc! {"
4777 «test
4778 testˇ»
4779 "});
4780 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4781 cx.assert_editor_state(indoc! {"
4782 <«ˇ»>test
4783 test</«ˇ»>
4784 "});
4785
4786 cx.set_state(indoc! {"
4787 teˇst
4788 "});
4789 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4790 cx.assert_editor_state(indoc! {"
4791 te<«ˇ»></«ˇ»>st
4792 "});
4793}
4794
4795#[gpui::test]
4796async fn test_wrap_in_tag_multi_selection(cx: &mut TestAppContext) {
4797 init_test(cx, |_| {});
4798
4799 let mut cx = EditorTestContext::new(cx).await;
4800
4801 let js_language = Arc::new(Language::new(
4802 LanguageConfig {
4803 name: "JavaScript".into(),
4804 wrap_characters: Some(language::WrapCharactersConfig {
4805 start_prefix: "<".into(),
4806 start_suffix: ">".into(),
4807 end_prefix: "</".into(),
4808 end_suffix: ">".into(),
4809 }),
4810 ..LanguageConfig::default()
4811 },
4812 None,
4813 ));
4814
4815 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
4816
4817 cx.set_state(indoc! {"
4818 «testˇ»
4819 «testˇ» «testˇ»
4820 «testˇ»
4821 "});
4822 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4823 cx.assert_editor_state(indoc! {"
4824 <«ˇ»>test</«ˇ»>
4825 <«ˇ»>test</«ˇ»> <«ˇ»>test</«ˇ»>
4826 <«ˇ»>test</«ˇ»>
4827 "});
4828
4829 cx.set_state(indoc! {"
4830 «test
4831 testˇ»
4832 «test
4833 testˇ»
4834 "});
4835 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4836 cx.assert_editor_state(indoc! {"
4837 <«ˇ»>test
4838 test</«ˇ»>
4839 <«ˇ»>test
4840 test</«ˇ»>
4841 "});
4842}
4843
4844#[gpui::test]
4845async fn test_wrap_in_tag_does_nothing_in_unsupported_languages(cx: &mut TestAppContext) {
4846 init_test(cx, |_| {});
4847
4848 let mut cx = EditorTestContext::new(cx).await;
4849
4850 let plaintext_language = Arc::new(Language::new(
4851 LanguageConfig {
4852 name: "Plain Text".into(),
4853 ..LanguageConfig::default()
4854 },
4855 None,
4856 ));
4857
4858 cx.update_buffer(|buffer, cx| buffer.set_language(Some(plaintext_language), cx));
4859
4860 cx.set_state(indoc! {"
4861 «testˇ»
4862 "});
4863 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4864 cx.assert_editor_state(indoc! {"
4865 «testˇ»
4866 "});
4867}
4868
4869#[gpui::test]
4870async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
4871 init_test(cx, |_| {});
4872
4873 let mut cx = EditorTestContext::new(cx).await;
4874
4875 // Manipulate with multiple selections on a single line
4876 cx.set_state(indoc! {"
4877 dd«dd
4878 cˇ»c«c
4879 bb
4880 aaaˇ»aa
4881 "});
4882 cx.update_editor(|e, window, cx| {
4883 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4884 });
4885 cx.assert_editor_state(indoc! {"
4886 «aaaaa
4887 bb
4888 ccc
4889 ddddˇ»
4890 "});
4891
4892 // Manipulate with multiple disjoin selections
4893 cx.set_state(indoc! {"
4894 5«
4895 4
4896 3
4897 2
4898 1ˇ»
4899
4900 dd«dd
4901 ccc
4902 bb
4903 aaaˇ»aa
4904 "});
4905 cx.update_editor(|e, window, cx| {
4906 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4907 });
4908 cx.assert_editor_state(indoc! {"
4909 «1
4910 2
4911 3
4912 4
4913 5ˇ»
4914
4915 «aaaaa
4916 bb
4917 ccc
4918 ddddˇ»
4919 "});
4920
4921 // Adding lines on each selection
4922 cx.set_state(indoc! {"
4923 2«
4924 1ˇ»
4925
4926 bb«bb
4927 aaaˇ»aa
4928 "});
4929 cx.update_editor(|e, window, cx| {
4930 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
4931 });
4932 cx.assert_editor_state(indoc! {"
4933 «2
4934 1
4935 added lineˇ»
4936
4937 «bbbb
4938 aaaaa
4939 added lineˇ»
4940 "});
4941
4942 // Removing lines on each selection
4943 cx.set_state(indoc! {"
4944 2«
4945 1ˇ»
4946
4947 bb«bb
4948 aaaˇ»aa
4949 "});
4950 cx.update_editor(|e, window, cx| {
4951 e.manipulate_immutable_lines(window, cx, |lines| {
4952 lines.pop();
4953 })
4954 });
4955 cx.assert_editor_state(indoc! {"
4956 «2ˇ»
4957
4958 «bbbbˇ»
4959 "});
4960}
4961
4962#[gpui::test]
4963async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
4964 init_test(cx, |settings| {
4965 settings.defaults.tab_size = NonZeroU32::new(3)
4966 });
4967
4968 let mut cx = EditorTestContext::new(cx).await;
4969
4970 // MULTI SELECTION
4971 // Ln.1 "«" tests empty lines
4972 // Ln.9 tests just leading whitespace
4973 cx.set_state(indoc! {"
4974 «
4975 abc // No indentationˇ»
4976 «\tabc // 1 tabˇ»
4977 \t\tabc « ˇ» // 2 tabs
4978 \t ab«c // Tab followed by space
4979 \tabc // Space followed by tab (3 spaces should be the result)
4980 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4981 abˇ»ˇc ˇ ˇ // Already space indented«
4982 \t
4983 \tabc\tdef // Only the leading tab is manipulatedˇ»
4984 "});
4985 cx.update_editor(|e, window, cx| {
4986 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4987 });
4988 cx.assert_editor_state(
4989 indoc! {"
4990 «
4991 abc // No indentation
4992 abc // 1 tab
4993 abc // 2 tabs
4994 abc // Tab followed by space
4995 abc // Space followed by tab (3 spaces should be the result)
4996 abc // Mixed indentation (tab conversion depends on the column)
4997 abc // Already space indented
4998 ·
4999 abc\tdef // Only the leading tab is manipulatedˇ»
5000 "}
5001 .replace("·", "")
5002 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5003 );
5004
5005 // Test on just a few lines, the others should remain unchanged
5006 // Only lines (3, 5, 10, 11) should change
5007 cx.set_state(
5008 indoc! {"
5009 ·
5010 abc // No indentation
5011 \tabcˇ // 1 tab
5012 \t\tabc // 2 tabs
5013 \t abcˇ // Tab followed by space
5014 \tabc // Space followed by tab (3 spaces should be the result)
5015 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5016 abc // Already space indented
5017 «\t
5018 \tabc\tdef // Only the leading tab is manipulatedˇ»
5019 "}
5020 .replace("·", "")
5021 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5022 );
5023 cx.update_editor(|e, window, cx| {
5024 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5025 });
5026 cx.assert_editor_state(
5027 indoc! {"
5028 ·
5029 abc // No indentation
5030 « abc // 1 tabˇ»
5031 \t\tabc // 2 tabs
5032 « abc // Tab followed by spaceˇ»
5033 \tabc // Space followed by tab (3 spaces should be the result)
5034 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5035 abc // Already space indented
5036 « ·
5037 abc\tdef // Only the leading tab is manipulatedˇ»
5038 "}
5039 .replace("·", "")
5040 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5041 );
5042
5043 // SINGLE SELECTION
5044 // Ln.1 "«" tests empty lines
5045 // Ln.9 tests just leading whitespace
5046 cx.set_state(indoc! {"
5047 «
5048 abc // No indentation
5049 \tabc // 1 tab
5050 \t\tabc // 2 tabs
5051 \t abc // Tab followed by space
5052 \tabc // Space followed by tab (3 spaces should be the result)
5053 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5054 abc // Already space indented
5055 \t
5056 \tabc\tdef // Only the leading tab is manipulatedˇ»
5057 "});
5058 cx.update_editor(|e, window, cx| {
5059 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5060 });
5061 cx.assert_editor_state(
5062 indoc! {"
5063 «
5064 abc // No indentation
5065 abc // 1 tab
5066 abc // 2 tabs
5067 abc // Tab followed by space
5068 abc // Space followed by tab (3 spaces should be the result)
5069 abc // Mixed indentation (tab conversion depends on the column)
5070 abc // Already space indented
5071 ·
5072 abc\tdef // Only the leading tab is manipulatedˇ»
5073 "}
5074 .replace("·", "")
5075 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5076 );
5077}
5078
5079#[gpui::test]
5080async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
5081 init_test(cx, |settings| {
5082 settings.defaults.tab_size = NonZeroU32::new(3)
5083 });
5084
5085 let mut cx = EditorTestContext::new(cx).await;
5086
5087 // MULTI SELECTION
5088 // Ln.1 "«" tests empty lines
5089 // Ln.11 tests just leading whitespace
5090 cx.set_state(indoc! {"
5091 «
5092 abˇ»ˇc // No indentation
5093 abc ˇ ˇ // 1 space (< 3 so dont convert)
5094 abc « // 2 spaces (< 3 so dont convert)
5095 abc // 3 spaces (convert)
5096 abc ˇ» // 5 spaces (1 tab + 2 spaces)
5097 «\tˇ»\t«\tˇ»abc // Already tab indented
5098 «\t abc // Tab followed by space
5099 \tabc // Space followed by tab (should be consumed due to tab)
5100 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5101 \tˇ» «\t
5102 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
5103 "});
5104 cx.update_editor(|e, window, cx| {
5105 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5106 });
5107 cx.assert_editor_state(indoc! {"
5108 «
5109 abc // No indentation
5110 abc // 1 space (< 3 so dont convert)
5111 abc // 2 spaces (< 3 so dont convert)
5112 \tabc // 3 spaces (convert)
5113 \t abc // 5 spaces (1 tab + 2 spaces)
5114 \t\t\tabc // Already tab indented
5115 \t abc // Tab followed by space
5116 \tabc // Space followed by tab (should be consumed due to tab)
5117 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5118 \t\t\t
5119 \tabc \t // Only the leading spaces should be convertedˇ»
5120 "});
5121
5122 // Test on just a few lines, the other should remain unchanged
5123 // Only lines (4, 8, 11, 12) should change
5124 cx.set_state(
5125 indoc! {"
5126 ·
5127 abc // No indentation
5128 abc // 1 space (< 3 so dont convert)
5129 abc // 2 spaces (< 3 so dont convert)
5130 « abc // 3 spaces (convert)ˇ»
5131 abc // 5 spaces (1 tab + 2 spaces)
5132 \t\t\tabc // Already tab indented
5133 \t abc // Tab followed by space
5134 \tabc ˇ // Space followed by tab (should be consumed due to tab)
5135 \t\t \tabc // Mixed indentation
5136 \t \t \t \tabc // Mixed indentation
5137 \t \tˇ
5138 « abc \t // Only the leading spaces should be convertedˇ»
5139 "}
5140 .replace("·", "")
5141 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5142 );
5143 cx.update_editor(|e, window, cx| {
5144 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5145 });
5146 cx.assert_editor_state(
5147 indoc! {"
5148 ·
5149 abc // No indentation
5150 abc // 1 space (< 3 so dont convert)
5151 abc // 2 spaces (< 3 so dont convert)
5152 «\tabc // 3 spaces (convert)ˇ»
5153 abc // 5 spaces (1 tab + 2 spaces)
5154 \t\t\tabc // Already tab indented
5155 \t abc // Tab followed by space
5156 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
5157 \t\t \tabc // Mixed indentation
5158 \t \t \t \tabc // Mixed indentation
5159 «\t\t\t
5160 \tabc \t // Only the leading spaces should be convertedˇ»
5161 "}
5162 .replace("·", "")
5163 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5164 );
5165
5166 // SINGLE SELECTION
5167 // Ln.1 "«" tests empty lines
5168 // Ln.11 tests just leading whitespace
5169 cx.set_state(indoc! {"
5170 «
5171 abc // No indentation
5172 abc // 1 space (< 3 so dont convert)
5173 abc // 2 spaces (< 3 so dont convert)
5174 abc // 3 spaces (convert)
5175 abc // 5 spaces (1 tab + 2 spaces)
5176 \t\t\tabc // Already tab indented
5177 \t abc // Tab followed by space
5178 \tabc // Space followed by tab (should be consumed due to tab)
5179 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5180 \t \t
5181 abc \t // Only the leading spaces should be convertedˇ»
5182 "});
5183 cx.update_editor(|e, window, cx| {
5184 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5185 });
5186 cx.assert_editor_state(indoc! {"
5187 «
5188 abc // No indentation
5189 abc // 1 space (< 3 so dont convert)
5190 abc // 2 spaces (< 3 so dont convert)
5191 \tabc // 3 spaces (convert)
5192 \t abc // 5 spaces (1 tab + 2 spaces)
5193 \t\t\tabc // Already tab indented
5194 \t abc // Tab followed by space
5195 \tabc // Space followed by tab (should be consumed due to tab)
5196 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5197 \t\t\t
5198 \tabc \t // Only the leading spaces should be convertedˇ»
5199 "});
5200}
5201
5202#[gpui::test]
5203async fn test_toggle_case(cx: &mut TestAppContext) {
5204 init_test(cx, |_| {});
5205
5206 let mut cx = EditorTestContext::new(cx).await;
5207
5208 // If all lower case -> upper case
5209 cx.set_state(indoc! {"
5210 «hello worldˇ»
5211 "});
5212 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5213 cx.assert_editor_state(indoc! {"
5214 «HELLO WORLDˇ»
5215 "});
5216
5217 // If all upper case -> lower case
5218 cx.set_state(indoc! {"
5219 «HELLO WORLDˇ»
5220 "});
5221 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5222 cx.assert_editor_state(indoc! {"
5223 «hello worldˇ»
5224 "});
5225
5226 // If any upper case characters are identified -> lower case
5227 // This matches JetBrains IDEs
5228 cx.set_state(indoc! {"
5229 «hEllo worldˇ»
5230 "});
5231 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5232 cx.assert_editor_state(indoc! {"
5233 «hello worldˇ»
5234 "});
5235}
5236
5237#[gpui::test]
5238async fn test_convert_to_sentence_case(cx: &mut TestAppContext) {
5239 init_test(cx, |_| {});
5240
5241 let mut cx = EditorTestContext::new(cx).await;
5242
5243 cx.set_state(indoc! {"
5244 «implement-windows-supportˇ»
5245 "});
5246 cx.update_editor(|e, window, cx| {
5247 e.convert_to_sentence_case(&ConvertToSentenceCase, window, cx)
5248 });
5249 cx.assert_editor_state(indoc! {"
5250 «Implement windows supportˇ»
5251 "});
5252}
5253
5254#[gpui::test]
5255async fn test_manipulate_text(cx: &mut TestAppContext) {
5256 init_test(cx, |_| {});
5257
5258 let mut cx = EditorTestContext::new(cx).await;
5259
5260 // Test convert_to_upper_case()
5261 cx.set_state(indoc! {"
5262 «hello worldˇ»
5263 "});
5264 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5265 cx.assert_editor_state(indoc! {"
5266 «HELLO WORLDˇ»
5267 "});
5268
5269 // Test convert_to_lower_case()
5270 cx.set_state(indoc! {"
5271 «HELLO WORLDˇ»
5272 "});
5273 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
5274 cx.assert_editor_state(indoc! {"
5275 «hello worldˇ»
5276 "});
5277
5278 // Test multiple line, single selection case
5279 cx.set_state(indoc! {"
5280 «The quick brown
5281 fox jumps over
5282 the lazy dogˇ»
5283 "});
5284 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
5285 cx.assert_editor_state(indoc! {"
5286 «The Quick Brown
5287 Fox Jumps Over
5288 The Lazy Dogˇ»
5289 "});
5290
5291 // Test multiple line, single selection case
5292 cx.set_state(indoc! {"
5293 «The quick brown
5294 fox jumps over
5295 the lazy dogˇ»
5296 "});
5297 cx.update_editor(|e, window, cx| {
5298 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
5299 });
5300 cx.assert_editor_state(indoc! {"
5301 «TheQuickBrown
5302 FoxJumpsOver
5303 TheLazyDogˇ»
5304 "});
5305
5306 // From here on out, test more complex cases of manipulate_text()
5307
5308 // Test no selection case - should affect words cursors are in
5309 // Cursor at beginning, middle, and end of word
5310 cx.set_state(indoc! {"
5311 ˇhello big beauˇtiful worldˇ
5312 "});
5313 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5314 cx.assert_editor_state(indoc! {"
5315 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
5316 "});
5317
5318 // Test multiple selections on a single line and across multiple lines
5319 cx.set_state(indoc! {"
5320 «Theˇ» quick «brown
5321 foxˇ» jumps «overˇ»
5322 the «lazyˇ» dog
5323 "});
5324 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5325 cx.assert_editor_state(indoc! {"
5326 «THEˇ» quick «BROWN
5327 FOXˇ» jumps «OVERˇ»
5328 the «LAZYˇ» dog
5329 "});
5330
5331 // Test case where text length grows
5332 cx.set_state(indoc! {"
5333 «tschüߡ»
5334 "});
5335 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5336 cx.assert_editor_state(indoc! {"
5337 «TSCHÜSSˇ»
5338 "});
5339
5340 // Test to make sure we don't crash when text shrinks
5341 cx.set_state(indoc! {"
5342 aaa_bbbˇ
5343 "});
5344 cx.update_editor(|e, window, cx| {
5345 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5346 });
5347 cx.assert_editor_state(indoc! {"
5348 «aaaBbbˇ»
5349 "});
5350
5351 // Test to make sure we all aware of the fact that each word can grow and shrink
5352 // Final selections should be aware of this fact
5353 cx.set_state(indoc! {"
5354 aaa_bˇbb bbˇb_ccc ˇccc_ddd
5355 "});
5356 cx.update_editor(|e, window, cx| {
5357 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5358 });
5359 cx.assert_editor_state(indoc! {"
5360 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
5361 "});
5362
5363 cx.set_state(indoc! {"
5364 «hElLo, WoRld!ˇ»
5365 "});
5366 cx.update_editor(|e, window, cx| {
5367 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
5368 });
5369 cx.assert_editor_state(indoc! {"
5370 «HeLlO, wOrLD!ˇ»
5371 "});
5372
5373 // Test selections with `line_mode() = true`.
5374 cx.update_editor(|editor, _window, _cx| editor.selections.set_line_mode(true));
5375 cx.set_state(indoc! {"
5376 «The quick brown
5377 fox jumps over
5378 tˇ»he lazy dog
5379 "});
5380 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5381 cx.assert_editor_state(indoc! {"
5382 «THE QUICK BROWN
5383 FOX JUMPS OVER
5384 THE LAZY DOGˇ»
5385 "});
5386}
5387
5388#[gpui::test]
5389fn test_duplicate_line(cx: &mut TestAppContext) {
5390 init_test(cx, |_| {});
5391
5392 let editor = cx.add_window(|window, cx| {
5393 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5394 build_editor(buffer, window, cx)
5395 });
5396 _ = editor.update(cx, |editor, window, cx| {
5397 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5398 s.select_display_ranges([
5399 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5400 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5401 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5402 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5403 ])
5404 });
5405 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5406 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5407 assert_eq!(
5408 editor.selections.display_ranges(cx),
5409 vec![
5410 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
5411 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
5412 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5413 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5414 ]
5415 );
5416 });
5417
5418 let editor = cx.add_window(|window, cx| {
5419 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5420 build_editor(buffer, window, cx)
5421 });
5422 _ = editor.update(cx, |editor, window, cx| {
5423 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5424 s.select_display_ranges([
5425 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5426 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5427 ])
5428 });
5429 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5430 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5431 assert_eq!(
5432 editor.selections.display_ranges(cx),
5433 vec![
5434 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
5435 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
5436 ]
5437 );
5438 });
5439
5440 // With `move_upwards` the selections stay in place, except for
5441 // the lines inserted above them
5442 let editor = cx.add_window(|window, cx| {
5443 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5444 build_editor(buffer, window, cx)
5445 });
5446 _ = editor.update(cx, |editor, window, cx| {
5447 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5448 s.select_display_ranges([
5449 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5450 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5451 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5452 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5453 ])
5454 });
5455 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5456 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5457 assert_eq!(
5458 editor.selections.display_ranges(cx),
5459 vec![
5460 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5461 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5462 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
5463 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5464 ]
5465 );
5466 });
5467
5468 let editor = cx.add_window(|window, cx| {
5469 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5470 build_editor(buffer, window, cx)
5471 });
5472 _ = editor.update(cx, |editor, window, cx| {
5473 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5474 s.select_display_ranges([
5475 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5476 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5477 ])
5478 });
5479 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5480 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5481 assert_eq!(
5482 editor.selections.display_ranges(cx),
5483 vec![
5484 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5485 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5486 ]
5487 );
5488 });
5489
5490 let editor = cx.add_window(|window, cx| {
5491 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5492 build_editor(buffer, window, cx)
5493 });
5494 _ = editor.update(cx, |editor, window, cx| {
5495 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5496 s.select_display_ranges([
5497 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5498 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5499 ])
5500 });
5501 editor.duplicate_selection(&DuplicateSelection, window, cx);
5502 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
5503 assert_eq!(
5504 editor.selections.display_ranges(cx),
5505 vec![
5506 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5507 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
5508 ]
5509 );
5510 });
5511}
5512
5513#[gpui::test]
5514fn test_move_line_up_down(cx: &mut TestAppContext) {
5515 init_test(cx, |_| {});
5516
5517 let editor = cx.add_window(|window, cx| {
5518 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5519 build_editor(buffer, window, cx)
5520 });
5521 _ = editor.update(cx, |editor, window, cx| {
5522 editor.fold_creases(
5523 vec![
5524 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5525 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5526 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5527 ],
5528 true,
5529 window,
5530 cx,
5531 );
5532 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5533 s.select_display_ranges([
5534 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5535 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5536 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5537 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
5538 ])
5539 });
5540 assert_eq!(
5541 editor.display_text(cx),
5542 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
5543 );
5544
5545 editor.move_line_up(&MoveLineUp, window, cx);
5546 assert_eq!(
5547 editor.display_text(cx),
5548 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
5549 );
5550 assert_eq!(
5551 editor.selections.display_ranges(cx),
5552 vec![
5553 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5554 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5555 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5556 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5557 ]
5558 );
5559 });
5560
5561 _ = editor.update(cx, |editor, window, cx| {
5562 editor.move_line_down(&MoveLineDown, window, cx);
5563 assert_eq!(
5564 editor.display_text(cx),
5565 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
5566 );
5567 assert_eq!(
5568 editor.selections.display_ranges(cx),
5569 vec![
5570 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5571 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5572 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5573 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5574 ]
5575 );
5576 });
5577
5578 _ = editor.update(cx, |editor, window, cx| {
5579 editor.move_line_down(&MoveLineDown, window, cx);
5580 assert_eq!(
5581 editor.display_text(cx),
5582 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
5583 );
5584 assert_eq!(
5585 editor.selections.display_ranges(cx),
5586 vec![
5587 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5588 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5589 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5590 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5591 ]
5592 );
5593 });
5594
5595 _ = editor.update(cx, |editor, window, cx| {
5596 editor.move_line_up(&MoveLineUp, window, cx);
5597 assert_eq!(
5598 editor.display_text(cx),
5599 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
5600 );
5601 assert_eq!(
5602 editor.selections.display_ranges(cx),
5603 vec![
5604 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5605 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5606 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5607 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5608 ]
5609 );
5610 });
5611}
5612
5613#[gpui::test]
5614fn test_move_line_up_selection_at_end_of_fold(cx: &mut TestAppContext) {
5615 init_test(cx, |_| {});
5616 let editor = cx.add_window(|window, cx| {
5617 let buffer = MultiBuffer::build_simple("\n\n\n\n\n\naaaa\nbbbb\ncccc", cx);
5618 build_editor(buffer, window, cx)
5619 });
5620 _ = editor.update(cx, |editor, window, cx| {
5621 editor.fold_creases(
5622 vec![Crease::simple(
5623 Point::new(6, 4)..Point::new(7, 4),
5624 FoldPlaceholder::test(),
5625 )],
5626 true,
5627 window,
5628 cx,
5629 );
5630 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5631 s.select_ranges([Point::new(7, 4)..Point::new(7, 4)])
5632 });
5633 assert_eq!(editor.display_text(cx), "\n\n\n\n\n\naaaa⋯\ncccc");
5634 editor.move_line_up(&MoveLineUp, window, cx);
5635 let buffer_text = editor.buffer.read(cx).snapshot(cx).text();
5636 assert_eq!(buffer_text, "\n\n\n\n\naaaa\nbbbb\n\ncccc");
5637 });
5638}
5639
5640#[gpui::test]
5641fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
5642 init_test(cx, |_| {});
5643
5644 let editor = cx.add_window(|window, cx| {
5645 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5646 build_editor(buffer, window, cx)
5647 });
5648 _ = editor.update(cx, |editor, window, cx| {
5649 let snapshot = editor.buffer.read(cx).snapshot(cx);
5650 editor.insert_blocks(
5651 [BlockProperties {
5652 style: BlockStyle::Fixed,
5653 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
5654 height: Some(1),
5655 render: Arc::new(|_| div().into_any()),
5656 priority: 0,
5657 }],
5658 Some(Autoscroll::fit()),
5659 cx,
5660 );
5661 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5662 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
5663 });
5664 editor.move_line_down(&MoveLineDown, window, cx);
5665 });
5666}
5667
5668#[gpui::test]
5669async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
5670 init_test(cx, |_| {});
5671
5672 let mut cx = EditorTestContext::new(cx).await;
5673 cx.set_state(
5674 &"
5675 ˇzero
5676 one
5677 two
5678 three
5679 four
5680 five
5681 "
5682 .unindent(),
5683 );
5684
5685 // Create a four-line block that replaces three lines of text.
5686 cx.update_editor(|editor, window, cx| {
5687 let snapshot = editor.snapshot(window, cx);
5688 let snapshot = &snapshot.buffer_snapshot;
5689 let placement = BlockPlacement::Replace(
5690 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
5691 );
5692 editor.insert_blocks(
5693 [BlockProperties {
5694 placement,
5695 height: Some(4),
5696 style: BlockStyle::Sticky,
5697 render: Arc::new(|_| gpui::div().into_any_element()),
5698 priority: 0,
5699 }],
5700 None,
5701 cx,
5702 );
5703 });
5704
5705 // Move down so that the cursor touches the block.
5706 cx.update_editor(|editor, window, cx| {
5707 editor.move_down(&Default::default(), window, cx);
5708 });
5709 cx.assert_editor_state(
5710 &"
5711 zero
5712 «one
5713 two
5714 threeˇ»
5715 four
5716 five
5717 "
5718 .unindent(),
5719 );
5720
5721 // Move down past the block.
5722 cx.update_editor(|editor, window, cx| {
5723 editor.move_down(&Default::default(), window, cx);
5724 });
5725 cx.assert_editor_state(
5726 &"
5727 zero
5728 one
5729 two
5730 three
5731 ˇfour
5732 five
5733 "
5734 .unindent(),
5735 );
5736}
5737
5738#[gpui::test]
5739fn test_transpose(cx: &mut TestAppContext) {
5740 init_test(cx, |_| {});
5741
5742 _ = cx.add_window(|window, cx| {
5743 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
5744 editor.set_style(EditorStyle::default(), window, cx);
5745 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5746 s.select_ranges([1..1])
5747 });
5748 editor.transpose(&Default::default(), window, cx);
5749 assert_eq!(editor.text(cx), "bac");
5750 assert_eq!(editor.selections.ranges(cx), [2..2]);
5751
5752 editor.transpose(&Default::default(), window, cx);
5753 assert_eq!(editor.text(cx), "bca");
5754 assert_eq!(editor.selections.ranges(cx), [3..3]);
5755
5756 editor.transpose(&Default::default(), window, cx);
5757 assert_eq!(editor.text(cx), "bac");
5758 assert_eq!(editor.selections.ranges(cx), [3..3]);
5759
5760 editor
5761 });
5762
5763 _ = cx.add_window(|window, cx| {
5764 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5765 editor.set_style(EditorStyle::default(), window, cx);
5766 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5767 s.select_ranges([3..3])
5768 });
5769 editor.transpose(&Default::default(), window, cx);
5770 assert_eq!(editor.text(cx), "acb\nde");
5771 assert_eq!(editor.selections.ranges(cx), [3..3]);
5772
5773 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5774 s.select_ranges([4..4])
5775 });
5776 editor.transpose(&Default::default(), window, cx);
5777 assert_eq!(editor.text(cx), "acbd\ne");
5778 assert_eq!(editor.selections.ranges(cx), [5..5]);
5779
5780 editor.transpose(&Default::default(), window, cx);
5781 assert_eq!(editor.text(cx), "acbde\n");
5782 assert_eq!(editor.selections.ranges(cx), [6..6]);
5783
5784 editor.transpose(&Default::default(), window, cx);
5785 assert_eq!(editor.text(cx), "acbd\ne");
5786 assert_eq!(editor.selections.ranges(cx), [6..6]);
5787
5788 editor
5789 });
5790
5791 _ = cx.add_window(|window, cx| {
5792 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5793 editor.set_style(EditorStyle::default(), window, cx);
5794 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5795 s.select_ranges([1..1, 2..2, 4..4])
5796 });
5797 editor.transpose(&Default::default(), window, cx);
5798 assert_eq!(editor.text(cx), "bacd\ne");
5799 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
5800
5801 editor.transpose(&Default::default(), window, cx);
5802 assert_eq!(editor.text(cx), "bcade\n");
5803 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
5804
5805 editor.transpose(&Default::default(), window, cx);
5806 assert_eq!(editor.text(cx), "bcda\ne");
5807 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5808
5809 editor.transpose(&Default::default(), window, cx);
5810 assert_eq!(editor.text(cx), "bcade\n");
5811 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5812
5813 editor.transpose(&Default::default(), window, cx);
5814 assert_eq!(editor.text(cx), "bcaed\n");
5815 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
5816
5817 editor
5818 });
5819
5820 _ = cx.add_window(|window, cx| {
5821 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
5822 editor.set_style(EditorStyle::default(), window, cx);
5823 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5824 s.select_ranges([4..4])
5825 });
5826 editor.transpose(&Default::default(), window, cx);
5827 assert_eq!(editor.text(cx), "🏀🍐✋");
5828 assert_eq!(editor.selections.ranges(cx), [8..8]);
5829
5830 editor.transpose(&Default::default(), window, cx);
5831 assert_eq!(editor.text(cx), "🏀✋🍐");
5832 assert_eq!(editor.selections.ranges(cx), [11..11]);
5833
5834 editor.transpose(&Default::default(), window, cx);
5835 assert_eq!(editor.text(cx), "🏀🍐✋");
5836 assert_eq!(editor.selections.ranges(cx), [11..11]);
5837
5838 editor
5839 });
5840}
5841
5842#[gpui::test]
5843async fn test_rewrap(cx: &mut TestAppContext) {
5844 init_test(cx, |settings| {
5845 settings.languages.0.extend([
5846 (
5847 "Markdown".into(),
5848 LanguageSettingsContent {
5849 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5850 preferred_line_length: Some(40),
5851 ..Default::default()
5852 },
5853 ),
5854 (
5855 "Plain Text".into(),
5856 LanguageSettingsContent {
5857 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5858 preferred_line_length: Some(40),
5859 ..Default::default()
5860 },
5861 ),
5862 (
5863 "C++".into(),
5864 LanguageSettingsContent {
5865 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5866 preferred_line_length: Some(40),
5867 ..Default::default()
5868 },
5869 ),
5870 (
5871 "Python".into(),
5872 LanguageSettingsContent {
5873 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5874 preferred_line_length: Some(40),
5875 ..Default::default()
5876 },
5877 ),
5878 (
5879 "Rust".into(),
5880 LanguageSettingsContent {
5881 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5882 preferred_line_length: Some(40),
5883 ..Default::default()
5884 },
5885 ),
5886 ])
5887 });
5888
5889 let mut cx = EditorTestContext::new(cx).await;
5890
5891 let cpp_language = Arc::new(Language::new(
5892 LanguageConfig {
5893 name: "C++".into(),
5894 line_comments: vec!["// ".into()],
5895 ..LanguageConfig::default()
5896 },
5897 None,
5898 ));
5899 let python_language = Arc::new(Language::new(
5900 LanguageConfig {
5901 name: "Python".into(),
5902 line_comments: vec!["# ".into()],
5903 ..LanguageConfig::default()
5904 },
5905 None,
5906 ));
5907 let markdown_language = Arc::new(Language::new(
5908 LanguageConfig {
5909 name: "Markdown".into(),
5910 rewrap_prefixes: vec![
5911 regex::Regex::new("\\d+\\.\\s+").unwrap(),
5912 regex::Regex::new("[-*+]\\s+").unwrap(),
5913 ],
5914 ..LanguageConfig::default()
5915 },
5916 None,
5917 ));
5918 let rust_language = Arc::new(
5919 Language::new(
5920 LanguageConfig {
5921 name: "Rust".into(),
5922 line_comments: vec!["// ".into(), "/// ".into()],
5923 ..LanguageConfig::default()
5924 },
5925 Some(tree_sitter_rust::LANGUAGE.into()),
5926 )
5927 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
5928 .unwrap(),
5929 );
5930
5931 let plaintext_language = Arc::new(Language::new(
5932 LanguageConfig {
5933 name: "Plain Text".into(),
5934 ..LanguageConfig::default()
5935 },
5936 None,
5937 ));
5938
5939 // Test basic rewrapping of a long line with a cursor
5940 assert_rewrap(
5941 indoc! {"
5942 // ˇThis is a long comment that needs to be wrapped.
5943 "},
5944 indoc! {"
5945 // ˇThis is a long comment that needs to
5946 // be wrapped.
5947 "},
5948 cpp_language.clone(),
5949 &mut cx,
5950 );
5951
5952 // Test rewrapping a full selection
5953 assert_rewrap(
5954 indoc! {"
5955 «// This selected long comment needs to be wrapped.ˇ»"
5956 },
5957 indoc! {"
5958 «// This selected long comment needs to
5959 // be wrapped.ˇ»"
5960 },
5961 cpp_language.clone(),
5962 &mut cx,
5963 );
5964
5965 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
5966 assert_rewrap(
5967 indoc! {"
5968 // ˇThis is the first line.
5969 // Thisˇ is the second line.
5970 // This is the thirdˇ line, all part of one paragraph.
5971 "},
5972 indoc! {"
5973 // ˇThis is the first line. Thisˇ is the
5974 // second line. This is the thirdˇ line,
5975 // all part of one paragraph.
5976 "},
5977 cpp_language.clone(),
5978 &mut cx,
5979 );
5980
5981 // Test multiple cursors in different paragraphs trigger separate rewraps
5982 assert_rewrap(
5983 indoc! {"
5984 // ˇThis is the first paragraph, first line.
5985 // ˇThis is the first paragraph, second line.
5986
5987 // ˇThis is the second paragraph, first line.
5988 // ˇThis is the second paragraph, second line.
5989 "},
5990 indoc! {"
5991 // ˇThis is the first paragraph, first
5992 // line. ˇThis is the first paragraph,
5993 // second line.
5994
5995 // ˇThis is the second paragraph, first
5996 // line. ˇThis is the second paragraph,
5997 // second line.
5998 "},
5999 cpp_language.clone(),
6000 &mut cx,
6001 );
6002
6003 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
6004 assert_rewrap(
6005 indoc! {"
6006 «// A regular long long comment to be wrapped.
6007 /// A documentation long comment to be wrapped.ˇ»
6008 "},
6009 indoc! {"
6010 «// A regular long long comment to be
6011 // wrapped.
6012 /// A documentation long comment to be
6013 /// wrapped.ˇ»
6014 "},
6015 rust_language.clone(),
6016 &mut cx,
6017 );
6018
6019 // Test that change in indentation level trigger seperate rewraps
6020 assert_rewrap(
6021 indoc! {"
6022 fn foo() {
6023 «// This is a long comment at the base indent.
6024 // This is a long comment at the next indent.ˇ»
6025 }
6026 "},
6027 indoc! {"
6028 fn foo() {
6029 «// This is a long comment at the
6030 // base indent.
6031 // This is a long comment at the
6032 // next indent.ˇ»
6033 }
6034 "},
6035 rust_language.clone(),
6036 &mut cx,
6037 );
6038
6039 // Test that different comment prefix characters (e.g., '#') are handled correctly
6040 assert_rewrap(
6041 indoc! {"
6042 # ˇThis is a long comment using a pound sign.
6043 "},
6044 indoc! {"
6045 # ˇThis is a long comment using a pound
6046 # sign.
6047 "},
6048 python_language,
6049 &mut cx,
6050 );
6051
6052 // Test rewrapping only affects comments, not code even when selected
6053 assert_rewrap(
6054 indoc! {"
6055 «/// This doc comment is long and should be wrapped.
6056 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6057 "},
6058 indoc! {"
6059 «/// This doc comment is long and should
6060 /// be wrapped.
6061 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6062 "},
6063 rust_language.clone(),
6064 &mut cx,
6065 );
6066
6067 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
6068 assert_rewrap(
6069 indoc! {"
6070 # Header
6071
6072 A long long long line of markdown text to wrap.ˇ
6073 "},
6074 indoc! {"
6075 # Header
6076
6077 A long long long line of markdown text
6078 to wrap.ˇ
6079 "},
6080 markdown_language.clone(),
6081 &mut cx,
6082 );
6083
6084 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
6085 assert_rewrap(
6086 indoc! {"
6087 «1. This is a numbered list item that is very long and needs to be wrapped properly.
6088 2. This is a numbered list item that is very long and needs to be wrapped properly.
6089 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
6090 "},
6091 indoc! {"
6092 «1. This is a numbered list item that is
6093 very long and needs to be wrapped
6094 properly.
6095 2. This is a numbered list item that is
6096 very long and needs to be wrapped
6097 properly.
6098 - This is an unordered list item that is
6099 also very long and should not merge
6100 with the numbered item.ˇ»
6101 "},
6102 markdown_language.clone(),
6103 &mut cx,
6104 );
6105
6106 // Test that rewrapping add indents for rewrapping boundary if not exists already.
6107 assert_rewrap(
6108 indoc! {"
6109 «1. This is a numbered list item that is
6110 very long and needs to be wrapped
6111 properly.
6112 2. This is a numbered list item that is
6113 very long and needs to be wrapped
6114 properly.
6115 - This is an unordered list item that is
6116 also very long and should not merge with
6117 the numbered item.ˇ»
6118 "},
6119 indoc! {"
6120 «1. This is a numbered list item that is
6121 very long and needs to be wrapped
6122 properly.
6123 2. This is a numbered list item that is
6124 very long and needs to be wrapped
6125 properly.
6126 - This is an unordered list item that is
6127 also very long and should not merge
6128 with the numbered item.ˇ»
6129 "},
6130 markdown_language.clone(),
6131 &mut cx,
6132 );
6133
6134 // Test that rewrapping maintain indents even when they already exists.
6135 assert_rewrap(
6136 indoc! {"
6137 «1. This is a numbered list
6138 item that is very long and needs to be wrapped properly.
6139 2. This is a numbered list
6140 item that is very long and needs to be wrapped properly.
6141 - This is an unordered list item that is also very long and
6142 should not merge with the numbered item.ˇ»
6143 "},
6144 indoc! {"
6145 «1. This is a numbered list item that is
6146 very long and needs to be wrapped
6147 properly.
6148 2. This is a numbered list item that is
6149 very long and needs to be wrapped
6150 properly.
6151 - This is an unordered list item that is
6152 also very long and should not merge
6153 with the numbered item.ˇ»
6154 "},
6155 markdown_language,
6156 &mut cx,
6157 );
6158
6159 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
6160 assert_rewrap(
6161 indoc! {"
6162 ˇThis is a very long line of plain text that will be wrapped.
6163 "},
6164 indoc! {"
6165 ˇThis is a very long line of plain text
6166 that will be wrapped.
6167 "},
6168 plaintext_language.clone(),
6169 &mut cx,
6170 );
6171
6172 // Test that non-commented code acts as a paragraph boundary within a selection
6173 assert_rewrap(
6174 indoc! {"
6175 «// This is the first long comment block to be wrapped.
6176 fn my_func(a: u32);
6177 // This is the second long comment block to be wrapped.ˇ»
6178 "},
6179 indoc! {"
6180 «// This is the first long comment block
6181 // to be wrapped.
6182 fn my_func(a: u32);
6183 // This is the second long comment block
6184 // to be wrapped.ˇ»
6185 "},
6186 rust_language,
6187 &mut cx,
6188 );
6189
6190 // Test rewrapping multiple selections, including ones with blank lines or tabs
6191 assert_rewrap(
6192 indoc! {"
6193 «ˇThis is a very long line that will be wrapped.
6194
6195 This is another paragraph in the same selection.»
6196
6197 «\tThis is a very long indented line that will be wrapped.ˇ»
6198 "},
6199 indoc! {"
6200 «ˇThis is a very long line that will be
6201 wrapped.
6202
6203 This is another paragraph in the same
6204 selection.»
6205
6206 «\tThis is a very long indented line
6207 \tthat will be wrapped.ˇ»
6208 "},
6209 plaintext_language,
6210 &mut cx,
6211 );
6212
6213 // Test that an empty comment line acts as a paragraph boundary
6214 assert_rewrap(
6215 indoc! {"
6216 // ˇThis is a long comment that will be wrapped.
6217 //
6218 // And this is another long comment that will also be wrapped.ˇ
6219 "},
6220 indoc! {"
6221 // ˇThis is a long comment that will be
6222 // wrapped.
6223 //
6224 // And this is another long comment that
6225 // will also be wrapped.ˇ
6226 "},
6227 cpp_language,
6228 &mut cx,
6229 );
6230
6231 #[track_caller]
6232 fn assert_rewrap(
6233 unwrapped_text: &str,
6234 wrapped_text: &str,
6235 language: Arc<Language>,
6236 cx: &mut EditorTestContext,
6237 ) {
6238 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6239 cx.set_state(unwrapped_text);
6240 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6241 cx.assert_editor_state(wrapped_text);
6242 }
6243}
6244
6245#[gpui::test]
6246async fn test_rewrap_block_comments(cx: &mut TestAppContext) {
6247 init_test(cx, |settings| {
6248 settings.languages.0.extend([(
6249 "Rust".into(),
6250 LanguageSettingsContent {
6251 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6252 preferred_line_length: Some(40),
6253 ..Default::default()
6254 },
6255 )])
6256 });
6257
6258 let mut cx = EditorTestContext::new(cx).await;
6259
6260 let rust_lang = Arc::new(
6261 Language::new(
6262 LanguageConfig {
6263 name: "Rust".into(),
6264 line_comments: vec!["// ".into()],
6265 block_comment: Some(BlockCommentConfig {
6266 start: "/*".into(),
6267 end: "*/".into(),
6268 prefix: "* ".into(),
6269 tab_size: 1,
6270 }),
6271 documentation_comment: Some(BlockCommentConfig {
6272 start: "/**".into(),
6273 end: "*/".into(),
6274 prefix: "* ".into(),
6275 tab_size: 1,
6276 }),
6277
6278 ..LanguageConfig::default()
6279 },
6280 Some(tree_sitter_rust::LANGUAGE.into()),
6281 )
6282 .with_override_query("[(line_comment) (block_comment)] @comment.inclusive")
6283 .unwrap(),
6284 );
6285
6286 // regular block comment
6287 assert_rewrap(
6288 indoc! {"
6289 /*
6290 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6291 */
6292 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6293 "},
6294 indoc! {"
6295 /*
6296 *ˇ Lorem ipsum dolor sit amet,
6297 * consectetur adipiscing elit.
6298 */
6299 /*
6300 *ˇ Lorem ipsum dolor sit amet,
6301 * consectetur adipiscing elit.
6302 */
6303 "},
6304 rust_lang.clone(),
6305 &mut cx,
6306 );
6307
6308 // indent is respected
6309 assert_rewrap(
6310 indoc! {"
6311 {}
6312 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6313 "},
6314 indoc! {"
6315 {}
6316 /*
6317 *ˇ Lorem ipsum dolor sit amet,
6318 * consectetur adipiscing elit.
6319 */
6320 "},
6321 rust_lang.clone(),
6322 &mut cx,
6323 );
6324
6325 // short block comments with inline delimiters
6326 assert_rewrap(
6327 indoc! {"
6328 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6329 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6330 */
6331 /*
6332 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6333 "},
6334 indoc! {"
6335 /*
6336 *ˇ Lorem ipsum dolor sit amet,
6337 * consectetur adipiscing elit.
6338 */
6339 /*
6340 *ˇ Lorem ipsum dolor sit amet,
6341 * consectetur adipiscing elit.
6342 */
6343 /*
6344 *ˇ Lorem ipsum dolor sit amet,
6345 * consectetur adipiscing elit.
6346 */
6347 "},
6348 rust_lang.clone(),
6349 &mut cx,
6350 );
6351
6352 // multiline block comment with inline start/end delimiters
6353 assert_rewrap(
6354 indoc! {"
6355 /*ˇ Lorem ipsum dolor sit amet,
6356 * consectetur adipiscing elit. */
6357 "},
6358 indoc! {"
6359 /*
6360 *ˇ Lorem ipsum dolor sit amet,
6361 * consectetur adipiscing elit.
6362 */
6363 "},
6364 rust_lang.clone(),
6365 &mut cx,
6366 );
6367
6368 // block comment rewrap still respects paragraph bounds
6369 assert_rewrap(
6370 indoc! {"
6371 /*
6372 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6373 *
6374 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6375 */
6376 "},
6377 indoc! {"
6378 /*
6379 *ˇ Lorem ipsum dolor sit amet,
6380 * consectetur adipiscing elit.
6381 *
6382 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6383 */
6384 "},
6385 rust_lang.clone(),
6386 &mut cx,
6387 );
6388
6389 // documentation comments
6390 assert_rewrap(
6391 indoc! {"
6392 /**ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6393 /**
6394 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6395 */
6396 "},
6397 indoc! {"
6398 /**
6399 *ˇ Lorem ipsum dolor sit amet,
6400 * consectetur adipiscing elit.
6401 */
6402 /**
6403 *ˇ Lorem ipsum dolor sit amet,
6404 * consectetur adipiscing elit.
6405 */
6406 "},
6407 rust_lang.clone(),
6408 &mut cx,
6409 );
6410
6411 // different, adjacent comments
6412 assert_rewrap(
6413 indoc! {"
6414 /**
6415 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6416 */
6417 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6418 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6419 "},
6420 indoc! {"
6421 /**
6422 *ˇ Lorem ipsum dolor sit amet,
6423 * consectetur adipiscing elit.
6424 */
6425 /*
6426 *ˇ Lorem ipsum dolor sit amet,
6427 * consectetur adipiscing elit.
6428 */
6429 //ˇ Lorem ipsum dolor sit amet,
6430 // consectetur adipiscing elit.
6431 "},
6432 rust_lang.clone(),
6433 &mut cx,
6434 );
6435
6436 // selection w/ single short block comment
6437 assert_rewrap(
6438 indoc! {"
6439 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6440 "},
6441 indoc! {"
6442 «/*
6443 * Lorem ipsum dolor sit amet,
6444 * consectetur adipiscing elit.
6445 */ˇ»
6446 "},
6447 rust_lang.clone(),
6448 &mut cx,
6449 );
6450
6451 // rewrapping a single comment w/ abutting comments
6452 assert_rewrap(
6453 indoc! {"
6454 /* ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. */
6455 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6456 "},
6457 indoc! {"
6458 /*
6459 * ˇLorem ipsum dolor sit amet,
6460 * consectetur adipiscing elit.
6461 */
6462 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6463 "},
6464 rust_lang.clone(),
6465 &mut cx,
6466 );
6467
6468 // selection w/ non-abutting short block comments
6469 assert_rewrap(
6470 indoc! {"
6471 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6472
6473 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6474 "},
6475 indoc! {"
6476 «/*
6477 * Lorem ipsum dolor sit amet,
6478 * consectetur adipiscing elit.
6479 */
6480
6481 /*
6482 * Lorem ipsum dolor sit amet,
6483 * consectetur adipiscing elit.
6484 */ˇ»
6485 "},
6486 rust_lang.clone(),
6487 &mut cx,
6488 );
6489
6490 // selection of multiline block comments
6491 assert_rewrap(
6492 indoc! {"
6493 «/* Lorem ipsum dolor sit amet,
6494 * consectetur adipiscing elit. */ˇ»
6495 "},
6496 indoc! {"
6497 «/*
6498 * Lorem ipsum dolor sit amet,
6499 * consectetur adipiscing elit.
6500 */ˇ»
6501 "},
6502 rust_lang.clone(),
6503 &mut cx,
6504 );
6505
6506 // partial selection of multiline block comments
6507 assert_rewrap(
6508 indoc! {"
6509 «/* Lorem ipsum dolor sit amet,ˇ»
6510 * consectetur adipiscing elit. */
6511 /* Lorem ipsum dolor sit amet,
6512 «* consectetur adipiscing elit. */ˇ»
6513 "},
6514 indoc! {"
6515 «/*
6516 * Lorem ipsum dolor sit amet,ˇ»
6517 * consectetur adipiscing elit. */
6518 /* Lorem ipsum dolor sit amet,
6519 «* consectetur adipiscing elit.
6520 */ˇ»
6521 "},
6522 rust_lang.clone(),
6523 &mut cx,
6524 );
6525
6526 // selection w/ abutting short block comments
6527 // TODO: should not be combined; should rewrap as 2 comments
6528 assert_rewrap(
6529 indoc! {"
6530 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6531 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6532 "},
6533 // desired behavior:
6534 // indoc! {"
6535 // «/*
6536 // * Lorem ipsum dolor sit amet,
6537 // * consectetur adipiscing elit.
6538 // */
6539 // /*
6540 // * Lorem ipsum dolor sit amet,
6541 // * consectetur adipiscing elit.
6542 // */ˇ»
6543 // "},
6544 // actual behaviour:
6545 indoc! {"
6546 «/*
6547 * Lorem ipsum dolor sit amet,
6548 * consectetur adipiscing elit. Lorem
6549 * ipsum dolor sit amet, consectetur
6550 * adipiscing elit.
6551 */ˇ»
6552 "},
6553 rust_lang.clone(),
6554 &mut cx,
6555 );
6556
6557 // TODO: same as above, but with delimiters on separate line
6558 // assert_rewrap(
6559 // indoc! {"
6560 // «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6561 // */
6562 // /*
6563 // * Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6564 // "},
6565 // // desired:
6566 // // indoc! {"
6567 // // «/*
6568 // // * Lorem ipsum dolor sit amet,
6569 // // * consectetur adipiscing elit.
6570 // // */
6571 // // /*
6572 // // * Lorem ipsum dolor sit amet,
6573 // // * consectetur adipiscing elit.
6574 // // */ˇ»
6575 // // "},
6576 // // actual: (but with trailing w/s on the empty lines)
6577 // indoc! {"
6578 // «/*
6579 // * Lorem ipsum dolor sit amet,
6580 // * consectetur adipiscing elit.
6581 // *
6582 // */
6583 // /*
6584 // *
6585 // * Lorem ipsum dolor sit amet,
6586 // * consectetur adipiscing elit.
6587 // */ˇ»
6588 // "},
6589 // rust_lang.clone(),
6590 // &mut cx,
6591 // );
6592
6593 // TODO these are unhandled edge cases; not correct, just documenting known issues
6594 assert_rewrap(
6595 indoc! {"
6596 /*
6597 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6598 */
6599 /*
6600 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6601 /*ˇ Lorem ipsum dolor sit amet */ /* consectetur adipiscing elit. */
6602 "},
6603 // desired:
6604 // indoc! {"
6605 // /*
6606 // *ˇ Lorem ipsum dolor sit amet,
6607 // * consectetur adipiscing elit.
6608 // */
6609 // /*
6610 // *ˇ Lorem ipsum dolor sit amet,
6611 // * consectetur adipiscing elit.
6612 // */
6613 // /*
6614 // *ˇ Lorem ipsum dolor sit amet
6615 // */ /* consectetur adipiscing elit. */
6616 // "},
6617 // actual:
6618 indoc! {"
6619 /*
6620 //ˇ Lorem ipsum dolor sit amet,
6621 // consectetur adipiscing elit.
6622 */
6623 /*
6624 * //ˇ Lorem ipsum dolor sit amet,
6625 * consectetur adipiscing elit.
6626 */
6627 /*
6628 *ˇ Lorem ipsum dolor sit amet */ /*
6629 * consectetur adipiscing elit.
6630 */
6631 "},
6632 rust_lang,
6633 &mut cx,
6634 );
6635
6636 #[track_caller]
6637 fn assert_rewrap(
6638 unwrapped_text: &str,
6639 wrapped_text: &str,
6640 language: Arc<Language>,
6641 cx: &mut EditorTestContext,
6642 ) {
6643 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6644 cx.set_state(unwrapped_text);
6645 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6646 cx.assert_editor_state(wrapped_text);
6647 }
6648}
6649
6650#[gpui::test]
6651async fn test_hard_wrap(cx: &mut TestAppContext) {
6652 init_test(cx, |_| {});
6653 let mut cx = EditorTestContext::new(cx).await;
6654
6655 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
6656 cx.update_editor(|editor, _, cx| {
6657 editor.set_hard_wrap(Some(14), cx);
6658 });
6659
6660 cx.set_state(indoc!(
6661 "
6662 one two three ˇ
6663 "
6664 ));
6665 cx.simulate_input("four");
6666 cx.run_until_parked();
6667
6668 cx.assert_editor_state(indoc!(
6669 "
6670 one two three
6671 fourˇ
6672 "
6673 ));
6674
6675 cx.update_editor(|editor, window, cx| {
6676 editor.newline(&Default::default(), window, cx);
6677 });
6678 cx.run_until_parked();
6679 cx.assert_editor_state(indoc!(
6680 "
6681 one two three
6682 four
6683 ˇ
6684 "
6685 ));
6686
6687 cx.simulate_input("five");
6688 cx.run_until_parked();
6689 cx.assert_editor_state(indoc!(
6690 "
6691 one two three
6692 four
6693 fiveˇ
6694 "
6695 ));
6696
6697 cx.update_editor(|editor, window, cx| {
6698 editor.newline(&Default::default(), window, cx);
6699 });
6700 cx.run_until_parked();
6701 cx.simulate_input("# ");
6702 cx.run_until_parked();
6703 cx.assert_editor_state(indoc!(
6704 "
6705 one two three
6706 four
6707 five
6708 # ˇ
6709 "
6710 ));
6711
6712 cx.update_editor(|editor, window, cx| {
6713 editor.newline(&Default::default(), window, cx);
6714 });
6715 cx.run_until_parked();
6716 cx.assert_editor_state(indoc!(
6717 "
6718 one two three
6719 four
6720 five
6721 #\x20
6722 #ˇ
6723 "
6724 ));
6725
6726 cx.simulate_input(" 6");
6727 cx.run_until_parked();
6728 cx.assert_editor_state(indoc!(
6729 "
6730 one two three
6731 four
6732 five
6733 #
6734 # 6ˇ
6735 "
6736 ));
6737}
6738
6739#[gpui::test]
6740async fn test_cut_line_ends(cx: &mut TestAppContext) {
6741 init_test(cx, |_| {});
6742
6743 let mut cx = EditorTestContext::new(cx).await;
6744
6745 cx.set_state(indoc! {"The quick brownˇ"});
6746 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
6747 cx.assert_editor_state(indoc! {"The quick brownˇ"});
6748
6749 cx.set_state(indoc! {"The emacs foxˇ"});
6750 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
6751 cx.assert_editor_state(indoc! {"The emacs foxˇ"});
6752
6753 cx.set_state(indoc! {"
6754 The quick« brownˇ»
6755 fox jumps overˇ
6756 the lazy dog"});
6757 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6758 cx.assert_editor_state(indoc! {"
6759 The quickˇ
6760 ˇthe lazy dog"});
6761
6762 cx.set_state(indoc! {"
6763 The quick« brownˇ»
6764 fox jumps overˇ
6765 the lazy dog"});
6766 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
6767 cx.assert_editor_state(indoc! {"
6768 The quickˇ
6769 fox jumps overˇthe lazy dog"});
6770
6771 cx.set_state(indoc! {"
6772 The quick« brownˇ»
6773 fox jumps overˇ
6774 the lazy dog"});
6775 cx.update_editor(|e, window, cx| {
6776 e.cut_to_end_of_line(
6777 &CutToEndOfLine {
6778 stop_at_newlines: true,
6779 },
6780 window,
6781 cx,
6782 )
6783 });
6784 cx.assert_editor_state(indoc! {"
6785 The quickˇ
6786 fox jumps overˇ
6787 the lazy dog"});
6788
6789 cx.set_state(indoc! {"
6790 The quick« brownˇ»
6791 fox jumps overˇ
6792 the lazy dog"});
6793 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
6794 cx.assert_editor_state(indoc! {"
6795 The quickˇ
6796 fox jumps overˇthe lazy dog"});
6797}
6798
6799#[gpui::test]
6800async fn test_clipboard(cx: &mut TestAppContext) {
6801 init_test(cx, |_| {});
6802
6803 let mut cx = EditorTestContext::new(cx).await;
6804
6805 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
6806 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6807 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
6808
6809 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
6810 cx.set_state("two ˇfour ˇsix ˇ");
6811 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6812 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
6813
6814 // Paste again but with only two cursors. Since the number of cursors doesn't
6815 // match the number of slices in the clipboard, the entire clipboard text
6816 // is pasted at each cursor.
6817 cx.set_state("ˇtwo one✅ four three six five ˇ");
6818 cx.update_editor(|e, window, cx| {
6819 e.handle_input("( ", window, cx);
6820 e.paste(&Paste, window, cx);
6821 e.handle_input(") ", window, cx);
6822 });
6823 cx.assert_editor_state(
6824 &([
6825 "( one✅ ",
6826 "three ",
6827 "five ) ˇtwo one✅ four three six five ( one✅ ",
6828 "three ",
6829 "five ) ˇ",
6830 ]
6831 .join("\n")),
6832 );
6833
6834 // Cut with three selections, one of which is full-line.
6835 cx.set_state(indoc! {"
6836 1«2ˇ»3
6837 4ˇ567
6838 «8ˇ»9"});
6839 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6840 cx.assert_editor_state(indoc! {"
6841 1ˇ3
6842 ˇ9"});
6843
6844 // Paste with three selections, noticing how the copied selection that was full-line
6845 // gets inserted before the second cursor.
6846 cx.set_state(indoc! {"
6847 1ˇ3
6848 9ˇ
6849 «oˇ»ne"});
6850 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6851 cx.assert_editor_state(indoc! {"
6852 12ˇ3
6853 4567
6854 9ˇ
6855 8ˇne"});
6856
6857 // Copy with a single cursor only, which writes the whole line into the clipboard.
6858 cx.set_state(indoc! {"
6859 The quick brown
6860 fox juˇmps over
6861 the lazy dog"});
6862 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6863 assert_eq!(
6864 cx.read_from_clipboard()
6865 .and_then(|item| item.text().as_deref().map(str::to_string)),
6866 Some("fox jumps over\n".to_string())
6867 );
6868
6869 // Paste with three selections, noticing how the copied full-line selection is inserted
6870 // before the empty selections but replaces the selection that is non-empty.
6871 cx.set_state(indoc! {"
6872 Tˇhe quick brown
6873 «foˇ»x jumps over
6874 tˇhe lazy dog"});
6875 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6876 cx.assert_editor_state(indoc! {"
6877 fox jumps over
6878 Tˇhe quick brown
6879 fox jumps over
6880 ˇx jumps over
6881 fox jumps over
6882 tˇhe lazy dog"});
6883}
6884
6885#[gpui::test]
6886async fn test_copy_trim(cx: &mut TestAppContext) {
6887 init_test(cx, |_| {});
6888
6889 let mut cx = EditorTestContext::new(cx).await;
6890 cx.set_state(
6891 r#" «for selection in selections.iter() {
6892 let mut start = selection.start;
6893 let mut end = selection.end;
6894 let is_entire_line = selection.is_empty();
6895 if is_entire_line {
6896 start = Point::new(start.row, 0);ˇ»
6897 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6898 }
6899 "#,
6900 );
6901 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6902 assert_eq!(
6903 cx.read_from_clipboard()
6904 .and_then(|item| item.text().as_deref().map(str::to_string)),
6905 Some(
6906 "for selection in selections.iter() {
6907 let mut start = selection.start;
6908 let mut end = selection.end;
6909 let is_entire_line = selection.is_empty();
6910 if is_entire_line {
6911 start = Point::new(start.row, 0);"
6912 .to_string()
6913 ),
6914 "Regular copying preserves all indentation selected",
6915 );
6916 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6917 assert_eq!(
6918 cx.read_from_clipboard()
6919 .and_then(|item| item.text().as_deref().map(str::to_string)),
6920 Some(
6921 "for selection in selections.iter() {
6922let mut start = selection.start;
6923let mut end = selection.end;
6924let is_entire_line = selection.is_empty();
6925if is_entire_line {
6926 start = Point::new(start.row, 0);"
6927 .to_string()
6928 ),
6929 "Copying with stripping should strip all leading whitespaces"
6930 );
6931
6932 cx.set_state(
6933 r#" « for selection in selections.iter() {
6934 let mut start = selection.start;
6935 let mut end = selection.end;
6936 let is_entire_line = selection.is_empty();
6937 if is_entire_line {
6938 start = Point::new(start.row, 0);ˇ»
6939 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6940 }
6941 "#,
6942 );
6943 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6944 assert_eq!(
6945 cx.read_from_clipboard()
6946 .and_then(|item| item.text().as_deref().map(str::to_string)),
6947 Some(
6948 " for selection in selections.iter() {
6949 let mut start = selection.start;
6950 let mut end = selection.end;
6951 let is_entire_line = selection.is_empty();
6952 if is_entire_line {
6953 start = Point::new(start.row, 0);"
6954 .to_string()
6955 ),
6956 "Regular copying preserves all indentation selected",
6957 );
6958 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6959 assert_eq!(
6960 cx.read_from_clipboard()
6961 .and_then(|item| item.text().as_deref().map(str::to_string)),
6962 Some(
6963 "for selection in selections.iter() {
6964let mut start = selection.start;
6965let mut end = selection.end;
6966let is_entire_line = selection.is_empty();
6967if is_entire_line {
6968 start = Point::new(start.row, 0);"
6969 .to_string()
6970 ),
6971 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
6972 );
6973
6974 cx.set_state(
6975 r#" «ˇ for selection in selections.iter() {
6976 let mut start = selection.start;
6977 let mut end = selection.end;
6978 let is_entire_line = selection.is_empty();
6979 if is_entire_line {
6980 start = Point::new(start.row, 0);»
6981 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6982 }
6983 "#,
6984 );
6985 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6986 assert_eq!(
6987 cx.read_from_clipboard()
6988 .and_then(|item| item.text().as_deref().map(str::to_string)),
6989 Some(
6990 " for selection in selections.iter() {
6991 let mut start = selection.start;
6992 let mut end = selection.end;
6993 let is_entire_line = selection.is_empty();
6994 if is_entire_line {
6995 start = Point::new(start.row, 0);"
6996 .to_string()
6997 ),
6998 "Regular copying for reverse selection works the same",
6999 );
7000 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7001 assert_eq!(
7002 cx.read_from_clipboard()
7003 .and_then(|item| item.text().as_deref().map(str::to_string)),
7004 Some(
7005 "for selection in selections.iter() {
7006let mut start = selection.start;
7007let mut end = selection.end;
7008let is_entire_line = selection.is_empty();
7009if is_entire_line {
7010 start = Point::new(start.row, 0);"
7011 .to_string()
7012 ),
7013 "Copying with stripping for reverse selection works the same"
7014 );
7015
7016 cx.set_state(
7017 r#" for selection «in selections.iter() {
7018 let mut start = selection.start;
7019 let mut end = selection.end;
7020 let is_entire_line = selection.is_empty();
7021 if is_entire_line {
7022 start = Point::new(start.row, 0);ˇ»
7023 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7024 }
7025 "#,
7026 );
7027 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7028 assert_eq!(
7029 cx.read_from_clipboard()
7030 .and_then(|item| item.text().as_deref().map(str::to_string)),
7031 Some(
7032 "in selections.iter() {
7033 let mut start = selection.start;
7034 let mut end = selection.end;
7035 let is_entire_line = selection.is_empty();
7036 if is_entire_line {
7037 start = Point::new(start.row, 0);"
7038 .to_string()
7039 ),
7040 "When selecting past the indent, the copying works as usual",
7041 );
7042 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7043 assert_eq!(
7044 cx.read_from_clipboard()
7045 .and_then(|item| item.text().as_deref().map(str::to_string)),
7046 Some(
7047 "in selections.iter() {
7048 let mut start = selection.start;
7049 let mut end = selection.end;
7050 let is_entire_line = selection.is_empty();
7051 if is_entire_line {
7052 start = Point::new(start.row, 0);"
7053 .to_string()
7054 ),
7055 "When selecting past the indent, nothing is trimmed"
7056 );
7057
7058 cx.set_state(
7059 r#" «for selection in selections.iter() {
7060 let mut start = selection.start;
7061
7062 let mut end = selection.end;
7063 let is_entire_line = selection.is_empty();
7064 if is_entire_line {
7065 start = Point::new(start.row, 0);
7066ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
7067 }
7068 "#,
7069 );
7070 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7071 assert_eq!(
7072 cx.read_from_clipboard()
7073 .and_then(|item| item.text().as_deref().map(str::to_string)),
7074 Some(
7075 "for selection in selections.iter() {
7076let mut start = selection.start;
7077
7078let mut end = selection.end;
7079let is_entire_line = selection.is_empty();
7080if is_entire_line {
7081 start = Point::new(start.row, 0);
7082"
7083 .to_string()
7084 ),
7085 "Copying with stripping should ignore empty lines"
7086 );
7087}
7088
7089#[gpui::test]
7090async fn test_paste_multiline(cx: &mut TestAppContext) {
7091 init_test(cx, |_| {});
7092
7093 let mut cx = EditorTestContext::new(cx).await;
7094 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7095
7096 // Cut an indented block, without the leading whitespace.
7097 cx.set_state(indoc! {"
7098 const a: B = (
7099 c(),
7100 «d(
7101 e,
7102 f
7103 )ˇ»
7104 );
7105 "});
7106 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7107 cx.assert_editor_state(indoc! {"
7108 const a: B = (
7109 c(),
7110 ˇ
7111 );
7112 "});
7113
7114 // Paste it at the same position.
7115 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7116 cx.assert_editor_state(indoc! {"
7117 const a: B = (
7118 c(),
7119 d(
7120 e,
7121 f
7122 )ˇ
7123 );
7124 "});
7125
7126 // Paste it at a line with a lower indent level.
7127 cx.set_state(indoc! {"
7128 ˇ
7129 const a: B = (
7130 c(),
7131 );
7132 "});
7133 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7134 cx.assert_editor_state(indoc! {"
7135 d(
7136 e,
7137 f
7138 )ˇ
7139 const a: B = (
7140 c(),
7141 );
7142 "});
7143
7144 // Cut an indented block, with the leading whitespace.
7145 cx.set_state(indoc! {"
7146 const a: B = (
7147 c(),
7148 « d(
7149 e,
7150 f
7151 )
7152 ˇ»);
7153 "});
7154 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7155 cx.assert_editor_state(indoc! {"
7156 const a: B = (
7157 c(),
7158 ˇ);
7159 "});
7160
7161 // Paste it at the same position.
7162 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7163 cx.assert_editor_state(indoc! {"
7164 const a: B = (
7165 c(),
7166 d(
7167 e,
7168 f
7169 )
7170 ˇ);
7171 "});
7172
7173 // Paste it at a line with a higher indent level.
7174 cx.set_state(indoc! {"
7175 const a: B = (
7176 c(),
7177 d(
7178 e,
7179 fˇ
7180 )
7181 );
7182 "});
7183 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7184 cx.assert_editor_state(indoc! {"
7185 const a: B = (
7186 c(),
7187 d(
7188 e,
7189 f d(
7190 e,
7191 f
7192 )
7193 ˇ
7194 )
7195 );
7196 "});
7197
7198 // Copy an indented block, starting mid-line
7199 cx.set_state(indoc! {"
7200 const a: B = (
7201 c(),
7202 somethin«g(
7203 e,
7204 f
7205 )ˇ»
7206 );
7207 "});
7208 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7209
7210 // Paste it on a line with a lower indent level
7211 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
7212 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7213 cx.assert_editor_state(indoc! {"
7214 const a: B = (
7215 c(),
7216 something(
7217 e,
7218 f
7219 )
7220 );
7221 g(
7222 e,
7223 f
7224 )ˇ"});
7225}
7226
7227#[gpui::test]
7228async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
7229 init_test(cx, |_| {});
7230
7231 cx.write_to_clipboard(ClipboardItem::new_string(
7232 " d(\n e\n );\n".into(),
7233 ));
7234
7235 let mut cx = EditorTestContext::new(cx).await;
7236 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7237
7238 cx.set_state(indoc! {"
7239 fn a() {
7240 b();
7241 if c() {
7242 ˇ
7243 }
7244 }
7245 "});
7246
7247 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7248 cx.assert_editor_state(indoc! {"
7249 fn a() {
7250 b();
7251 if c() {
7252 d(
7253 e
7254 );
7255 ˇ
7256 }
7257 }
7258 "});
7259
7260 cx.set_state(indoc! {"
7261 fn a() {
7262 b();
7263 ˇ
7264 }
7265 "});
7266
7267 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7268 cx.assert_editor_state(indoc! {"
7269 fn a() {
7270 b();
7271 d(
7272 e
7273 );
7274 ˇ
7275 }
7276 "});
7277}
7278
7279#[gpui::test]
7280fn test_select_all(cx: &mut TestAppContext) {
7281 init_test(cx, |_| {});
7282
7283 let editor = cx.add_window(|window, cx| {
7284 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
7285 build_editor(buffer, window, cx)
7286 });
7287 _ = editor.update(cx, |editor, window, cx| {
7288 editor.select_all(&SelectAll, window, cx);
7289 assert_eq!(
7290 editor.selections.display_ranges(cx),
7291 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
7292 );
7293 });
7294}
7295
7296#[gpui::test]
7297fn test_select_line(cx: &mut TestAppContext) {
7298 init_test(cx, |_| {});
7299
7300 let editor = cx.add_window(|window, cx| {
7301 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
7302 build_editor(buffer, window, cx)
7303 });
7304 _ = editor.update(cx, |editor, window, cx| {
7305 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7306 s.select_display_ranges([
7307 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7308 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7309 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7310 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
7311 ])
7312 });
7313 editor.select_line(&SelectLine, window, cx);
7314 assert_eq!(
7315 editor.selections.display_ranges(cx),
7316 vec![
7317 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
7318 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
7319 ]
7320 );
7321 });
7322
7323 _ = editor.update(cx, |editor, window, cx| {
7324 editor.select_line(&SelectLine, window, cx);
7325 assert_eq!(
7326 editor.selections.display_ranges(cx),
7327 vec![
7328 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
7329 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
7330 ]
7331 );
7332 });
7333
7334 _ = editor.update(cx, |editor, window, cx| {
7335 editor.select_line(&SelectLine, window, cx);
7336 assert_eq!(
7337 editor.selections.display_ranges(cx),
7338 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
7339 );
7340 });
7341}
7342
7343#[gpui::test]
7344async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
7345 init_test(cx, |_| {});
7346 let mut cx = EditorTestContext::new(cx).await;
7347
7348 #[track_caller]
7349 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
7350 cx.set_state(initial_state);
7351 cx.update_editor(|e, window, cx| {
7352 e.split_selection_into_lines(&Default::default(), window, cx)
7353 });
7354 cx.assert_editor_state(expected_state);
7355 }
7356
7357 // Selection starts and ends at the middle of lines, left-to-right
7358 test(
7359 &mut cx,
7360 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
7361 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7362 );
7363 // Same thing, right-to-left
7364 test(
7365 &mut cx,
7366 "aa\nb«b\ncc\ndd\neˇ»e\nff",
7367 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7368 );
7369
7370 // Whole buffer, left-to-right, last line *doesn't* end with newline
7371 test(
7372 &mut cx,
7373 "«ˇaa\nbb\ncc\ndd\nee\nff»",
7374 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7375 );
7376 // Same thing, right-to-left
7377 test(
7378 &mut cx,
7379 "«aa\nbb\ncc\ndd\nee\nffˇ»",
7380 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7381 );
7382
7383 // Whole buffer, left-to-right, last line ends with newline
7384 test(
7385 &mut cx,
7386 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
7387 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7388 );
7389 // Same thing, right-to-left
7390 test(
7391 &mut cx,
7392 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
7393 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7394 );
7395
7396 // Starts at the end of a line, ends at the start of another
7397 test(
7398 &mut cx,
7399 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
7400 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
7401 );
7402}
7403
7404#[gpui::test]
7405async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
7406 init_test(cx, |_| {});
7407
7408 let editor = cx.add_window(|window, cx| {
7409 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
7410 build_editor(buffer, window, cx)
7411 });
7412
7413 // setup
7414 _ = editor.update(cx, |editor, window, cx| {
7415 editor.fold_creases(
7416 vec![
7417 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
7418 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
7419 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
7420 ],
7421 true,
7422 window,
7423 cx,
7424 );
7425 assert_eq!(
7426 editor.display_text(cx),
7427 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7428 );
7429 });
7430
7431 _ = editor.update(cx, |editor, window, cx| {
7432 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7433 s.select_display_ranges([
7434 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7435 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7436 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7437 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
7438 ])
7439 });
7440 editor.split_selection_into_lines(&Default::default(), window, cx);
7441 assert_eq!(
7442 editor.display_text(cx),
7443 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7444 );
7445 });
7446 EditorTestContext::for_editor(editor, cx)
7447 .await
7448 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
7449
7450 _ = editor.update(cx, |editor, window, cx| {
7451 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7452 s.select_display_ranges([
7453 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
7454 ])
7455 });
7456 editor.split_selection_into_lines(&Default::default(), window, cx);
7457 assert_eq!(
7458 editor.display_text(cx),
7459 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
7460 );
7461 assert_eq!(
7462 editor.selections.display_ranges(cx),
7463 [
7464 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
7465 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
7466 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
7467 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
7468 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
7469 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
7470 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
7471 ]
7472 );
7473 });
7474 EditorTestContext::for_editor(editor, cx)
7475 .await
7476 .assert_editor_state(
7477 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
7478 );
7479}
7480
7481#[gpui::test]
7482async fn test_add_selection_above_below(cx: &mut TestAppContext) {
7483 init_test(cx, |_| {});
7484
7485 let mut cx = EditorTestContext::new(cx).await;
7486
7487 cx.set_state(indoc!(
7488 r#"abc
7489 defˇghi
7490
7491 jk
7492 nlmo
7493 "#
7494 ));
7495
7496 cx.update_editor(|editor, window, cx| {
7497 editor.add_selection_above(&Default::default(), window, cx);
7498 });
7499
7500 cx.assert_editor_state(indoc!(
7501 r#"abcˇ
7502 defˇghi
7503
7504 jk
7505 nlmo
7506 "#
7507 ));
7508
7509 cx.update_editor(|editor, window, cx| {
7510 editor.add_selection_above(&Default::default(), window, cx);
7511 });
7512
7513 cx.assert_editor_state(indoc!(
7514 r#"abcˇ
7515 defˇghi
7516
7517 jk
7518 nlmo
7519 "#
7520 ));
7521
7522 cx.update_editor(|editor, window, cx| {
7523 editor.add_selection_below(&Default::default(), window, cx);
7524 });
7525
7526 cx.assert_editor_state(indoc!(
7527 r#"abc
7528 defˇghi
7529
7530 jk
7531 nlmo
7532 "#
7533 ));
7534
7535 cx.update_editor(|editor, window, cx| {
7536 editor.undo_selection(&Default::default(), window, cx);
7537 });
7538
7539 cx.assert_editor_state(indoc!(
7540 r#"abcˇ
7541 defˇghi
7542
7543 jk
7544 nlmo
7545 "#
7546 ));
7547
7548 cx.update_editor(|editor, window, cx| {
7549 editor.redo_selection(&Default::default(), window, cx);
7550 });
7551
7552 cx.assert_editor_state(indoc!(
7553 r#"abc
7554 defˇghi
7555
7556 jk
7557 nlmo
7558 "#
7559 ));
7560
7561 cx.update_editor(|editor, window, cx| {
7562 editor.add_selection_below(&Default::default(), window, cx);
7563 });
7564
7565 cx.assert_editor_state(indoc!(
7566 r#"abc
7567 defˇghi
7568 ˇ
7569 jk
7570 nlmo
7571 "#
7572 ));
7573
7574 cx.update_editor(|editor, window, cx| {
7575 editor.add_selection_below(&Default::default(), window, cx);
7576 });
7577
7578 cx.assert_editor_state(indoc!(
7579 r#"abc
7580 defˇghi
7581 ˇ
7582 jkˇ
7583 nlmo
7584 "#
7585 ));
7586
7587 cx.update_editor(|editor, window, cx| {
7588 editor.add_selection_below(&Default::default(), window, cx);
7589 });
7590
7591 cx.assert_editor_state(indoc!(
7592 r#"abc
7593 defˇghi
7594 ˇ
7595 jkˇ
7596 nlmˇo
7597 "#
7598 ));
7599
7600 cx.update_editor(|editor, window, cx| {
7601 editor.add_selection_below(&Default::default(), window, cx);
7602 });
7603
7604 cx.assert_editor_state(indoc!(
7605 r#"abc
7606 defˇghi
7607 ˇ
7608 jkˇ
7609 nlmˇo
7610 ˇ"#
7611 ));
7612
7613 // change selections
7614 cx.set_state(indoc!(
7615 r#"abc
7616 def«ˇg»hi
7617
7618 jk
7619 nlmo
7620 "#
7621 ));
7622
7623 cx.update_editor(|editor, window, cx| {
7624 editor.add_selection_below(&Default::default(), window, cx);
7625 });
7626
7627 cx.assert_editor_state(indoc!(
7628 r#"abc
7629 def«ˇg»hi
7630
7631 jk
7632 nlm«ˇo»
7633 "#
7634 ));
7635
7636 cx.update_editor(|editor, window, cx| {
7637 editor.add_selection_below(&Default::default(), window, cx);
7638 });
7639
7640 cx.assert_editor_state(indoc!(
7641 r#"abc
7642 def«ˇg»hi
7643
7644 jk
7645 nlm«ˇo»
7646 "#
7647 ));
7648
7649 cx.update_editor(|editor, window, cx| {
7650 editor.add_selection_above(&Default::default(), window, cx);
7651 });
7652
7653 cx.assert_editor_state(indoc!(
7654 r#"abc
7655 def«ˇg»hi
7656
7657 jk
7658 nlmo
7659 "#
7660 ));
7661
7662 cx.update_editor(|editor, window, cx| {
7663 editor.add_selection_above(&Default::default(), window, cx);
7664 });
7665
7666 cx.assert_editor_state(indoc!(
7667 r#"abc
7668 def«ˇg»hi
7669
7670 jk
7671 nlmo
7672 "#
7673 ));
7674
7675 // Change selections again
7676 cx.set_state(indoc!(
7677 r#"a«bc
7678 defgˇ»hi
7679
7680 jk
7681 nlmo
7682 "#
7683 ));
7684
7685 cx.update_editor(|editor, window, cx| {
7686 editor.add_selection_below(&Default::default(), window, cx);
7687 });
7688
7689 cx.assert_editor_state(indoc!(
7690 r#"a«bcˇ»
7691 d«efgˇ»hi
7692
7693 j«kˇ»
7694 nlmo
7695 "#
7696 ));
7697
7698 cx.update_editor(|editor, window, cx| {
7699 editor.add_selection_below(&Default::default(), window, cx);
7700 });
7701 cx.assert_editor_state(indoc!(
7702 r#"a«bcˇ»
7703 d«efgˇ»hi
7704
7705 j«kˇ»
7706 n«lmoˇ»
7707 "#
7708 ));
7709 cx.update_editor(|editor, window, cx| {
7710 editor.add_selection_above(&Default::default(), window, cx);
7711 });
7712
7713 cx.assert_editor_state(indoc!(
7714 r#"a«bcˇ»
7715 d«efgˇ»hi
7716
7717 j«kˇ»
7718 nlmo
7719 "#
7720 ));
7721
7722 // Change selections again
7723 cx.set_state(indoc!(
7724 r#"abc
7725 d«ˇefghi
7726
7727 jk
7728 nlm»o
7729 "#
7730 ));
7731
7732 cx.update_editor(|editor, window, cx| {
7733 editor.add_selection_above(&Default::default(), window, cx);
7734 });
7735
7736 cx.assert_editor_state(indoc!(
7737 r#"a«ˇbc»
7738 d«ˇef»ghi
7739
7740 j«ˇk»
7741 n«ˇlm»o
7742 "#
7743 ));
7744
7745 cx.update_editor(|editor, window, cx| {
7746 editor.add_selection_below(&Default::default(), window, cx);
7747 });
7748
7749 cx.assert_editor_state(indoc!(
7750 r#"abc
7751 d«ˇef»ghi
7752
7753 j«ˇk»
7754 n«ˇlm»o
7755 "#
7756 ));
7757}
7758
7759#[gpui::test]
7760async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
7761 init_test(cx, |_| {});
7762 let mut cx = EditorTestContext::new(cx).await;
7763
7764 cx.set_state(indoc!(
7765 r#"line onˇe
7766 liˇne two
7767 line three
7768 line four"#
7769 ));
7770
7771 cx.update_editor(|editor, window, cx| {
7772 editor.add_selection_below(&Default::default(), window, cx);
7773 });
7774
7775 // test multiple cursors expand in the same direction
7776 cx.assert_editor_state(indoc!(
7777 r#"line onˇe
7778 liˇne twˇo
7779 liˇne three
7780 line four"#
7781 ));
7782
7783 cx.update_editor(|editor, window, cx| {
7784 editor.add_selection_below(&Default::default(), window, cx);
7785 });
7786
7787 cx.update_editor(|editor, window, cx| {
7788 editor.add_selection_below(&Default::default(), window, cx);
7789 });
7790
7791 // test multiple cursors expand below overflow
7792 cx.assert_editor_state(indoc!(
7793 r#"line onˇe
7794 liˇne twˇo
7795 liˇne thˇree
7796 liˇne foˇur"#
7797 ));
7798
7799 cx.update_editor(|editor, window, cx| {
7800 editor.add_selection_above(&Default::default(), window, cx);
7801 });
7802
7803 // test multiple cursors retrieves back correctly
7804 cx.assert_editor_state(indoc!(
7805 r#"line onˇe
7806 liˇne twˇo
7807 liˇne thˇree
7808 line four"#
7809 ));
7810
7811 cx.update_editor(|editor, window, cx| {
7812 editor.add_selection_above(&Default::default(), window, cx);
7813 });
7814
7815 cx.update_editor(|editor, window, cx| {
7816 editor.add_selection_above(&Default::default(), window, cx);
7817 });
7818
7819 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
7820 cx.assert_editor_state(indoc!(
7821 r#"liˇne onˇe
7822 liˇne two
7823 line three
7824 line four"#
7825 ));
7826
7827 cx.update_editor(|editor, window, cx| {
7828 editor.undo_selection(&Default::default(), window, cx);
7829 });
7830
7831 // test undo
7832 cx.assert_editor_state(indoc!(
7833 r#"line onˇe
7834 liˇne twˇo
7835 line three
7836 line four"#
7837 ));
7838
7839 cx.update_editor(|editor, window, cx| {
7840 editor.redo_selection(&Default::default(), window, cx);
7841 });
7842
7843 // test redo
7844 cx.assert_editor_state(indoc!(
7845 r#"liˇne onˇe
7846 liˇne two
7847 line three
7848 line four"#
7849 ));
7850
7851 cx.set_state(indoc!(
7852 r#"abcd
7853 ef«ghˇ»
7854 ijkl
7855 «mˇ»nop"#
7856 ));
7857
7858 cx.update_editor(|editor, window, cx| {
7859 editor.add_selection_above(&Default::default(), window, cx);
7860 });
7861
7862 // test multiple selections expand in the same direction
7863 cx.assert_editor_state(indoc!(
7864 r#"ab«cdˇ»
7865 ef«ghˇ»
7866 «iˇ»jkl
7867 «mˇ»nop"#
7868 ));
7869
7870 cx.update_editor(|editor, window, cx| {
7871 editor.add_selection_above(&Default::default(), window, cx);
7872 });
7873
7874 // test multiple selection upward overflow
7875 cx.assert_editor_state(indoc!(
7876 r#"ab«cdˇ»
7877 «eˇ»f«ghˇ»
7878 «iˇ»jkl
7879 «mˇ»nop"#
7880 ));
7881
7882 cx.update_editor(|editor, window, cx| {
7883 editor.add_selection_below(&Default::default(), window, cx);
7884 });
7885
7886 // test multiple selection retrieves back correctly
7887 cx.assert_editor_state(indoc!(
7888 r#"abcd
7889 ef«ghˇ»
7890 «iˇ»jkl
7891 «mˇ»nop"#
7892 ));
7893
7894 cx.update_editor(|editor, window, cx| {
7895 editor.add_selection_below(&Default::default(), window, cx);
7896 });
7897
7898 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
7899 cx.assert_editor_state(indoc!(
7900 r#"abcd
7901 ef«ghˇ»
7902 ij«klˇ»
7903 «mˇ»nop"#
7904 ));
7905
7906 cx.update_editor(|editor, window, cx| {
7907 editor.undo_selection(&Default::default(), window, cx);
7908 });
7909
7910 // test undo
7911 cx.assert_editor_state(indoc!(
7912 r#"abcd
7913 ef«ghˇ»
7914 «iˇ»jkl
7915 «mˇ»nop"#
7916 ));
7917
7918 cx.update_editor(|editor, window, cx| {
7919 editor.redo_selection(&Default::default(), window, cx);
7920 });
7921
7922 // test redo
7923 cx.assert_editor_state(indoc!(
7924 r#"abcd
7925 ef«ghˇ»
7926 ij«klˇ»
7927 «mˇ»nop"#
7928 ));
7929}
7930
7931#[gpui::test]
7932async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
7933 init_test(cx, |_| {});
7934 let mut cx = EditorTestContext::new(cx).await;
7935
7936 cx.set_state(indoc!(
7937 r#"line onˇe
7938 liˇne two
7939 line three
7940 line four"#
7941 ));
7942
7943 cx.update_editor(|editor, window, cx| {
7944 editor.add_selection_below(&Default::default(), window, cx);
7945 editor.add_selection_below(&Default::default(), window, cx);
7946 editor.add_selection_below(&Default::default(), window, cx);
7947 });
7948
7949 // initial state with two multi cursor groups
7950 cx.assert_editor_state(indoc!(
7951 r#"line onˇe
7952 liˇne twˇo
7953 liˇne thˇree
7954 liˇne foˇur"#
7955 ));
7956
7957 // add single cursor in middle - simulate opt click
7958 cx.update_editor(|editor, window, cx| {
7959 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
7960 editor.begin_selection(new_cursor_point, true, 1, window, cx);
7961 editor.end_selection(window, cx);
7962 });
7963
7964 cx.assert_editor_state(indoc!(
7965 r#"line onˇe
7966 liˇne twˇo
7967 liˇneˇ thˇree
7968 liˇne foˇur"#
7969 ));
7970
7971 cx.update_editor(|editor, window, cx| {
7972 editor.add_selection_above(&Default::default(), window, cx);
7973 });
7974
7975 // test new added selection expands above and existing selection shrinks
7976 cx.assert_editor_state(indoc!(
7977 r#"line onˇe
7978 liˇneˇ twˇo
7979 liˇneˇ thˇree
7980 line four"#
7981 ));
7982
7983 cx.update_editor(|editor, window, cx| {
7984 editor.add_selection_above(&Default::default(), window, cx);
7985 });
7986
7987 // test new added selection expands above and existing selection shrinks
7988 cx.assert_editor_state(indoc!(
7989 r#"lineˇ onˇe
7990 liˇneˇ twˇo
7991 lineˇ three
7992 line four"#
7993 ));
7994
7995 // intial state with two selection groups
7996 cx.set_state(indoc!(
7997 r#"abcd
7998 ef«ghˇ»
7999 ijkl
8000 «mˇ»nop"#
8001 ));
8002
8003 cx.update_editor(|editor, window, cx| {
8004 editor.add_selection_above(&Default::default(), window, cx);
8005 editor.add_selection_above(&Default::default(), window, cx);
8006 });
8007
8008 cx.assert_editor_state(indoc!(
8009 r#"ab«cdˇ»
8010 «eˇ»f«ghˇ»
8011 «iˇ»jkl
8012 «mˇ»nop"#
8013 ));
8014
8015 // add single selection in middle - simulate opt drag
8016 cx.update_editor(|editor, window, cx| {
8017 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
8018 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8019 editor.update_selection(
8020 DisplayPoint::new(DisplayRow(2), 4),
8021 0,
8022 gpui::Point::<f32>::default(),
8023 window,
8024 cx,
8025 );
8026 editor.end_selection(window, cx);
8027 });
8028
8029 cx.assert_editor_state(indoc!(
8030 r#"ab«cdˇ»
8031 «eˇ»f«ghˇ»
8032 «iˇ»jk«lˇ»
8033 «mˇ»nop"#
8034 ));
8035
8036 cx.update_editor(|editor, window, cx| {
8037 editor.add_selection_below(&Default::default(), window, cx);
8038 });
8039
8040 // test new added selection expands below, others shrinks from above
8041 cx.assert_editor_state(indoc!(
8042 r#"abcd
8043 ef«ghˇ»
8044 «iˇ»jk«lˇ»
8045 «mˇ»no«pˇ»"#
8046 ));
8047}
8048
8049#[gpui::test]
8050async fn test_select_next(cx: &mut TestAppContext) {
8051 init_test(cx, |_| {});
8052
8053 let mut cx = EditorTestContext::new(cx).await;
8054 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8055
8056 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8057 .unwrap();
8058 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8059
8060 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8061 .unwrap();
8062 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8063
8064 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8065 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8066
8067 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8068 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8069
8070 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8071 .unwrap();
8072 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8073
8074 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8075 .unwrap();
8076 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8077
8078 // Test selection direction should be preserved
8079 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8080
8081 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8082 .unwrap();
8083 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
8084}
8085
8086#[gpui::test]
8087async fn test_select_all_matches(cx: &mut TestAppContext) {
8088 init_test(cx, |_| {});
8089
8090 let mut cx = EditorTestContext::new(cx).await;
8091
8092 // Test caret-only selections
8093 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8094 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8095 .unwrap();
8096 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8097
8098 // Test left-to-right selections
8099 cx.set_state("abc\n«abcˇ»\nabc");
8100 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8101 .unwrap();
8102 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
8103
8104 // Test right-to-left selections
8105 cx.set_state("abc\n«ˇabc»\nabc");
8106 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8107 .unwrap();
8108 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
8109
8110 // Test selecting whitespace with caret selection
8111 cx.set_state("abc\nˇ abc\nabc");
8112 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8113 .unwrap();
8114 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8115
8116 // Test selecting whitespace with left-to-right selection
8117 cx.set_state("abc\n«ˇ »abc\nabc");
8118 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8119 .unwrap();
8120 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
8121
8122 // Test no matches with right-to-left selection
8123 cx.set_state("abc\n« ˇ»abc\nabc");
8124 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8125 .unwrap();
8126 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8127
8128 // Test with a single word and clip_at_line_ends=true (#29823)
8129 cx.set_state("aˇbc");
8130 cx.update_editor(|e, window, cx| {
8131 e.set_clip_at_line_ends(true, cx);
8132 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8133 e.set_clip_at_line_ends(false, cx);
8134 });
8135 cx.assert_editor_state("«abcˇ»");
8136}
8137
8138#[gpui::test]
8139async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
8140 init_test(cx, |_| {});
8141
8142 let mut cx = EditorTestContext::new(cx).await;
8143
8144 let large_body_1 = "\nd".repeat(200);
8145 let large_body_2 = "\ne".repeat(200);
8146
8147 cx.set_state(&format!(
8148 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
8149 ));
8150 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
8151 let scroll_position = editor.scroll_position(cx);
8152 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
8153 scroll_position
8154 });
8155
8156 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8157 .unwrap();
8158 cx.assert_editor_state(&format!(
8159 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
8160 ));
8161 let scroll_position_after_selection =
8162 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
8163 assert_eq!(
8164 initial_scroll_position, scroll_position_after_selection,
8165 "Scroll position should not change after selecting all matches"
8166 );
8167}
8168
8169#[gpui::test]
8170async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
8171 init_test(cx, |_| {});
8172
8173 let mut cx = EditorLspTestContext::new_rust(
8174 lsp::ServerCapabilities {
8175 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8176 ..Default::default()
8177 },
8178 cx,
8179 )
8180 .await;
8181
8182 cx.set_state(indoc! {"
8183 line 1
8184 line 2
8185 linˇe 3
8186 line 4
8187 line 5
8188 "});
8189
8190 // Make an edit
8191 cx.update_editor(|editor, window, cx| {
8192 editor.handle_input("X", window, cx);
8193 });
8194
8195 // Move cursor to a different position
8196 cx.update_editor(|editor, window, cx| {
8197 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8198 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
8199 });
8200 });
8201
8202 cx.assert_editor_state(indoc! {"
8203 line 1
8204 line 2
8205 linXe 3
8206 line 4
8207 liˇne 5
8208 "});
8209
8210 cx.lsp
8211 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8212 Ok(Some(vec![lsp::TextEdit::new(
8213 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8214 "PREFIX ".to_string(),
8215 )]))
8216 });
8217
8218 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
8219 .unwrap()
8220 .await
8221 .unwrap();
8222
8223 cx.assert_editor_state(indoc! {"
8224 PREFIX line 1
8225 line 2
8226 linXe 3
8227 line 4
8228 liˇne 5
8229 "});
8230
8231 // Undo formatting
8232 cx.update_editor(|editor, window, cx| {
8233 editor.undo(&Default::default(), window, cx);
8234 });
8235
8236 // Verify cursor moved back to position after edit
8237 cx.assert_editor_state(indoc! {"
8238 line 1
8239 line 2
8240 linXˇe 3
8241 line 4
8242 line 5
8243 "});
8244}
8245
8246#[gpui::test]
8247async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) {
8248 init_test(cx, |_| {});
8249
8250 let mut cx = EditorTestContext::new(cx).await;
8251
8252 let provider = cx.new(|_| FakeEditPredictionProvider::default());
8253 cx.update_editor(|editor, window, cx| {
8254 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
8255 });
8256
8257 cx.set_state(indoc! {"
8258 line 1
8259 line 2
8260 linˇe 3
8261 line 4
8262 line 5
8263 line 6
8264 line 7
8265 line 8
8266 line 9
8267 line 10
8268 "});
8269
8270 let snapshot = cx.buffer_snapshot();
8271 let edit_position = snapshot.anchor_after(Point::new(2, 4));
8272
8273 cx.update(|_, cx| {
8274 provider.update(cx, |provider, _| {
8275 provider.set_edit_prediction(Some(edit_prediction::EditPrediction::Local {
8276 id: None,
8277 edits: vec![(edit_position..edit_position, "X".into())],
8278 edit_preview: None,
8279 }))
8280 })
8281 });
8282
8283 cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
8284 cx.update_editor(|editor, window, cx| {
8285 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
8286 });
8287
8288 cx.assert_editor_state(indoc! {"
8289 line 1
8290 line 2
8291 lineXˇ 3
8292 line 4
8293 line 5
8294 line 6
8295 line 7
8296 line 8
8297 line 9
8298 line 10
8299 "});
8300
8301 cx.update_editor(|editor, window, cx| {
8302 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8303 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
8304 });
8305 });
8306
8307 cx.assert_editor_state(indoc! {"
8308 line 1
8309 line 2
8310 lineX 3
8311 line 4
8312 line 5
8313 line 6
8314 line 7
8315 line 8
8316 line 9
8317 liˇne 10
8318 "});
8319
8320 cx.update_editor(|editor, window, cx| {
8321 editor.undo(&Default::default(), window, cx);
8322 });
8323
8324 cx.assert_editor_state(indoc! {"
8325 line 1
8326 line 2
8327 lineˇ 3
8328 line 4
8329 line 5
8330 line 6
8331 line 7
8332 line 8
8333 line 9
8334 line 10
8335 "});
8336}
8337
8338#[gpui::test]
8339async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
8340 init_test(cx, |_| {});
8341
8342 let mut cx = EditorTestContext::new(cx).await;
8343 cx.set_state(
8344 r#"let foo = 2;
8345lˇet foo = 2;
8346let fooˇ = 2;
8347let foo = 2;
8348let foo = ˇ2;"#,
8349 );
8350
8351 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8352 .unwrap();
8353 cx.assert_editor_state(
8354 r#"let foo = 2;
8355«letˇ» foo = 2;
8356let «fooˇ» = 2;
8357let foo = 2;
8358let foo = «2ˇ»;"#,
8359 );
8360
8361 // noop for multiple selections with different contents
8362 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8363 .unwrap();
8364 cx.assert_editor_state(
8365 r#"let foo = 2;
8366«letˇ» foo = 2;
8367let «fooˇ» = 2;
8368let foo = 2;
8369let foo = «2ˇ»;"#,
8370 );
8371
8372 // Test last selection direction should be preserved
8373 cx.set_state(
8374 r#"let foo = 2;
8375let foo = 2;
8376let «fooˇ» = 2;
8377let «ˇfoo» = 2;
8378let foo = 2;"#,
8379 );
8380
8381 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8382 .unwrap();
8383 cx.assert_editor_state(
8384 r#"let foo = 2;
8385let foo = 2;
8386let «fooˇ» = 2;
8387let «ˇfoo» = 2;
8388let «ˇfoo» = 2;"#,
8389 );
8390}
8391
8392#[gpui::test]
8393async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
8394 init_test(cx, |_| {});
8395
8396 let mut cx =
8397 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
8398
8399 cx.assert_editor_state(indoc! {"
8400 ˇbbb
8401 ccc
8402
8403 bbb
8404 ccc
8405 "});
8406 cx.dispatch_action(SelectPrevious::default());
8407 cx.assert_editor_state(indoc! {"
8408 «bbbˇ»
8409 ccc
8410
8411 bbb
8412 ccc
8413 "});
8414 cx.dispatch_action(SelectPrevious::default());
8415 cx.assert_editor_state(indoc! {"
8416 «bbbˇ»
8417 ccc
8418
8419 «bbbˇ»
8420 ccc
8421 "});
8422}
8423
8424#[gpui::test]
8425async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
8426 init_test(cx, |_| {});
8427
8428 let mut cx = EditorTestContext::new(cx).await;
8429 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8430
8431 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8432 .unwrap();
8433 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8434
8435 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8436 .unwrap();
8437 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8438
8439 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8440 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8441
8442 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8443 cx.assert_editor_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 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
8448
8449 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8450 .unwrap();
8451 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8452}
8453
8454#[gpui::test]
8455async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
8456 init_test(cx, |_| {});
8457
8458 let mut cx = EditorTestContext::new(cx).await;
8459 cx.set_state("aˇ");
8460
8461 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8462 .unwrap();
8463 cx.assert_editor_state("«aˇ»");
8464 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8465 .unwrap();
8466 cx.assert_editor_state("«aˇ»");
8467}
8468
8469#[gpui::test]
8470async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
8471 init_test(cx, |_| {});
8472
8473 let mut cx = EditorTestContext::new(cx).await;
8474 cx.set_state(
8475 r#"let foo = 2;
8476lˇet foo = 2;
8477let fooˇ = 2;
8478let foo = 2;
8479let foo = ˇ2;"#,
8480 );
8481
8482 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8483 .unwrap();
8484 cx.assert_editor_state(
8485 r#"let foo = 2;
8486«letˇ» foo = 2;
8487let «fooˇ» = 2;
8488let foo = 2;
8489let foo = «2ˇ»;"#,
8490 );
8491
8492 // noop for multiple selections with different contents
8493 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8494 .unwrap();
8495 cx.assert_editor_state(
8496 r#"let foo = 2;
8497«letˇ» foo = 2;
8498let «fooˇ» = 2;
8499let foo = 2;
8500let foo = «2ˇ»;"#,
8501 );
8502}
8503
8504#[gpui::test]
8505async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
8506 init_test(cx, |_| {});
8507
8508 let mut cx = EditorTestContext::new(cx).await;
8509 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8510
8511 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8512 .unwrap();
8513 // selection direction is preserved
8514 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8515
8516 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8517 .unwrap();
8518 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8519
8520 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8521 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8522
8523 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8524 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8525
8526 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8527 .unwrap();
8528 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
8529
8530 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8531 .unwrap();
8532 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
8533}
8534
8535#[gpui::test]
8536async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
8537 init_test(cx, |_| {});
8538
8539 let language = Arc::new(Language::new(
8540 LanguageConfig::default(),
8541 Some(tree_sitter_rust::LANGUAGE.into()),
8542 ));
8543
8544 let text = r#"
8545 use mod1::mod2::{mod3, mod4};
8546
8547 fn fn_1(param1: bool, param2: &str) {
8548 let var1 = "text";
8549 }
8550 "#
8551 .unindent();
8552
8553 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8554 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8555 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8556
8557 editor
8558 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8559 .await;
8560
8561 editor.update_in(cx, |editor, window, cx| {
8562 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8563 s.select_display_ranges([
8564 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
8565 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
8566 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
8567 ]);
8568 });
8569 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8570 });
8571 editor.update(cx, |editor, cx| {
8572 assert_text_with_selections(
8573 editor,
8574 indoc! {r#"
8575 use mod1::mod2::{mod3, «mod4ˇ»};
8576
8577 fn fn_1«ˇ(param1: bool, param2: &str)» {
8578 let var1 = "«ˇtext»";
8579 }
8580 "#},
8581 cx,
8582 );
8583 });
8584
8585 editor.update_in(cx, |editor, window, cx| {
8586 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8587 });
8588 editor.update(cx, |editor, cx| {
8589 assert_text_with_selections(
8590 editor,
8591 indoc! {r#"
8592 use mod1::mod2::«{mod3, mod4}ˇ»;
8593
8594 «ˇfn fn_1(param1: bool, param2: &str) {
8595 let var1 = "text";
8596 }»
8597 "#},
8598 cx,
8599 );
8600 });
8601
8602 editor.update_in(cx, |editor, window, cx| {
8603 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8604 });
8605 assert_eq!(
8606 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
8607 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8608 );
8609
8610 // Trying to expand the selected syntax node one more time has no effect.
8611 editor.update_in(cx, |editor, window, cx| {
8612 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8613 });
8614 assert_eq!(
8615 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
8616 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8617 );
8618
8619 editor.update_in(cx, |editor, window, cx| {
8620 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8621 });
8622 editor.update(cx, |editor, cx| {
8623 assert_text_with_selections(
8624 editor,
8625 indoc! {r#"
8626 use mod1::mod2::«{mod3, mod4}ˇ»;
8627
8628 «ˇfn fn_1(param1: bool, param2: &str) {
8629 let var1 = "text";
8630 }»
8631 "#},
8632 cx,
8633 );
8634 });
8635
8636 editor.update_in(cx, |editor, window, cx| {
8637 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8638 });
8639 editor.update(cx, |editor, cx| {
8640 assert_text_with_selections(
8641 editor,
8642 indoc! {r#"
8643 use mod1::mod2::{mod3, «mod4ˇ»};
8644
8645 fn fn_1«ˇ(param1: bool, param2: &str)» {
8646 let var1 = "«ˇtext»";
8647 }
8648 "#},
8649 cx,
8650 );
8651 });
8652
8653 editor.update_in(cx, |editor, window, cx| {
8654 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8655 });
8656 editor.update(cx, |editor, cx| {
8657 assert_text_with_selections(
8658 editor,
8659 indoc! {r#"
8660 use mod1::mod2::{mod3, moˇd4};
8661
8662 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
8663 let var1 = "teˇxt";
8664 }
8665 "#},
8666 cx,
8667 );
8668 });
8669
8670 // Trying to shrink the selected syntax node one more time has no effect.
8671 editor.update_in(cx, |editor, window, cx| {
8672 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8673 });
8674 editor.update_in(cx, |editor, _, cx| {
8675 assert_text_with_selections(
8676 editor,
8677 indoc! {r#"
8678 use mod1::mod2::{mod3, moˇd4};
8679
8680 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
8681 let var1 = "teˇxt";
8682 }
8683 "#},
8684 cx,
8685 );
8686 });
8687
8688 // Ensure that we keep expanding the selection if the larger selection starts or ends within
8689 // a fold.
8690 editor.update_in(cx, |editor, window, cx| {
8691 editor.fold_creases(
8692 vec![
8693 Crease::simple(
8694 Point::new(0, 21)..Point::new(0, 24),
8695 FoldPlaceholder::test(),
8696 ),
8697 Crease::simple(
8698 Point::new(3, 20)..Point::new(3, 22),
8699 FoldPlaceholder::test(),
8700 ),
8701 ],
8702 true,
8703 window,
8704 cx,
8705 );
8706 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8707 });
8708 editor.update(cx, |editor, cx| {
8709 assert_text_with_selections(
8710 editor,
8711 indoc! {r#"
8712 use mod1::mod2::«{mod3, mod4}ˇ»;
8713
8714 fn fn_1«ˇ(param1: bool, param2: &str)» {
8715 let var1 = "«ˇtext»";
8716 }
8717 "#},
8718 cx,
8719 );
8720 });
8721}
8722
8723#[gpui::test]
8724async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
8725 init_test(cx, |_| {});
8726
8727 let language = Arc::new(Language::new(
8728 LanguageConfig::default(),
8729 Some(tree_sitter_rust::LANGUAGE.into()),
8730 ));
8731
8732 let text = "let a = 2;";
8733
8734 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8735 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8736 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8737
8738 editor
8739 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8740 .await;
8741
8742 // Test case 1: Cursor at end of word
8743 editor.update_in(cx, |editor, window, cx| {
8744 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8745 s.select_display_ranges([
8746 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
8747 ]);
8748 });
8749 });
8750 editor.update(cx, |editor, cx| {
8751 assert_text_with_selections(editor, "let aˇ = 2;", cx);
8752 });
8753 editor.update_in(cx, |editor, window, cx| {
8754 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8755 });
8756 editor.update(cx, |editor, cx| {
8757 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
8758 });
8759 editor.update_in(cx, |editor, window, cx| {
8760 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8761 });
8762 editor.update(cx, |editor, cx| {
8763 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
8764 });
8765
8766 // Test case 2: Cursor at end of statement
8767 editor.update_in(cx, |editor, window, cx| {
8768 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8769 s.select_display_ranges([
8770 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
8771 ]);
8772 });
8773 });
8774 editor.update(cx, |editor, cx| {
8775 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
8776 });
8777 editor.update_in(cx, |editor, window, cx| {
8778 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8779 });
8780 editor.update(cx, |editor, cx| {
8781 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
8782 });
8783}
8784
8785#[gpui::test]
8786async fn test_select_larger_syntax_node_for_cursor_at_symbol(cx: &mut TestAppContext) {
8787 init_test(cx, |_| {});
8788
8789 let language = Arc::new(Language::new(
8790 LanguageConfig {
8791 name: "JavaScript".into(),
8792 ..Default::default()
8793 },
8794 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
8795 ));
8796
8797 let text = r#"
8798 let a = {
8799 key: "value",
8800 };
8801 "#
8802 .unindent();
8803
8804 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8805 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8806 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8807
8808 editor
8809 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8810 .await;
8811
8812 // Test case 1: Cursor after '{'
8813 editor.update_in(cx, |editor, window, cx| {
8814 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8815 s.select_display_ranges([
8816 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 9)
8817 ]);
8818 });
8819 });
8820 editor.update(cx, |editor, cx| {
8821 assert_text_with_selections(
8822 editor,
8823 indoc! {r#"
8824 let a = {ˇ
8825 key: "value",
8826 };
8827 "#},
8828 cx,
8829 );
8830 });
8831 editor.update_in(cx, |editor, window, cx| {
8832 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8833 });
8834 editor.update(cx, |editor, cx| {
8835 assert_text_with_selections(
8836 editor,
8837 indoc! {r#"
8838 let a = «ˇ{
8839 key: "value",
8840 }»;
8841 "#},
8842 cx,
8843 );
8844 });
8845
8846 // Test case 2: Cursor after ':'
8847 editor.update_in(cx, |editor, window, cx| {
8848 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8849 s.select_display_ranges([
8850 DisplayPoint::new(DisplayRow(1), 8)..DisplayPoint::new(DisplayRow(1), 8)
8851 ]);
8852 });
8853 });
8854 editor.update(cx, |editor, cx| {
8855 assert_text_with_selections(
8856 editor,
8857 indoc! {r#"
8858 let a = {
8859 key:ˇ "value",
8860 };
8861 "#},
8862 cx,
8863 );
8864 });
8865 editor.update_in(cx, |editor, window, cx| {
8866 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8867 });
8868 editor.update(cx, |editor, cx| {
8869 assert_text_with_selections(
8870 editor,
8871 indoc! {r#"
8872 let a = {
8873 «ˇkey: "value"»,
8874 };
8875 "#},
8876 cx,
8877 );
8878 });
8879 editor.update_in(cx, |editor, window, cx| {
8880 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8881 });
8882 editor.update(cx, |editor, cx| {
8883 assert_text_with_selections(
8884 editor,
8885 indoc! {r#"
8886 let a = «ˇ{
8887 key: "value",
8888 }»;
8889 "#},
8890 cx,
8891 );
8892 });
8893
8894 // Test case 3: Cursor after ','
8895 editor.update_in(cx, |editor, window, cx| {
8896 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8897 s.select_display_ranges([
8898 DisplayPoint::new(DisplayRow(1), 17)..DisplayPoint::new(DisplayRow(1), 17)
8899 ]);
8900 });
8901 });
8902 editor.update(cx, |editor, cx| {
8903 assert_text_with_selections(
8904 editor,
8905 indoc! {r#"
8906 let a = {
8907 key: "value",ˇ
8908 };
8909 "#},
8910 cx,
8911 );
8912 });
8913 editor.update_in(cx, |editor, window, cx| {
8914 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8915 });
8916 editor.update(cx, |editor, cx| {
8917 assert_text_with_selections(
8918 editor,
8919 indoc! {r#"
8920 let a = «ˇ{
8921 key: "value",
8922 }»;
8923 "#},
8924 cx,
8925 );
8926 });
8927
8928 // Test case 4: Cursor after ';'
8929 editor.update_in(cx, |editor, window, cx| {
8930 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8931 s.select_display_ranges([
8932 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)
8933 ]);
8934 });
8935 });
8936 editor.update(cx, |editor, cx| {
8937 assert_text_with_selections(
8938 editor,
8939 indoc! {r#"
8940 let a = {
8941 key: "value",
8942 };ˇ
8943 "#},
8944 cx,
8945 );
8946 });
8947 editor.update_in(cx, |editor, window, cx| {
8948 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8949 });
8950 editor.update(cx, |editor, cx| {
8951 assert_text_with_selections(
8952 editor,
8953 indoc! {r#"
8954 «ˇlet a = {
8955 key: "value",
8956 };
8957 »"#},
8958 cx,
8959 );
8960 });
8961}
8962
8963#[gpui::test]
8964async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
8965 init_test(cx, |_| {});
8966
8967 let language = Arc::new(Language::new(
8968 LanguageConfig::default(),
8969 Some(tree_sitter_rust::LANGUAGE.into()),
8970 ));
8971
8972 let text = r#"
8973 use mod1::mod2::{mod3, mod4};
8974
8975 fn fn_1(param1: bool, param2: &str) {
8976 let var1 = "hello world";
8977 }
8978 "#
8979 .unindent();
8980
8981 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8982 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8983 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8984
8985 editor
8986 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8987 .await;
8988
8989 // Test 1: Cursor on a letter of a string word
8990 editor.update_in(cx, |editor, window, cx| {
8991 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8992 s.select_display_ranges([
8993 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
8994 ]);
8995 });
8996 });
8997 editor.update_in(cx, |editor, window, cx| {
8998 assert_text_with_selections(
8999 editor,
9000 indoc! {r#"
9001 use mod1::mod2::{mod3, mod4};
9002
9003 fn fn_1(param1: bool, param2: &str) {
9004 let var1 = "hˇello world";
9005 }
9006 "#},
9007 cx,
9008 );
9009 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9010 assert_text_with_selections(
9011 editor,
9012 indoc! {r#"
9013 use mod1::mod2::{mod3, mod4};
9014
9015 fn fn_1(param1: bool, param2: &str) {
9016 let var1 = "«ˇhello» world";
9017 }
9018 "#},
9019 cx,
9020 );
9021 });
9022
9023 // Test 2: Partial selection within a word
9024 editor.update_in(cx, |editor, window, cx| {
9025 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9026 s.select_display_ranges([
9027 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
9028 ]);
9029 });
9030 });
9031 editor.update_in(cx, |editor, window, cx| {
9032 assert_text_with_selections(
9033 editor,
9034 indoc! {r#"
9035 use mod1::mod2::{mod3, mod4};
9036
9037 fn fn_1(param1: bool, param2: &str) {
9038 let var1 = "h«elˇ»lo world";
9039 }
9040 "#},
9041 cx,
9042 );
9043 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9044 assert_text_with_selections(
9045 editor,
9046 indoc! {r#"
9047 use mod1::mod2::{mod3, mod4};
9048
9049 fn fn_1(param1: bool, param2: &str) {
9050 let var1 = "«ˇhello» world";
9051 }
9052 "#},
9053 cx,
9054 );
9055 });
9056
9057 // Test 3: Complete word already selected
9058 editor.update_in(cx, |editor, window, cx| {
9059 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9060 s.select_display_ranges([
9061 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
9062 ]);
9063 });
9064 });
9065 editor.update_in(cx, |editor, window, cx| {
9066 assert_text_with_selections(
9067 editor,
9068 indoc! {r#"
9069 use mod1::mod2::{mod3, mod4};
9070
9071 fn fn_1(param1: bool, param2: &str) {
9072 let var1 = "«helloˇ» world";
9073 }
9074 "#},
9075 cx,
9076 );
9077 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9078 assert_text_with_selections(
9079 editor,
9080 indoc! {r#"
9081 use mod1::mod2::{mod3, mod4};
9082
9083 fn fn_1(param1: bool, param2: &str) {
9084 let var1 = "«hello worldˇ»";
9085 }
9086 "#},
9087 cx,
9088 );
9089 });
9090
9091 // Test 4: Selection spanning across words
9092 editor.update_in(cx, |editor, window, cx| {
9093 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9094 s.select_display_ranges([
9095 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
9096 ]);
9097 });
9098 });
9099 editor.update_in(cx, |editor, window, cx| {
9100 assert_text_with_selections(
9101 editor,
9102 indoc! {r#"
9103 use mod1::mod2::{mod3, mod4};
9104
9105 fn fn_1(param1: bool, param2: &str) {
9106 let var1 = "hel«lo woˇ»rld";
9107 }
9108 "#},
9109 cx,
9110 );
9111 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9112 assert_text_with_selections(
9113 editor,
9114 indoc! {r#"
9115 use mod1::mod2::{mod3, mod4};
9116
9117 fn fn_1(param1: bool, param2: &str) {
9118 let var1 = "«ˇhello world»";
9119 }
9120 "#},
9121 cx,
9122 );
9123 });
9124
9125 // Test 5: Expansion beyond string
9126 editor.update_in(cx, |editor, window, cx| {
9127 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9128 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9129 assert_text_with_selections(
9130 editor,
9131 indoc! {r#"
9132 use mod1::mod2::{mod3, mod4};
9133
9134 fn fn_1(param1: bool, param2: &str) {
9135 «ˇlet var1 = "hello world";»
9136 }
9137 "#},
9138 cx,
9139 );
9140 });
9141}
9142
9143#[gpui::test]
9144async fn test_unwrap_syntax_nodes(cx: &mut gpui::TestAppContext) {
9145 init_test(cx, |_| {});
9146
9147 let mut cx = EditorTestContext::new(cx).await;
9148
9149 let language = Arc::new(Language::new(
9150 LanguageConfig::default(),
9151 Some(tree_sitter_rust::LANGUAGE.into()),
9152 ));
9153
9154 cx.update_buffer(|buffer, cx| {
9155 buffer.set_language(Some(language), cx);
9156 });
9157
9158 cx.set_state(indoc! { r#"use mod1::{mod2::{«mod3ˇ», mod4}, mod5::{mod6, «mod7ˇ»}};"# });
9159 cx.update_editor(|editor, window, cx| {
9160 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9161 });
9162
9163 cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# });
9164
9165 cx.set_state(indoc! { r#"fn a() {
9166 // what
9167 // a
9168 // ˇlong
9169 // method
9170 // I
9171 // sure
9172 // hope
9173 // it
9174 // works
9175 }"# });
9176
9177 let buffer = cx.update_multibuffer(|multibuffer, _| multibuffer.as_singleton().unwrap());
9178 let multi_buffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
9179 cx.update(|_, cx| {
9180 multi_buffer.update(cx, |multi_buffer, cx| {
9181 multi_buffer.set_excerpts_for_path(
9182 PathKey::for_buffer(&buffer, cx),
9183 buffer,
9184 [Point::new(1, 0)..Point::new(1, 0)],
9185 3,
9186 cx,
9187 );
9188 });
9189 });
9190
9191 let editor2 = cx.new_window_entity(|window, cx| {
9192 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
9193 });
9194
9195 let mut cx = EditorTestContext::for_editor_in(editor2, &mut cx).await;
9196 cx.update_editor(|editor, window, cx| {
9197 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
9198 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)]);
9199 })
9200 });
9201
9202 cx.assert_editor_state(indoc! { "
9203 fn a() {
9204 // what
9205 // a
9206 ˇ // long
9207 // method"});
9208
9209 cx.update_editor(|editor, window, cx| {
9210 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9211 });
9212
9213 // Although we could potentially make the action work when the syntax node
9214 // is half-hidden, it seems a bit dangerous as you can't easily tell what it
9215 // did. Maybe we could also expand the excerpt to contain the range?
9216 cx.assert_editor_state(indoc! { "
9217 fn a() {
9218 // what
9219 // a
9220 ˇ // long
9221 // method"});
9222}
9223
9224#[gpui::test]
9225async fn test_fold_function_bodies(cx: &mut TestAppContext) {
9226 init_test(cx, |_| {});
9227
9228 let base_text = r#"
9229 impl A {
9230 // this is an uncommitted comment
9231
9232 fn b() {
9233 c();
9234 }
9235
9236 // this is another uncommitted comment
9237
9238 fn d() {
9239 // e
9240 // f
9241 }
9242 }
9243
9244 fn g() {
9245 // h
9246 }
9247 "#
9248 .unindent();
9249
9250 let text = r#"
9251 ˇimpl A {
9252
9253 fn b() {
9254 c();
9255 }
9256
9257 fn d() {
9258 // e
9259 // f
9260 }
9261 }
9262
9263 fn g() {
9264 // h
9265 }
9266 "#
9267 .unindent();
9268
9269 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9270 cx.set_state(&text);
9271 cx.set_head_text(&base_text);
9272 cx.update_editor(|editor, window, cx| {
9273 editor.expand_all_diff_hunks(&Default::default(), window, cx);
9274 });
9275
9276 cx.assert_state_with_diff(
9277 "
9278 ˇimpl A {
9279 - // this is an uncommitted comment
9280
9281 fn b() {
9282 c();
9283 }
9284
9285 - // this is another uncommitted comment
9286 -
9287 fn d() {
9288 // e
9289 // f
9290 }
9291 }
9292
9293 fn g() {
9294 // h
9295 }
9296 "
9297 .unindent(),
9298 );
9299
9300 let expected_display_text = "
9301 impl A {
9302 // this is an uncommitted comment
9303
9304 fn b() {
9305 ⋯
9306 }
9307
9308 // this is another uncommitted comment
9309
9310 fn d() {
9311 ⋯
9312 }
9313 }
9314
9315 fn g() {
9316 ⋯
9317 }
9318 "
9319 .unindent();
9320
9321 cx.update_editor(|editor, window, cx| {
9322 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
9323 assert_eq!(editor.display_text(cx), expected_display_text);
9324 });
9325}
9326
9327#[gpui::test]
9328async fn test_autoindent(cx: &mut TestAppContext) {
9329 init_test(cx, |_| {});
9330
9331 let language = Arc::new(
9332 Language::new(
9333 LanguageConfig {
9334 brackets: BracketPairConfig {
9335 pairs: vec![
9336 BracketPair {
9337 start: "{".to_string(),
9338 end: "}".to_string(),
9339 close: false,
9340 surround: false,
9341 newline: true,
9342 },
9343 BracketPair {
9344 start: "(".to_string(),
9345 end: ")".to_string(),
9346 close: false,
9347 surround: false,
9348 newline: true,
9349 },
9350 ],
9351 ..Default::default()
9352 },
9353 ..Default::default()
9354 },
9355 Some(tree_sitter_rust::LANGUAGE.into()),
9356 )
9357 .with_indents_query(
9358 r#"
9359 (_ "(" ")" @end) @indent
9360 (_ "{" "}" @end) @indent
9361 "#,
9362 )
9363 .unwrap(),
9364 );
9365
9366 let text = "fn a() {}";
9367
9368 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9369 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9370 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9371 editor
9372 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9373 .await;
9374
9375 editor.update_in(cx, |editor, window, cx| {
9376 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9377 s.select_ranges([5..5, 8..8, 9..9])
9378 });
9379 editor.newline(&Newline, window, cx);
9380 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
9381 assert_eq!(
9382 editor.selections.ranges(cx),
9383 &[
9384 Point::new(1, 4)..Point::new(1, 4),
9385 Point::new(3, 4)..Point::new(3, 4),
9386 Point::new(5, 0)..Point::new(5, 0)
9387 ]
9388 );
9389 });
9390}
9391
9392#[gpui::test]
9393async fn test_autoindent_disabled(cx: &mut TestAppContext) {
9394 init_test(cx, |settings| settings.defaults.auto_indent = Some(false));
9395
9396 let language = Arc::new(
9397 Language::new(
9398 LanguageConfig {
9399 brackets: BracketPairConfig {
9400 pairs: vec![
9401 BracketPair {
9402 start: "{".to_string(),
9403 end: "}".to_string(),
9404 close: false,
9405 surround: false,
9406 newline: true,
9407 },
9408 BracketPair {
9409 start: "(".to_string(),
9410 end: ")".to_string(),
9411 close: false,
9412 surround: false,
9413 newline: true,
9414 },
9415 ],
9416 ..Default::default()
9417 },
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 );
9430
9431 let text = "fn a() {}";
9432
9433 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9434 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9435 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9436 editor
9437 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9438 .await;
9439
9440 editor.update_in(cx, |editor, window, cx| {
9441 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9442 s.select_ranges([5..5, 8..8, 9..9])
9443 });
9444 editor.newline(&Newline, window, cx);
9445 assert_eq!(
9446 editor.text(cx),
9447 indoc!(
9448 "
9449 fn a(
9450
9451 ) {
9452
9453 }
9454 "
9455 )
9456 );
9457 assert_eq!(
9458 editor.selections.ranges(cx),
9459 &[
9460 Point::new(1, 0)..Point::new(1, 0),
9461 Point::new(3, 0)..Point::new(3, 0),
9462 Point::new(5, 0)..Point::new(5, 0)
9463 ]
9464 );
9465 });
9466}
9467
9468#[gpui::test]
9469async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) {
9470 init_test(cx, |settings| {
9471 settings.defaults.auto_indent = Some(true);
9472 settings.languages.0.insert(
9473 "python".into(),
9474 LanguageSettingsContent {
9475 auto_indent: Some(false),
9476 ..Default::default()
9477 },
9478 );
9479 });
9480
9481 let mut cx = EditorTestContext::new(cx).await;
9482
9483 let injected_language = Arc::new(
9484 Language::new(
9485 LanguageConfig {
9486 brackets: BracketPairConfig {
9487 pairs: vec![
9488 BracketPair {
9489 start: "{".to_string(),
9490 end: "}".to_string(),
9491 close: false,
9492 surround: false,
9493 newline: true,
9494 },
9495 BracketPair {
9496 start: "(".to_string(),
9497 end: ")".to_string(),
9498 close: true,
9499 surround: false,
9500 newline: true,
9501 },
9502 ],
9503 ..Default::default()
9504 },
9505 name: "python".into(),
9506 ..Default::default()
9507 },
9508 Some(tree_sitter_python::LANGUAGE.into()),
9509 )
9510 .with_indents_query(
9511 r#"
9512 (_ "(" ")" @end) @indent
9513 (_ "{" "}" @end) @indent
9514 "#,
9515 )
9516 .unwrap(),
9517 );
9518
9519 let language = Arc::new(
9520 Language::new(
9521 LanguageConfig {
9522 brackets: BracketPairConfig {
9523 pairs: vec![
9524 BracketPair {
9525 start: "{".to_string(),
9526 end: "}".to_string(),
9527 close: false,
9528 surround: false,
9529 newline: true,
9530 },
9531 BracketPair {
9532 start: "(".to_string(),
9533 end: ")".to_string(),
9534 close: true,
9535 surround: false,
9536 newline: true,
9537 },
9538 ],
9539 ..Default::default()
9540 },
9541 name: LanguageName::new("rust"),
9542 ..Default::default()
9543 },
9544 Some(tree_sitter_rust::LANGUAGE.into()),
9545 )
9546 .with_indents_query(
9547 r#"
9548 (_ "(" ")" @end) @indent
9549 (_ "{" "}" @end) @indent
9550 "#,
9551 )
9552 .unwrap()
9553 .with_injection_query(
9554 r#"
9555 (macro_invocation
9556 macro: (identifier) @_macro_name
9557 (token_tree) @injection.content
9558 (#set! injection.language "python"))
9559 "#,
9560 )
9561 .unwrap(),
9562 );
9563
9564 cx.language_registry().add(injected_language);
9565 cx.language_registry().add(language.clone());
9566
9567 cx.update_buffer(|buffer, cx| {
9568 buffer.set_language(Some(language), cx);
9569 });
9570
9571 cx.set_state(r#"struct A {ˇ}"#);
9572
9573 cx.update_editor(|editor, window, cx| {
9574 editor.newline(&Default::default(), window, cx);
9575 });
9576
9577 cx.assert_editor_state(indoc!(
9578 "struct A {
9579 ˇ
9580 }"
9581 ));
9582
9583 cx.set_state(r#"select_biased!(ˇ)"#);
9584
9585 cx.update_editor(|editor, window, cx| {
9586 editor.newline(&Default::default(), window, cx);
9587 editor.handle_input("def ", window, cx);
9588 editor.handle_input("(", window, cx);
9589 editor.newline(&Default::default(), window, cx);
9590 editor.handle_input("a", window, cx);
9591 });
9592
9593 cx.assert_editor_state(indoc!(
9594 "select_biased!(
9595 def (
9596 aˇ
9597 )
9598 )"
9599 ));
9600}
9601
9602#[gpui::test]
9603async fn test_autoindent_selections(cx: &mut TestAppContext) {
9604 init_test(cx, |_| {});
9605
9606 {
9607 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9608 cx.set_state(indoc! {"
9609 impl A {
9610
9611 fn b() {}
9612
9613 «fn c() {
9614
9615 }ˇ»
9616 }
9617 "});
9618
9619 cx.update_editor(|editor, window, cx| {
9620 editor.autoindent(&Default::default(), window, cx);
9621 });
9622
9623 cx.assert_editor_state(indoc! {"
9624 impl A {
9625
9626 fn b() {}
9627
9628 «fn c() {
9629
9630 }ˇ»
9631 }
9632 "});
9633 }
9634
9635 {
9636 let mut cx = EditorTestContext::new_multibuffer(
9637 cx,
9638 [indoc! { "
9639 impl A {
9640 «
9641 // a
9642 fn b(){}
9643 »
9644 «
9645 }
9646 fn c(){}
9647 »
9648 "}],
9649 );
9650
9651 let buffer = cx.update_editor(|editor, _, cx| {
9652 let buffer = editor.buffer().update(cx, |buffer, _| {
9653 buffer.all_buffers().iter().next().unwrap().clone()
9654 });
9655 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
9656 buffer
9657 });
9658
9659 cx.run_until_parked();
9660 cx.update_editor(|editor, window, cx| {
9661 editor.select_all(&Default::default(), window, cx);
9662 editor.autoindent(&Default::default(), window, cx)
9663 });
9664 cx.run_until_parked();
9665
9666 cx.update(|_, cx| {
9667 assert_eq!(
9668 buffer.read(cx).text(),
9669 indoc! { "
9670 impl A {
9671
9672 // a
9673 fn b(){}
9674
9675
9676 }
9677 fn c(){}
9678
9679 " }
9680 )
9681 });
9682 }
9683}
9684
9685#[gpui::test]
9686async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
9687 init_test(cx, |_| {});
9688
9689 let mut cx = EditorTestContext::new(cx).await;
9690
9691 let language = Arc::new(Language::new(
9692 LanguageConfig {
9693 brackets: BracketPairConfig {
9694 pairs: vec![
9695 BracketPair {
9696 start: "{".to_string(),
9697 end: "}".to_string(),
9698 close: true,
9699 surround: true,
9700 newline: true,
9701 },
9702 BracketPair {
9703 start: "(".to_string(),
9704 end: ")".to_string(),
9705 close: true,
9706 surround: true,
9707 newline: true,
9708 },
9709 BracketPair {
9710 start: "/*".to_string(),
9711 end: " */".to_string(),
9712 close: true,
9713 surround: true,
9714 newline: true,
9715 },
9716 BracketPair {
9717 start: "[".to_string(),
9718 end: "]".to_string(),
9719 close: false,
9720 surround: false,
9721 newline: true,
9722 },
9723 BracketPair {
9724 start: "\"".to_string(),
9725 end: "\"".to_string(),
9726 close: true,
9727 surround: true,
9728 newline: false,
9729 },
9730 BracketPair {
9731 start: "<".to_string(),
9732 end: ">".to_string(),
9733 close: false,
9734 surround: true,
9735 newline: true,
9736 },
9737 ],
9738 ..Default::default()
9739 },
9740 autoclose_before: "})]".to_string(),
9741 ..Default::default()
9742 },
9743 Some(tree_sitter_rust::LANGUAGE.into()),
9744 ));
9745
9746 cx.language_registry().add(language.clone());
9747 cx.update_buffer(|buffer, cx| {
9748 buffer.set_language(Some(language), cx);
9749 });
9750
9751 cx.set_state(
9752 &r#"
9753 🏀ˇ
9754 εˇ
9755 ❤️ˇ
9756 "#
9757 .unindent(),
9758 );
9759
9760 // autoclose multiple nested brackets at multiple cursors
9761 cx.update_editor(|editor, window, cx| {
9762 editor.handle_input("{", window, cx);
9763 editor.handle_input("{", window, cx);
9764 editor.handle_input("{", window, cx);
9765 });
9766 cx.assert_editor_state(
9767 &"
9768 🏀{{{ˇ}}}
9769 ε{{{ˇ}}}
9770 ❤️{{{ˇ}}}
9771 "
9772 .unindent(),
9773 );
9774
9775 // insert a different closing bracket
9776 cx.update_editor(|editor, window, cx| {
9777 editor.handle_input(")", window, cx);
9778 });
9779 cx.assert_editor_state(
9780 &"
9781 🏀{{{)ˇ}}}
9782 ε{{{)ˇ}}}
9783 ❤️{{{)ˇ}}}
9784 "
9785 .unindent(),
9786 );
9787
9788 // skip over the auto-closed brackets when typing a closing bracket
9789 cx.update_editor(|editor, window, cx| {
9790 editor.move_right(&MoveRight, window, cx);
9791 editor.handle_input("}", window, cx);
9792 editor.handle_input("}", window, cx);
9793 editor.handle_input("}", window, cx);
9794 });
9795 cx.assert_editor_state(
9796 &"
9797 🏀{{{)}}}}ˇ
9798 ε{{{)}}}}ˇ
9799 ❤️{{{)}}}}ˇ
9800 "
9801 .unindent(),
9802 );
9803
9804 // autoclose multi-character pairs
9805 cx.set_state(
9806 &"
9807 ˇ
9808 ˇ
9809 "
9810 .unindent(),
9811 );
9812 cx.update_editor(|editor, window, cx| {
9813 editor.handle_input("/", window, cx);
9814 editor.handle_input("*", window, cx);
9815 });
9816 cx.assert_editor_state(
9817 &"
9818 /*ˇ */
9819 /*ˇ */
9820 "
9821 .unindent(),
9822 );
9823
9824 // one cursor autocloses a multi-character pair, one cursor
9825 // does not autoclose.
9826 cx.set_state(
9827 &"
9828 /ˇ
9829 ˇ
9830 "
9831 .unindent(),
9832 );
9833 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
9834 cx.assert_editor_state(
9835 &"
9836 /*ˇ */
9837 *ˇ
9838 "
9839 .unindent(),
9840 );
9841
9842 // Don't autoclose if the next character isn't whitespace and isn't
9843 // listed in the language's "autoclose_before" section.
9844 cx.set_state("ˇa b");
9845 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
9846 cx.assert_editor_state("{ˇa b");
9847
9848 // Don't autoclose if `close` is false for the bracket pair
9849 cx.set_state("ˇ");
9850 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
9851 cx.assert_editor_state("[ˇ");
9852
9853 // Surround with brackets if text is selected
9854 cx.set_state("«aˇ» b");
9855 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
9856 cx.assert_editor_state("{«aˇ»} b");
9857
9858 // Autoclose when not immediately after a word character
9859 cx.set_state("a ˇ");
9860 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9861 cx.assert_editor_state("a \"ˇ\"");
9862
9863 // Autoclose pair where the start and end characters are the same
9864 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9865 cx.assert_editor_state("a \"\"ˇ");
9866
9867 // Don't autoclose when immediately after a word character
9868 cx.set_state("aˇ");
9869 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9870 cx.assert_editor_state("a\"ˇ");
9871
9872 // Do autoclose when after a non-word character
9873 cx.set_state("{ˇ");
9874 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9875 cx.assert_editor_state("{\"ˇ\"");
9876
9877 // Non identical pairs autoclose regardless of preceding character
9878 cx.set_state("aˇ");
9879 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
9880 cx.assert_editor_state("a{ˇ}");
9881
9882 // Don't autoclose pair if autoclose is disabled
9883 cx.set_state("ˇ");
9884 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
9885 cx.assert_editor_state("<ˇ");
9886
9887 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
9888 cx.set_state("«aˇ» b");
9889 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
9890 cx.assert_editor_state("<«aˇ»> b");
9891}
9892
9893#[gpui::test]
9894async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
9895 init_test(cx, |settings| {
9896 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
9897 });
9898
9899 let mut cx = EditorTestContext::new(cx).await;
9900
9901 let language = Arc::new(Language::new(
9902 LanguageConfig {
9903 brackets: BracketPairConfig {
9904 pairs: vec![
9905 BracketPair {
9906 start: "{".to_string(),
9907 end: "}".to_string(),
9908 close: true,
9909 surround: true,
9910 newline: true,
9911 },
9912 BracketPair {
9913 start: "(".to_string(),
9914 end: ")".to_string(),
9915 close: true,
9916 surround: true,
9917 newline: true,
9918 },
9919 BracketPair {
9920 start: "[".to_string(),
9921 end: "]".to_string(),
9922 close: false,
9923 surround: false,
9924 newline: true,
9925 },
9926 ],
9927 ..Default::default()
9928 },
9929 autoclose_before: "})]".to_string(),
9930 ..Default::default()
9931 },
9932 Some(tree_sitter_rust::LANGUAGE.into()),
9933 ));
9934
9935 cx.language_registry().add(language.clone());
9936 cx.update_buffer(|buffer, cx| {
9937 buffer.set_language(Some(language), cx);
9938 });
9939
9940 cx.set_state(
9941 &"
9942 ˇ
9943 ˇ
9944 ˇ
9945 "
9946 .unindent(),
9947 );
9948
9949 // ensure only matching closing brackets are skipped over
9950 cx.update_editor(|editor, window, cx| {
9951 editor.handle_input("}", window, cx);
9952 editor.move_left(&MoveLeft, window, cx);
9953 editor.handle_input(")", window, cx);
9954 editor.move_left(&MoveLeft, window, cx);
9955 });
9956 cx.assert_editor_state(
9957 &"
9958 ˇ)}
9959 ˇ)}
9960 ˇ)}
9961 "
9962 .unindent(),
9963 );
9964
9965 // skip-over closing brackets at multiple cursors
9966 cx.update_editor(|editor, window, cx| {
9967 editor.handle_input(")", window, cx);
9968 editor.handle_input("}", window, cx);
9969 });
9970 cx.assert_editor_state(
9971 &"
9972 )}ˇ
9973 )}ˇ
9974 )}ˇ
9975 "
9976 .unindent(),
9977 );
9978
9979 // ignore non-close brackets
9980 cx.update_editor(|editor, window, cx| {
9981 editor.handle_input("]", window, cx);
9982 editor.move_left(&MoveLeft, window, cx);
9983 editor.handle_input("]", window, cx);
9984 });
9985 cx.assert_editor_state(
9986 &"
9987 )}]ˇ]
9988 )}]ˇ]
9989 )}]ˇ]
9990 "
9991 .unindent(),
9992 );
9993}
9994
9995#[gpui::test]
9996async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
9997 init_test(cx, |_| {});
9998
9999 let mut cx = EditorTestContext::new(cx).await;
10000
10001 let html_language = Arc::new(
10002 Language::new(
10003 LanguageConfig {
10004 name: "HTML".into(),
10005 brackets: BracketPairConfig {
10006 pairs: vec![
10007 BracketPair {
10008 start: "<".into(),
10009 end: ">".into(),
10010 close: true,
10011 ..Default::default()
10012 },
10013 BracketPair {
10014 start: "{".into(),
10015 end: "}".into(),
10016 close: true,
10017 ..Default::default()
10018 },
10019 BracketPair {
10020 start: "(".into(),
10021 end: ")".into(),
10022 close: true,
10023 ..Default::default()
10024 },
10025 ],
10026 ..Default::default()
10027 },
10028 autoclose_before: "})]>".into(),
10029 ..Default::default()
10030 },
10031 Some(tree_sitter_html::LANGUAGE.into()),
10032 )
10033 .with_injection_query(
10034 r#"
10035 (script_element
10036 (raw_text) @injection.content
10037 (#set! injection.language "javascript"))
10038 "#,
10039 )
10040 .unwrap(),
10041 );
10042
10043 let javascript_language = Arc::new(Language::new(
10044 LanguageConfig {
10045 name: "JavaScript".into(),
10046 brackets: BracketPairConfig {
10047 pairs: vec![
10048 BracketPair {
10049 start: "/*".into(),
10050 end: " */".into(),
10051 close: true,
10052 ..Default::default()
10053 },
10054 BracketPair {
10055 start: "{".into(),
10056 end: "}".into(),
10057 close: true,
10058 ..Default::default()
10059 },
10060 BracketPair {
10061 start: "(".into(),
10062 end: ")".into(),
10063 close: true,
10064 ..Default::default()
10065 },
10066 ],
10067 ..Default::default()
10068 },
10069 autoclose_before: "})]>".into(),
10070 ..Default::default()
10071 },
10072 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
10073 ));
10074
10075 cx.language_registry().add(html_language.clone());
10076 cx.language_registry().add(javascript_language);
10077 cx.executor().run_until_parked();
10078
10079 cx.update_buffer(|buffer, cx| {
10080 buffer.set_language(Some(html_language), cx);
10081 });
10082
10083 cx.set_state(
10084 &r#"
10085 <body>ˇ
10086 <script>
10087 var x = 1;ˇ
10088 </script>
10089 </body>ˇ
10090 "#
10091 .unindent(),
10092 );
10093
10094 // Precondition: different languages are active at different locations.
10095 cx.update_editor(|editor, window, cx| {
10096 let snapshot = editor.snapshot(window, cx);
10097 let cursors = editor.selections.ranges::<usize>(cx);
10098 let languages = cursors
10099 .iter()
10100 .map(|c| snapshot.language_at(c.start).unwrap().name())
10101 .collect::<Vec<_>>();
10102 assert_eq!(
10103 languages,
10104 &["HTML".into(), "JavaScript".into(), "HTML".into()]
10105 );
10106 });
10107
10108 // Angle brackets autoclose in HTML, but not JavaScript.
10109 cx.update_editor(|editor, window, cx| {
10110 editor.handle_input("<", window, cx);
10111 editor.handle_input("a", window, cx);
10112 });
10113 cx.assert_editor_state(
10114 &r#"
10115 <body><aˇ>
10116 <script>
10117 var x = 1;<aˇ
10118 </script>
10119 </body><aˇ>
10120 "#
10121 .unindent(),
10122 );
10123
10124 // Curly braces and parens autoclose in both HTML and JavaScript.
10125 cx.update_editor(|editor, window, cx| {
10126 editor.handle_input(" b=", window, cx);
10127 editor.handle_input("{", window, cx);
10128 editor.handle_input("c", window, cx);
10129 editor.handle_input("(", window, cx);
10130 });
10131 cx.assert_editor_state(
10132 &r#"
10133 <body><a b={c(ˇ)}>
10134 <script>
10135 var x = 1;<a b={c(ˇ)}
10136 </script>
10137 </body><a b={c(ˇ)}>
10138 "#
10139 .unindent(),
10140 );
10141
10142 // Brackets that were already autoclosed are skipped.
10143 cx.update_editor(|editor, window, cx| {
10144 editor.handle_input(")", window, cx);
10145 editor.handle_input("d", window, cx);
10146 editor.handle_input("}", window, cx);
10147 });
10148 cx.assert_editor_state(
10149 &r#"
10150 <body><a b={c()d}ˇ>
10151 <script>
10152 var x = 1;<a b={c()d}ˇ
10153 </script>
10154 </body><a b={c()d}ˇ>
10155 "#
10156 .unindent(),
10157 );
10158 cx.update_editor(|editor, window, cx| {
10159 editor.handle_input(">", window, cx);
10160 });
10161 cx.assert_editor_state(
10162 &r#"
10163 <body><a b={c()d}>ˇ
10164 <script>
10165 var x = 1;<a b={c()d}>ˇ
10166 </script>
10167 </body><a b={c()d}>ˇ
10168 "#
10169 .unindent(),
10170 );
10171
10172 // Reset
10173 cx.set_state(
10174 &r#"
10175 <body>ˇ
10176 <script>
10177 var x = 1;ˇ
10178 </script>
10179 </body>ˇ
10180 "#
10181 .unindent(),
10182 );
10183
10184 cx.update_editor(|editor, window, cx| {
10185 editor.handle_input("<", window, cx);
10186 });
10187 cx.assert_editor_state(
10188 &r#"
10189 <body><ˇ>
10190 <script>
10191 var x = 1;<ˇ
10192 </script>
10193 </body><ˇ>
10194 "#
10195 .unindent(),
10196 );
10197
10198 // When backspacing, the closing angle brackets are removed.
10199 cx.update_editor(|editor, window, cx| {
10200 editor.backspace(&Backspace, window, cx);
10201 });
10202 cx.assert_editor_state(
10203 &r#"
10204 <body>ˇ
10205 <script>
10206 var x = 1;ˇ
10207 </script>
10208 </body>ˇ
10209 "#
10210 .unindent(),
10211 );
10212
10213 // Block comments autoclose in JavaScript, but not HTML.
10214 cx.update_editor(|editor, window, cx| {
10215 editor.handle_input("/", window, cx);
10216 editor.handle_input("*", window, cx);
10217 });
10218 cx.assert_editor_state(
10219 &r#"
10220 <body>/*ˇ
10221 <script>
10222 var x = 1;/*ˇ */
10223 </script>
10224 </body>/*ˇ
10225 "#
10226 .unindent(),
10227 );
10228}
10229
10230#[gpui::test]
10231async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
10232 init_test(cx, |_| {});
10233
10234 let mut cx = EditorTestContext::new(cx).await;
10235
10236 let rust_language = Arc::new(
10237 Language::new(
10238 LanguageConfig {
10239 name: "Rust".into(),
10240 brackets: serde_json::from_value(json!([
10241 { "start": "{", "end": "}", "close": true, "newline": true },
10242 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
10243 ]))
10244 .unwrap(),
10245 autoclose_before: "})]>".into(),
10246 ..Default::default()
10247 },
10248 Some(tree_sitter_rust::LANGUAGE.into()),
10249 )
10250 .with_override_query("(string_literal) @string")
10251 .unwrap(),
10252 );
10253
10254 cx.language_registry().add(rust_language.clone());
10255 cx.update_buffer(|buffer, cx| {
10256 buffer.set_language(Some(rust_language), cx);
10257 });
10258
10259 cx.set_state(
10260 &r#"
10261 let x = ˇ
10262 "#
10263 .unindent(),
10264 );
10265
10266 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
10267 cx.update_editor(|editor, window, cx| {
10268 editor.handle_input("\"", window, cx);
10269 });
10270 cx.assert_editor_state(
10271 &r#"
10272 let x = "ˇ"
10273 "#
10274 .unindent(),
10275 );
10276
10277 // Inserting another quotation mark. The cursor moves across the existing
10278 // automatically-inserted quotation mark.
10279 cx.update_editor(|editor, window, cx| {
10280 editor.handle_input("\"", window, cx);
10281 });
10282 cx.assert_editor_state(
10283 &r#"
10284 let x = ""ˇ
10285 "#
10286 .unindent(),
10287 );
10288
10289 // Reset
10290 cx.set_state(
10291 &r#"
10292 let x = ˇ
10293 "#
10294 .unindent(),
10295 );
10296
10297 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
10298 cx.update_editor(|editor, window, cx| {
10299 editor.handle_input("\"", window, cx);
10300 editor.handle_input(" ", window, cx);
10301 editor.move_left(&Default::default(), window, cx);
10302 editor.handle_input("\\", window, cx);
10303 editor.handle_input("\"", window, cx);
10304 });
10305 cx.assert_editor_state(
10306 &r#"
10307 let x = "\"ˇ "
10308 "#
10309 .unindent(),
10310 );
10311
10312 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
10313 // mark. Nothing is inserted.
10314 cx.update_editor(|editor, window, cx| {
10315 editor.move_right(&Default::default(), window, cx);
10316 editor.handle_input("\"", window, cx);
10317 });
10318 cx.assert_editor_state(
10319 &r#"
10320 let x = "\" "ˇ
10321 "#
10322 .unindent(),
10323 );
10324}
10325
10326#[gpui::test]
10327async fn test_surround_with_pair(cx: &mut TestAppContext) {
10328 init_test(cx, |_| {});
10329
10330 let language = Arc::new(Language::new(
10331 LanguageConfig {
10332 brackets: BracketPairConfig {
10333 pairs: vec![
10334 BracketPair {
10335 start: "{".to_string(),
10336 end: "}".to_string(),
10337 close: true,
10338 surround: true,
10339 newline: true,
10340 },
10341 BracketPair {
10342 start: "/* ".to_string(),
10343 end: "*/".to_string(),
10344 close: true,
10345 surround: true,
10346 ..Default::default()
10347 },
10348 ],
10349 ..Default::default()
10350 },
10351 ..Default::default()
10352 },
10353 Some(tree_sitter_rust::LANGUAGE.into()),
10354 ));
10355
10356 let text = r#"
10357 a
10358 b
10359 c
10360 "#
10361 .unindent();
10362
10363 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10364 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10365 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10366 editor
10367 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10368 .await;
10369
10370 editor.update_in(cx, |editor, window, cx| {
10371 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10372 s.select_display_ranges([
10373 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10374 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10375 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
10376 ])
10377 });
10378
10379 editor.handle_input("{", window, cx);
10380 editor.handle_input("{", window, cx);
10381 editor.handle_input("{", window, cx);
10382 assert_eq!(
10383 editor.text(cx),
10384 "
10385 {{{a}}}
10386 {{{b}}}
10387 {{{c}}}
10388 "
10389 .unindent()
10390 );
10391 assert_eq!(
10392 editor.selections.display_ranges(cx),
10393 [
10394 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
10395 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
10396 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
10397 ]
10398 );
10399
10400 editor.undo(&Undo, window, cx);
10401 editor.undo(&Undo, window, cx);
10402 editor.undo(&Undo, window, cx);
10403 assert_eq!(
10404 editor.text(cx),
10405 "
10406 a
10407 b
10408 c
10409 "
10410 .unindent()
10411 );
10412 assert_eq!(
10413 editor.selections.display_ranges(cx),
10414 [
10415 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10416 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10417 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10418 ]
10419 );
10420
10421 // Ensure inserting the first character of a multi-byte bracket pair
10422 // doesn't surround the selections with the bracket.
10423 editor.handle_input("/", window, cx);
10424 assert_eq!(
10425 editor.text(cx),
10426 "
10427 /
10428 /
10429 /
10430 "
10431 .unindent()
10432 );
10433 assert_eq!(
10434 editor.selections.display_ranges(cx),
10435 [
10436 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10437 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10438 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10439 ]
10440 );
10441
10442 editor.undo(&Undo, window, cx);
10443 assert_eq!(
10444 editor.text(cx),
10445 "
10446 a
10447 b
10448 c
10449 "
10450 .unindent()
10451 );
10452 assert_eq!(
10453 editor.selections.display_ranges(cx),
10454 [
10455 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10456 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10457 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10458 ]
10459 );
10460
10461 // Ensure inserting the last character of a multi-byte bracket pair
10462 // doesn't surround the selections with the bracket.
10463 editor.handle_input("*", window, cx);
10464 assert_eq!(
10465 editor.text(cx),
10466 "
10467 *
10468 *
10469 *
10470 "
10471 .unindent()
10472 );
10473 assert_eq!(
10474 editor.selections.display_ranges(cx),
10475 [
10476 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10477 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10478 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10479 ]
10480 );
10481 });
10482}
10483
10484#[gpui::test]
10485async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
10486 init_test(cx, |_| {});
10487
10488 let language = Arc::new(Language::new(
10489 LanguageConfig {
10490 brackets: BracketPairConfig {
10491 pairs: vec![BracketPair {
10492 start: "{".to_string(),
10493 end: "}".to_string(),
10494 close: true,
10495 surround: true,
10496 newline: true,
10497 }],
10498 ..Default::default()
10499 },
10500 autoclose_before: "}".to_string(),
10501 ..Default::default()
10502 },
10503 Some(tree_sitter_rust::LANGUAGE.into()),
10504 ));
10505
10506 let text = r#"
10507 a
10508 b
10509 c
10510 "#
10511 .unindent();
10512
10513 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10514 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10515 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10516 editor
10517 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10518 .await;
10519
10520 editor.update_in(cx, |editor, window, cx| {
10521 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10522 s.select_ranges([
10523 Point::new(0, 1)..Point::new(0, 1),
10524 Point::new(1, 1)..Point::new(1, 1),
10525 Point::new(2, 1)..Point::new(2, 1),
10526 ])
10527 });
10528
10529 editor.handle_input("{", window, cx);
10530 editor.handle_input("{", window, cx);
10531 editor.handle_input("_", window, cx);
10532 assert_eq!(
10533 editor.text(cx),
10534 "
10535 a{{_}}
10536 b{{_}}
10537 c{{_}}
10538 "
10539 .unindent()
10540 );
10541 assert_eq!(
10542 editor.selections.ranges::<Point>(cx),
10543 [
10544 Point::new(0, 4)..Point::new(0, 4),
10545 Point::new(1, 4)..Point::new(1, 4),
10546 Point::new(2, 4)..Point::new(2, 4)
10547 ]
10548 );
10549
10550 editor.backspace(&Default::default(), window, cx);
10551 editor.backspace(&Default::default(), window, cx);
10552 assert_eq!(
10553 editor.text(cx),
10554 "
10555 a{}
10556 b{}
10557 c{}
10558 "
10559 .unindent()
10560 );
10561 assert_eq!(
10562 editor.selections.ranges::<Point>(cx),
10563 [
10564 Point::new(0, 2)..Point::new(0, 2),
10565 Point::new(1, 2)..Point::new(1, 2),
10566 Point::new(2, 2)..Point::new(2, 2)
10567 ]
10568 );
10569
10570 editor.delete_to_previous_word_start(&Default::default(), window, cx);
10571 assert_eq!(
10572 editor.text(cx),
10573 "
10574 a
10575 b
10576 c
10577 "
10578 .unindent()
10579 );
10580 assert_eq!(
10581 editor.selections.ranges::<Point>(cx),
10582 [
10583 Point::new(0, 1)..Point::new(0, 1),
10584 Point::new(1, 1)..Point::new(1, 1),
10585 Point::new(2, 1)..Point::new(2, 1)
10586 ]
10587 );
10588 });
10589}
10590
10591#[gpui::test]
10592async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
10593 init_test(cx, |settings| {
10594 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10595 });
10596
10597 let mut cx = EditorTestContext::new(cx).await;
10598
10599 let language = Arc::new(Language::new(
10600 LanguageConfig {
10601 brackets: BracketPairConfig {
10602 pairs: vec![
10603 BracketPair {
10604 start: "{".to_string(),
10605 end: "}".to_string(),
10606 close: true,
10607 surround: true,
10608 newline: true,
10609 },
10610 BracketPair {
10611 start: "(".to_string(),
10612 end: ")".to_string(),
10613 close: true,
10614 surround: true,
10615 newline: true,
10616 },
10617 BracketPair {
10618 start: "[".to_string(),
10619 end: "]".to_string(),
10620 close: false,
10621 surround: true,
10622 newline: true,
10623 },
10624 ],
10625 ..Default::default()
10626 },
10627 autoclose_before: "})]".to_string(),
10628 ..Default::default()
10629 },
10630 Some(tree_sitter_rust::LANGUAGE.into()),
10631 ));
10632
10633 cx.language_registry().add(language.clone());
10634 cx.update_buffer(|buffer, cx| {
10635 buffer.set_language(Some(language), cx);
10636 });
10637
10638 cx.set_state(
10639 &"
10640 {(ˇ)}
10641 [[ˇ]]
10642 {(ˇ)}
10643 "
10644 .unindent(),
10645 );
10646
10647 cx.update_editor(|editor, window, cx| {
10648 editor.backspace(&Default::default(), window, cx);
10649 editor.backspace(&Default::default(), window, cx);
10650 });
10651
10652 cx.assert_editor_state(
10653 &"
10654 ˇ
10655 ˇ]]
10656 ˇ
10657 "
10658 .unindent(),
10659 );
10660
10661 cx.update_editor(|editor, window, cx| {
10662 editor.handle_input("{", window, cx);
10663 editor.handle_input("{", window, cx);
10664 editor.move_right(&MoveRight, window, cx);
10665 editor.move_right(&MoveRight, window, cx);
10666 editor.move_left(&MoveLeft, window, cx);
10667 editor.move_left(&MoveLeft, window, cx);
10668 editor.backspace(&Default::default(), window, cx);
10669 });
10670
10671 cx.assert_editor_state(
10672 &"
10673 {ˇ}
10674 {ˇ}]]
10675 {ˇ}
10676 "
10677 .unindent(),
10678 );
10679
10680 cx.update_editor(|editor, window, cx| {
10681 editor.backspace(&Default::default(), window, cx);
10682 });
10683
10684 cx.assert_editor_state(
10685 &"
10686 ˇ
10687 ˇ]]
10688 ˇ
10689 "
10690 .unindent(),
10691 );
10692}
10693
10694#[gpui::test]
10695async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
10696 init_test(cx, |_| {});
10697
10698 let language = Arc::new(Language::new(
10699 LanguageConfig::default(),
10700 Some(tree_sitter_rust::LANGUAGE.into()),
10701 ));
10702
10703 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
10704 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10705 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10706 editor
10707 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10708 .await;
10709
10710 editor.update_in(cx, |editor, window, cx| {
10711 editor.set_auto_replace_emoji_shortcode(true);
10712
10713 editor.handle_input("Hello ", window, cx);
10714 editor.handle_input(":wave", window, cx);
10715 assert_eq!(editor.text(cx), "Hello :wave".unindent());
10716
10717 editor.handle_input(":", window, cx);
10718 assert_eq!(editor.text(cx), "Hello 👋".unindent());
10719
10720 editor.handle_input(" :smile", window, cx);
10721 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
10722
10723 editor.handle_input(":", window, cx);
10724 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
10725
10726 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
10727 editor.handle_input(":wave", window, cx);
10728 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
10729
10730 editor.handle_input(":", window, cx);
10731 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
10732
10733 editor.handle_input(":1", window, cx);
10734 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
10735
10736 editor.handle_input(":", window, cx);
10737 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
10738
10739 // Ensure shortcode does not get replaced when it is part of a word
10740 editor.handle_input(" Test:wave", window, cx);
10741 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
10742
10743 editor.handle_input(":", window, cx);
10744 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
10745
10746 editor.set_auto_replace_emoji_shortcode(false);
10747
10748 // Ensure shortcode does not get replaced when auto replace is off
10749 editor.handle_input(" :wave", window, cx);
10750 assert_eq!(
10751 editor.text(cx),
10752 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
10753 );
10754
10755 editor.handle_input(":", window, cx);
10756 assert_eq!(
10757 editor.text(cx),
10758 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
10759 );
10760 });
10761}
10762
10763#[gpui::test]
10764async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
10765 init_test(cx, |_| {});
10766
10767 let (text, insertion_ranges) = marked_text_ranges(
10768 indoc! {"
10769 ˇ
10770 "},
10771 false,
10772 );
10773
10774 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
10775 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10776
10777 _ = editor.update_in(cx, |editor, window, cx| {
10778 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
10779
10780 editor
10781 .insert_snippet(&insertion_ranges, snippet, window, cx)
10782 .unwrap();
10783
10784 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
10785 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
10786 assert_eq!(editor.text(cx), expected_text);
10787 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
10788 }
10789
10790 assert(
10791 editor,
10792 cx,
10793 indoc! {"
10794 type «» =•
10795 "},
10796 );
10797
10798 assert!(editor.context_menu_visible(), "There should be a matches");
10799 });
10800}
10801
10802#[gpui::test]
10803async fn test_snippets(cx: &mut TestAppContext) {
10804 init_test(cx, |_| {});
10805
10806 let mut cx = EditorTestContext::new(cx).await;
10807
10808 cx.set_state(indoc! {"
10809 a.ˇ b
10810 a.ˇ b
10811 a.ˇ b
10812 "});
10813
10814 cx.update_editor(|editor, window, cx| {
10815 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
10816 let insertion_ranges = editor
10817 .selections
10818 .all(cx)
10819 .iter()
10820 .map(|s| s.range())
10821 .collect::<Vec<_>>();
10822 editor
10823 .insert_snippet(&insertion_ranges, snippet, window, cx)
10824 .unwrap();
10825 });
10826
10827 cx.assert_editor_state(indoc! {"
10828 a.f(«oneˇ», two, «threeˇ») b
10829 a.f(«oneˇ», two, «threeˇ») b
10830 a.f(«oneˇ», two, «threeˇ») b
10831 "});
10832
10833 // Can't move earlier than the first tab stop
10834 cx.update_editor(|editor, window, cx| {
10835 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
10836 });
10837 cx.assert_editor_state(indoc! {"
10838 a.f(«oneˇ», two, «threeˇ») b
10839 a.f(«oneˇ», two, «threeˇ») b
10840 a.f(«oneˇ», two, «threeˇ») b
10841 "});
10842
10843 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10844 cx.assert_editor_state(indoc! {"
10845 a.f(one, «twoˇ», three) b
10846 a.f(one, «twoˇ», three) b
10847 a.f(one, «twoˇ», three) b
10848 "});
10849
10850 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
10851 cx.assert_editor_state(indoc! {"
10852 a.f(«oneˇ», two, «threeˇ») b
10853 a.f(«oneˇ», two, «threeˇ») b
10854 a.f(«oneˇ», two, «threeˇ») b
10855 "});
10856
10857 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10858 cx.assert_editor_state(indoc! {"
10859 a.f(one, «twoˇ», three) b
10860 a.f(one, «twoˇ», three) b
10861 a.f(one, «twoˇ», three) b
10862 "});
10863 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10864 cx.assert_editor_state(indoc! {"
10865 a.f(one, two, three)ˇ b
10866 a.f(one, two, three)ˇ b
10867 a.f(one, two, three)ˇ b
10868 "});
10869
10870 // As soon as the last tab stop is reached, snippet state is gone
10871 cx.update_editor(|editor, window, cx| {
10872 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
10873 });
10874 cx.assert_editor_state(indoc! {"
10875 a.f(one, two, three)ˇ b
10876 a.f(one, two, three)ˇ b
10877 a.f(one, two, three)ˇ b
10878 "});
10879}
10880
10881#[gpui::test]
10882async fn test_snippet_indentation(cx: &mut TestAppContext) {
10883 init_test(cx, |_| {});
10884
10885 let mut cx = EditorTestContext::new(cx).await;
10886
10887 cx.update_editor(|editor, window, cx| {
10888 let snippet = Snippet::parse(indoc! {"
10889 /*
10890 * Multiline comment with leading indentation
10891 *
10892 * $1
10893 */
10894 $0"})
10895 .unwrap();
10896 let insertion_ranges = editor
10897 .selections
10898 .all(cx)
10899 .iter()
10900 .map(|s| s.range())
10901 .collect::<Vec<_>>();
10902 editor
10903 .insert_snippet(&insertion_ranges, snippet, window, cx)
10904 .unwrap();
10905 });
10906
10907 cx.assert_editor_state(indoc! {"
10908 /*
10909 * Multiline comment with leading indentation
10910 *
10911 * ˇ
10912 */
10913 "});
10914
10915 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10916 cx.assert_editor_state(indoc! {"
10917 /*
10918 * Multiline comment with leading indentation
10919 *
10920 *•
10921 */
10922 ˇ"});
10923}
10924
10925#[gpui::test]
10926async fn test_document_format_during_save(cx: &mut TestAppContext) {
10927 init_test(cx, |_| {});
10928
10929 let fs = FakeFs::new(cx.executor());
10930 fs.insert_file(path!("/file.rs"), Default::default()).await;
10931
10932 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
10933
10934 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10935 language_registry.add(rust_lang());
10936 let mut fake_servers = language_registry.register_fake_lsp(
10937 "Rust",
10938 FakeLspAdapter {
10939 capabilities: lsp::ServerCapabilities {
10940 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10941 ..Default::default()
10942 },
10943 ..Default::default()
10944 },
10945 );
10946
10947 let buffer = project
10948 .update(cx, |project, cx| {
10949 project.open_local_buffer(path!("/file.rs"), cx)
10950 })
10951 .await
10952 .unwrap();
10953
10954 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10955 let (editor, cx) = cx.add_window_view(|window, cx| {
10956 build_editor_with_project(project.clone(), buffer, window, cx)
10957 });
10958 editor.update_in(cx, |editor, window, cx| {
10959 editor.set_text("one\ntwo\nthree\n", window, cx)
10960 });
10961 assert!(cx.read(|cx| editor.is_dirty(cx)));
10962
10963 cx.executor().start_waiting();
10964 let fake_server = fake_servers.next().await.unwrap();
10965
10966 {
10967 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10968 move |params, _| async move {
10969 assert_eq!(
10970 params.text_document.uri,
10971 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
10972 );
10973 assert_eq!(params.options.tab_size, 4);
10974 Ok(Some(vec![lsp::TextEdit::new(
10975 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10976 ", ".to_string(),
10977 )]))
10978 },
10979 );
10980 let save = editor
10981 .update_in(cx, |editor, window, cx| {
10982 editor.save(
10983 SaveOptions {
10984 format: true,
10985 autosave: false,
10986 },
10987 project.clone(),
10988 window,
10989 cx,
10990 )
10991 })
10992 .unwrap();
10993 cx.executor().start_waiting();
10994 save.await;
10995
10996 assert_eq!(
10997 editor.update(cx, |editor, cx| editor.text(cx)),
10998 "one, two\nthree\n"
10999 );
11000 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11001 }
11002
11003 {
11004 editor.update_in(cx, |editor, window, cx| {
11005 editor.set_text("one\ntwo\nthree\n", window, cx)
11006 });
11007 assert!(cx.read(|cx| editor.is_dirty(cx)));
11008
11009 // Ensure we can still save even if formatting hangs.
11010 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11011 move |params, _| async move {
11012 assert_eq!(
11013 params.text_document.uri,
11014 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11015 );
11016 futures::future::pending::<()>().await;
11017 unreachable!()
11018 },
11019 );
11020 let save = editor
11021 .update_in(cx, |editor, window, cx| {
11022 editor.save(
11023 SaveOptions {
11024 format: true,
11025 autosave: false,
11026 },
11027 project.clone(),
11028 window,
11029 cx,
11030 )
11031 })
11032 .unwrap();
11033 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11034 cx.executor().start_waiting();
11035 save.await;
11036 assert_eq!(
11037 editor.update(cx, |editor, cx| editor.text(cx)),
11038 "one\ntwo\nthree\n"
11039 );
11040 }
11041
11042 // Set rust language override and assert overridden tabsize is sent to language server
11043 update_test_language_settings(cx, |settings| {
11044 settings.languages.0.insert(
11045 "Rust".into(),
11046 LanguageSettingsContent {
11047 tab_size: NonZeroU32::new(8),
11048 ..Default::default()
11049 },
11050 );
11051 });
11052
11053 {
11054 editor.update_in(cx, |editor, window, cx| {
11055 editor.set_text("somehting_new\n", window, cx)
11056 });
11057 assert!(cx.read(|cx| editor.is_dirty(cx)));
11058 let _formatting_request_signal = fake_server
11059 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
11060 assert_eq!(
11061 params.text_document.uri,
11062 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11063 );
11064 assert_eq!(params.options.tab_size, 8);
11065 Ok(Some(vec![]))
11066 });
11067 let save = editor
11068 .update_in(cx, |editor, window, cx| {
11069 editor.save(
11070 SaveOptions {
11071 format: true,
11072 autosave: false,
11073 },
11074 project.clone(),
11075 window,
11076 cx,
11077 )
11078 })
11079 .unwrap();
11080 cx.executor().start_waiting();
11081 save.await;
11082 }
11083}
11084
11085#[gpui::test]
11086async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
11087 init_test(cx, |settings| {
11088 settings.defaults.ensure_final_newline_on_save = Some(false);
11089 });
11090
11091 let fs = FakeFs::new(cx.executor());
11092 fs.insert_file(path!("/file.txt"), "foo".into()).await;
11093
11094 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
11095
11096 let buffer = project
11097 .update(cx, |project, cx| {
11098 project.open_local_buffer(path!("/file.txt"), cx)
11099 })
11100 .await
11101 .unwrap();
11102
11103 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11104 let (editor, cx) = cx.add_window_view(|window, cx| {
11105 build_editor_with_project(project.clone(), buffer, window, cx)
11106 });
11107 editor.update_in(cx, |editor, window, cx| {
11108 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
11109 s.select_ranges([0..0])
11110 });
11111 });
11112 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11113
11114 editor.update_in(cx, |editor, window, cx| {
11115 editor.handle_input("\n", window, cx)
11116 });
11117 cx.run_until_parked();
11118 save(&editor, &project, cx).await;
11119 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11120
11121 editor.update_in(cx, |editor, window, cx| {
11122 editor.undo(&Default::default(), window, cx);
11123 });
11124 save(&editor, &project, cx).await;
11125 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11126
11127 editor.update_in(cx, |editor, window, cx| {
11128 editor.redo(&Default::default(), window, cx);
11129 });
11130 cx.run_until_parked();
11131 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11132
11133 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
11134 let save = editor
11135 .update_in(cx, |editor, window, cx| {
11136 editor.save(
11137 SaveOptions {
11138 format: true,
11139 autosave: false,
11140 },
11141 project.clone(),
11142 window,
11143 cx,
11144 )
11145 })
11146 .unwrap();
11147 cx.executor().start_waiting();
11148 save.await;
11149 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11150 }
11151}
11152
11153#[gpui::test]
11154async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
11155 init_test(cx, |_| {});
11156
11157 let cols = 4;
11158 let rows = 10;
11159 let sample_text_1 = sample_text(rows, cols, 'a');
11160 assert_eq!(
11161 sample_text_1,
11162 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
11163 );
11164 let sample_text_2 = sample_text(rows, cols, 'l');
11165 assert_eq!(
11166 sample_text_2,
11167 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
11168 );
11169 let sample_text_3 = sample_text(rows, cols, 'v');
11170 assert_eq!(
11171 sample_text_3,
11172 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
11173 );
11174
11175 let fs = FakeFs::new(cx.executor());
11176 fs.insert_tree(
11177 path!("/a"),
11178 json!({
11179 "main.rs": sample_text_1,
11180 "other.rs": sample_text_2,
11181 "lib.rs": sample_text_3,
11182 }),
11183 )
11184 .await;
11185
11186 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11187 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11188 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11189
11190 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11191 language_registry.add(rust_lang());
11192 let mut fake_servers = language_registry.register_fake_lsp(
11193 "Rust",
11194 FakeLspAdapter {
11195 capabilities: lsp::ServerCapabilities {
11196 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11197 ..Default::default()
11198 },
11199 ..Default::default()
11200 },
11201 );
11202
11203 let worktree = project.update(cx, |project, cx| {
11204 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
11205 assert_eq!(worktrees.len(), 1);
11206 worktrees.pop().unwrap()
11207 });
11208 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11209
11210 let buffer_1 = project
11211 .update(cx, |project, cx| {
11212 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
11213 })
11214 .await
11215 .unwrap();
11216 let buffer_2 = project
11217 .update(cx, |project, cx| {
11218 project.open_buffer((worktree_id, rel_path("other.rs")), cx)
11219 })
11220 .await
11221 .unwrap();
11222 let buffer_3 = project
11223 .update(cx, |project, cx| {
11224 project.open_buffer((worktree_id, rel_path("lib.rs")), cx)
11225 })
11226 .await
11227 .unwrap();
11228
11229 let multi_buffer = cx.new(|cx| {
11230 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11231 multi_buffer.push_excerpts(
11232 buffer_1.clone(),
11233 [
11234 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11235 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11236 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11237 ],
11238 cx,
11239 );
11240 multi_buffer.push_excerpts(
11241 buffer_2.clone(),
11242 [
11243 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11244 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11245 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11246 ],
11247 cx,
11248 );
11249 multi_buffer.push_excerpts(
11250 buffer_3.clone(),
11251 [
11252 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11253 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11254 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11255 ],
11256 cx,
11257 );
11258 multi_buffer
11259 });
11260 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
11261 Editor::new(
11262 EditorMode::full(),
11263 multi_buffer,
11264 Some(project.clone()),
11265 window,
11266 cx,
11267 )
11268 });
11269
11270 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11271 editor.change_selections(
11272 SelectionEffects::scroll(Autoscroll::Next),
11273 window,
11274 cx,
11275 |s| s.select_ranges(Some(1..2)),
11276 );
11277 editor.insert("|one|two|three|", window, cx);
11278 });
11279 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11280 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11281 editor.change_selections(
11282 SelectionEffects::scroll(Autoscroll::Next),
11283 window,
11284 cx,
11285 |s| s.select_ranges(Some(60..70)),
11286 );
11287 editor.insert("|four|five|six|", window, cx);
11288 });
11289 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11290
11291 // First two buffers should be edited, but not the third one.
11292 assert_eq!(
11293 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11294 "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}",
11295 );
11296 buffer_1.update(cx, |buffer, _| {
11297 assert!(buffer.is_dirty());
11298 assert_eq!(
11299 buffer.text(),
11300 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
11301 )
11302 });
11303 buffer_2.update(cx, |buffer, _| {
11304 assert!(buffer.is_dirty());
11305 assert_eq!(
11306 buffer.text(),
11307 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
11308 )
11309 });
11310 buffer_3.update(cx, |buffer, _| {
11311 assert!(!buffer.is_dirty());
11312 assert_eq!(buffer.text(), sample_text_3,)
11313 });
11314 cx.executor().run_until_parked();
11315
11316 cx.executor().start_waiting();
11317 let save = multi_buffer_editor
11318 .update_in(cx, |editor, window, cx| {
11319 editor.save(
11320 SaveOptions {
11321 format: true,
11322 autosave: false,
11323 },
11324 project.clone(),
11325 window,
11326 cx,
11327 )
11328 })
11329 .unwrap();
11330
11331 let fake_server = fake_servers.next().await.unwrap();
11332 fake_server
11333 .server
11334 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
11335 Ok(Some(vec![lsp::TextEdit::new(
11336 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11337 format!("[{} formatted]", params.text_document.uri),
11338 )]))
11339 })
11340 .detach();
11341 save.await;
11342
11343 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
11344 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
11345 assert_eq!(
11346 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11347 uri!(
11348 "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}"
11349 ),
11350 );
11351 buffer_1.update(cx, |buffer, _| {
11352 assert!(!buffer.is_dirty());
11353 assert_eq!(
11354 buffer.text(),
11355 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
11356 )
11357 });
11358 buffer_2.update(cx, |buffer, _| {
11359 assert!(!buffer.is_dirty());
11360 assert_eq!(
11361 buffer.text(),
11362 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
11363 )
11364 });
11365 buffer_3.update(cx, |buffer, _| {
11366 assert!(!buffer.is_dirty());
11367 assert_eq!(buffer.text(), sample_text_3,)
11368 });
11369}
11370
11371#[gpui::test]
11372async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
11373 init_test(cx, |_| {});
11374
11375 let fs = FakeFs::new(cx.executor());
11376 fs.insert_tree(
11377 path!("/dir"),
11378 json!({
11379 "file1.rs": "fn main() { println!(\"hello\"); }",
11380 "file2.rs": "fn test() { println!(\"test\"); }",
11381 "file3.rs": "fn other() { println!(\"other\"); }\n",
11382 }),
11383 )
11384 .await;
11385
11386 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
11387 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11388 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11389
11390 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11391 language_registry.add(rust_lang());
11392
11393 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
11394 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11395
11396 // Open three buffers
11397 let buffer_1 = project
11398 .update(cx, |project, cx| {
11399 project.open_buffer((worktree_id, rel_path("file1.rs")), cx)
11400 })
11401 .await
11402 .unwrap();
11403 let buffer_2 = project
11404 .update(cx, |project, cx| {
11405 project.open_buffer((worktree_id, rel_path("file2.rs")), cx)
11406 })
11407 .await
11408 .unwrap();
11409 let buffer_3 = project
11410 .update(cx, |project, cx| {
11411 project.open_buffer((worktree_id, rel_path("file3.rs")), cx)
11412 })
11413 .await
11414 .unwrap();
11415
11416 // Create a multi-buffer with all three buffers
11417 let multi_buffer = cx.new(|cx| {
11418 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11419 multi_buffer.push_excerpts(
11420 buffer_1.clone(),
11421 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11422 cx,
11423 );
11424 multi_buffer.push_excerpts(
11425 buffer_2.clone(),
11426 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11427 cx,
11428 );
11429 multi_buffer.push_excerpts(
11430 buffer_3.clone(),
11431 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11432 cx,
11433 );
11434 multi_buffer
11435 });
11436
11437 let editor = cx.new_window_entity(|window, cx| {
11438 Editor::new(
11439 EditorMode::full(),
11440 multi_buffer,
11441 Some(project.clone()),
11442 window,
11443 cx,
11444 )
11445 });
11446
11447 // Edit only the first buffer
11448 editor.update_in(cx, |editor, window, cx| {
11449 editor.change_selections(
11450 SelectionEffects::scroll(Autoscroll::Next),
11451 window,
11452 cx,
11453 |s| s.select_ranges(Some(10..10)),
11454 );
11455 editor.insert("// edited", window, cx);
11456 });
11457
11458 // Verify that only buffer 1 is dirty
11459 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
11460 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11461 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11462
11463 // Get write counts after file creation (files were created with initial content)
11464 // We expect each file to have been written once during creation
11465 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
11466 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
11467 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
11468
11469 // Perform autosave
11470 let save_task = editor.update_in(cx, |editor, window, cx| {
11471 editor.save(
11472 SaveOptions {
11473 format: true,
11474 autosave: true,
11475 },
11476 project.clone(),
11477 window,
11478 cx,
11479 )
11480 });
11481 save_task.await.unwrap();
11482
11483 // Only the dirty buffer should have been saved
11484 assert_eq!(
11485 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11486 1,
11487 "Buffer 1 was dirty, so it should have been written once during autosave"
11488 );
11489 assert_eq!(
11490 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11491 0,
11492 "Buffer 2 was clean, so it should not have been written during autosave"
11493 );
11494 assert_eq!(
11495 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11496 0,
11497 "Buffer 3 was clean, so it should not have been written during autosave"
11498 );
11499
11500 // Verify buffer states after autosave
11501 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11502 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11503 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11504
11505 // Now perform a manual save (format = true)
11506 let save_task = editor.update_in(cx, |editor, window, cx| {
11507 editor.save(
11508 SaveOptions {
11509 format: true,
11510 autosave: false,
11511 },
11512 project.clone(),
11513 window,
11514 cx,
11515 )
11516 });
11517 save_task.await.unwrap();
11518
11519 // During manual save, clean buffers don't get written to disk
11520 // They just get did_save called for language server notifications
11521 assert_eq!(
11522 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11523 1,
11524 "Buffer 1 should only have been written once total (during autosave, not manual save)"
11525 );
11526 assert_eq!(
11527 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11528 0,
11529 "Buffer 2 should not have been written at all"
11530 );
11531 assert_eq!(
11532 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11533 0,
11534 "Buffer 3 should not have been written at all"
11535 );
11536}
11537
11538async fn setup_range_format_test(
11539 cx: &mut TestAppContext,
11540) -> (
11541 Entity<Project>,
11542 Entity<Editor>,
11543 &mut gpui::VisualTestContext,
11544 lsp::FakeLanguageServer,
11545) {
11546 init_test(cx, |_| {});
11547
11548 let fs = FakeFs::new(cx.executor());
11549 fs.insert_file(path!("/file.rs"), Default::default()).await;
11550
11551 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11552
11553 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11554 language_registry.add(rust_lang());
11555 let mut fake_servers = language_registry.register_fake_lsp(
11556 "Rust",
11557 FakeLspAdapter {
11558 capabilities: lsp::ServerCapabilities {
11559 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
11560 ..lsp::ServerCapabilities::default()
11561 },
11562 ..FakeLspAdapter::default()
11563 },
11564 );
11565
11566 let buffer = project
11567 .update(cx, |project, cx| {
11568 project.open_local_buffer(path!("/file.rs"), cx)
11569 })
11570 .await
11571 .unwrap();
11572
11573 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11574 let (editor, cx) = cx.add_window_view(|window, cx| {
11575 build_editor_with_project(project.clone(), buffer, window, cx)
11576 });
11577
11578 cx.executor().start_waiting();
11579 let fake_server = fake_servers.next().await.unwrap();
11580
11581 (project, editor, cx, fake_server)
11582}
11583
11584#[gpui::test]
11585async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
11586 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11587
11588 editor.update_in(cx, |editor, window, cx| {
11589 editor.set_text("one\ntwo\nthree\n", window, cx)
11590 });
11591 assert!(cx.read(|cx| editor.is_dirty(cx)));
11592
11593 let save = editor
11594 .update_in(cx, |editor, window, cx| {
11595 editor.save(
11596 SaveOptions {
11597 format: true,
11598 autosave: false,
11599 },
11600 project.clone(),
11601 window,
11602 cx,
11603 )
11604 })
11605 .unwrap();
11606 fake_server
11607 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
11608 assert_eq!(
11609 params.text_document.uri,
11610 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11611 );
11612 assert_eq!(params.options.tab_size, 4);
11613 Ok(Some(vec![lsp::TextEdit::new(
11614 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11615 ", ".to_string(),
11616 )]))
11617 })
11618 .next()
11619 .await;
11620 cx.executor().start_waiting();
11621 save.await;
11622 assert_eq!(
11623 editor.update(cx, |editor, cx| editor.text(cx)),
11624 "one, two\nthree\n"
11625 );
11626 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11627}
11628
11629#[gpui::test]
11630async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
11631 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11632
11633 editor.update_in(cx, |editor, window, cx| {
11634 editor.set_text("one\ntwo\nthree\n", window, cx)
11635 });
11636 assert!(cx.read(|cx| editor.is_dirty(cx)));
11637
11638 // Test that save still works when formatting hangs
11639 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
11640 move |params, _| async move {
11641 assert_eq!(
11642 params.text_document.uri,
11643 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11644 );
11645 futures::future::pending::<()>().await;
11646 unreachable!()
11647 },
11648 );
11649 let save = editor
11650 .update_in(cx, |editor, window, cx| {
11651 editor.save(
11652 SaveOptions {
11653 format: true,
11654 autosave: false,
11655 },
11656 project.clone(),
11657 window,
11658 cx,
11659 )
11660 })
11661 .unwrap();
11662 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11663 cx.executor().start_waiting();
11664 save.await;
11665 assert_eq!(
11666 editor.update(cx, |editor, cx| editor.text(cx)),
11667 "one\ntwo\nthree\n"
11668 );
11669 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11670}
11671
11672#[gpui::test]
11673async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
11674 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11675
11676 // Buffer starts clean, no formatting should be requested
11677 let save = editor
11678 .update_in(cx, |editor, window, cx| {
11679 editor.save(
11680 SaveOptions {
11681 format: false,
11682 autosave: false,
11683 },
11684 project.clone(),
11685 window,
11686 cx,
11687 )
11688 })
11689 .unwrap();
11690 let _pending_format_request = fake_server
11691 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
11692 panic!("Should not be invoked");
11693 })
11694 .next();
11695 cx.executor().start_waiting();
11696 save.await;
11697 cx.run_until_parked();
11698}
11699
11700#[gpui::test]
11701async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
11702 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11703
11704 // Set Rust language override and assert overridden tabsize is sent to language server
11705 update_test_language_settings(cx, |settings| {
11706 settings.languages.0.insert(
11707 "Rust".into(),
11708 LanguageSettingsContent {
11709 tab_size: NonZeroU32::new(8),
11710 ..Default::default()
11711 },
11712 );
11713 });
11714
11715 editor.update_in(cx, |editor, window, cx| {
11716 editor.set_text("something_new\n", window, cx)
11717 });
11718 assert!(cx.read(|cx| editor.is_dirty(cx)));
11719 let save = editor
11720 .update_in(cx, |editor, window, cx| {
11721 editor.save(
11722 SaveOptions {
11723 format: true,
11724 autosave: false,
11725 },
11726 project.clone(),
11727 window,
11728 cx,
11729 )
11730 })
11731 .unwrap();
11732 fake_server
11733 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
11734 assert_eq!(
11735 params.text_document.uri,
11736 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11737 );
11738 assert_eq!(params.options.tab_size, 8);
11739 Ok(Some(Vec::new()))
11740 })
11741 .next()
11742 .await;
11743 save.await;
11744}
11745
11746#[gpui::test]
11747async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
11748 init_test(cx, |settings| {
11749 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
11750 Formatter::LanguageServer { name: None },
11751 )))
11752 });
11753
11754 let fs = FakeFs::new(cx.executor());
11755 fs.insert_file(path!("/file.rs"), Default::default()).await;
11756
11757 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11758
11759 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11760 language_registry.add(Arc::new(Language::new(
11761 LanguageConfig {
11762 name: "Rust".into(),
11763 matcher: LanguageMatcher {
11764 path_suffixes: vec!["rs".to_string()],
11765 ..Default::default()
11766 },
11767 ..LanguageConfig::default()
11768 },
11769 Some(tree_sitter_rust::LANGUAGE.into()),
11770 )));
11771 update_test_language_settings(cx, |settings| {
11772 // Enable Prettier formatting for the same buffer, and ensure
11773 // LSP is called instead of Prettier.
11774 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
11775 });
11776 let mut fake_servers = language_registry.register_fake_lsp(
11777 "Rust",
11778 FakeLspAdapter {
11779 capabilities: lsp::ServerCapabilities {
11780 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11781 ..Default::default()
11782 },
11783 ..Default::default()
11784 },
11785 );
11786
11787 let buffer = project
11788 .update(cx, |project, cx| {
11789 project.open_local_buffer(path!("/file.rs"), cx)
11790 })
11791 .await
11792 .unwrap();
11793
11794 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11795 let (editor, cx) = cx.add_window_view(|window, cx| {
11796 build_editor_with_project(project.clone(), buffer, window, cx)
11797 });
11798 editor.update_in(cx, |editor, window, cx| {
11799 editor.set_text("one\ntwo\nthree\n", window, cx)
11800 });
11801
11802 cx.executor().start_waiting();
11803 let fake_server = fake_servers.next().await.unwrap();
11804
11805 let format = editor
11806 .update_in(cx, |editor, window, cx| {
11807 editor.perform_format(
11808 project.clone(),
11809 FormatTrigger::Manual,
11810 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
11811 window,
11812 cx,
11813 )
11814 })
11815 .unwrap();
11816 fake_server
11817 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
11818 assert_eq!(
11819 params.text_document.uri,
11820 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11821 );
11822 assert_eq!(params.options.tab_size, 4);
11823 Ok(Some(vec![lsp::TextEdit::new(
11824 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11825 ", ".to_string(),
11826 )]))
11827 })
11828 .next()
11829 .await;
11830 cx.executor().start_waiting();
11831 format.await;
11832 assert_eq!(
11833 editor.update(cx, |editor, cx| editor.text(cx)),
11834 "one, two\nthree\n"
11835 );
11836
11837 editor.update_in(cx, |editor, window, cx| {
11838 editor.set_text("one\ntwo\nthree\n", window, cx)
11839 });
11840 // Ensure we don't lock if formatting hangs.
11841 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11842 move |params, _| async move {
11843 assert_eq!(
11844 params.text_document.uri,
11845 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11846 );
11847 futures::future::pending::<()>().await;
11848 unreachable!()
11849 },
11850 );
11851 let format = editor
11852 .update_in(cx, |editor, window, cx| {
11853 editor.perform_format(
11854 project,
11855 FormatTrigger::Manual,
11856 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
11857 window,
11858 cx,
11859 )
11860 })
11861 .unwrap();
11862 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11863 cx.executor().start_waiting();
11864 format.await;
11865 assert_eq!(
11866 editor.update(cx, |editor, cx| editor.text(cx)),
11867 "one\ntwo\nthree\n"
11868 );
11869}
11870
11871#[gpui::test]
11872async fn test_multiple_formatters(cx: &mut TestAppContext) {
11873 init_test(cx, |settings| {
11874 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
11875 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
11876 Formatter::LanguageServer { name: None },
11877 Formatter::CodeAction("code-action-1".into()),
11878 Formatter::CodeAction("code-action-2".into()),
11879 ])))
11880 });
11881
11882 let fs = FakeFs::new(cx.executor());
11883 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
11884 .await;
11885
11886 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11887 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11888 language_registry.add(rust_lang());
11889
11890 let mut fake_servers = language_registry.register_fake_lsp(
11891 "Rust",
11892 FakeLspAdapter {
11893 capabilities: lsp::ServerCapabilities {
11894 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11895 execute_command_provider: Some(lsp::ExecuteCommandOptions {
11896 commands: vec!["the-command-for-code-action-1".into()],
11897 ..Default::default()
11898 }),
11899 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
11900 ..Default::default()
11901 },
11902 ..Default::default()
11903 },
11904 );
11905
11906 let buffer = project
11907 .update(cx, |project, cx| {
11908 project.open_local_buffer(path!("/file.rs"), cx)
11909 })
11910 .await
11911 .unwrap();
11912
11913 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11914 let (editor, cx) = cx.add_window_view(|window, cx| {
11915 build_editor_with_project(project.clone(), buffer, window, cx)
11916 });
11917
11918 cx.executor().start_waiting();
11919
11920 let fake_server = fake_servers.next().await.unwrap();
11921 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11922 move |_params, _| async move {
11923 Ok(Some(vec![lsp::TextEdit::new(
11924 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
11925 "applied-formatting\n".to_string(),
11926 )]))
11927 },
11928 );
11929 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
11930 move |params, _| async move {
11931 let requested_code_actions = params.context.only.expect("Expected code action request");
11932 assert_eq!(requested_code_actions.len(), 1);
11933
11934 let uri = lsp::Uri::from_file_path(path!("/file.rs")).unwrap();
11935 let code_action = match requested_code_actions[0].as_str() {
11936 "code-action-1" => lsp::CodeAction {
11937 kind: Some("code-action-1".into()),
11938 edit: Some(lsp::WorkspaceEdit::new(
11939 [(
11940 uri,
11941 vec![lsp::TextEdit::new(
11942 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
11943 "applied-code-action-1-edit\n".to_string(),
11944 )],
11945 )]
11946 .into_iter()
11947 .collect(),
11948 )),
11949 command: Some(lsp::Command {
11950 command: "the-command-for-code-action-1".into(),
11951 ..Default::default()
11952 }),
11953 ..Default::default()
11954 },
11955 "code-action-2" => lsp::CodeAction {
11956 kind: Some("code-action-2".into()),
11957 edit: Some(lsp::WorkspaceEdit::new(
11958 [(
11959 uri,
11960 vec![lsp::TextEdit::new(
11961 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
11962 "applied-code-action-2-edit\n".to_string(),
11963 )],
11964 )]
11965 .into_iter()
11966 .collect(),
11967 )),
11968 ..Default::default()
11969 },
11970 req => panic!("Unexpected code action request: {:?}", req),
11971 };
11972 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
11973 code_action,
11974 )]))
11975 },
11976 );
11977
11978 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
11979 move |params, _| async move { Ok(params) }
11980 });
11981
11982 let command_lock = Arc::new(futures::lock::Mutex::new(()));
11983 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
11984 let fake = fake_server.clone();
11985 let lock = command_lock.clone();
11986 move |params, _| {
11987 assert_eq!(params.command, "the-command-for-code-action-1");
11988 let fake = fake.clone();
11989 let lock = lock.clone();
11990 async move {
11991 lock.lock().await;
11992 fake.server
11993 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
11994 label: None,
11995 edit: lsp::WorkspaceEdit {
11996 changes: Some(
11997 [(
11998 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
11999 vec![lsp::TextEdit {
12000 range: lsp::Range::new(
12001 lsp::Position::new(0, 0),
12002 lsp::Position::new(0, 0),
12003 ),
12004 new_text: "applied-code-action-1-command\n".into(),
12005 }],
12006 )]
12007 .into_iter()
12008 .collect(),
12009 ),
12010 ..Default::default()
12011 },
12012 })
12013 .await
12014 .into_response()
12015 .unwrap();
12016 Ok(Some(json!(null)))
12017 }
12018 }
12019 });
12020
12021 cx.executor().start_waiting();
12022 editor
12023 .update_in(cx, |editor, window, cx| {
12024 editor.perform_format(
12025 project.clone(),
12026 FormatTrigger::Manual,
12027 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12028 window,
12029 cx,
12030 )
12031 })
12032 .unwrap()
12033 .await;
12034 editor.update(cx, |editor, cx| {
12035 assert_eq!(
12036 editor.text(cx),
12037 r#"
12038 applied-code-action-2-edit
12039 applied-code-action-1-command
12040 applied-code-action-1-edit
12041 applied-formatting
12042 one
12043 two
12044 three
12045 "#
12046 .unindent()
12047 );
12048 });
12049
12050 editor.update_in(cx, |editor, window, cx| {
12051 editor.undo(&Default::default(), window, cx);
12052 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12053 });
12054
12055 // Perform a manual edit while waiting for an LSP command
12056 // that's being run as part of a formatting code action.
12057 let lock_guard = command_lock.lock().await;
12058 let format = editor
12059 .update_in(cx, |editor, window, cx| {
12060 editor.perform_format(
12061 project.clone(),
12062 FormatTrigger::Manual,
12063 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12064 window,
12065 cx,
12066 )
12067 })
12068 .unwrap();
12069 cx.run_until_parked();
12070 editor.update(cx, |editor, cx| {
12071 assert_eq!(
12072 editor.text(cx),
12073 r#"
12074 applied-code-action-1-edit
12075 applied-formatting
12076 one
12077 two
12078 three
12079 "#
12080 .unindent()
12081 );
12082
12083 editor.buffer.update(cx, |buffer, cx| {
12084 let ix = buffer.len(cx);
12085 buffer.edit([(ix..ix, "edited\n")], None, cx);
12086 });
12087 });
12088
12089 // Allow the LSP command to proceed. Because the buffer was edited,
12090 // the second code action will not be run.
12091 drop(lock_guard);
12092 format.await;
12093 editor.update_in(cx, |editor, window, cx| {
12094 assert_eq!(
12095 editor.text(cx),
12096 r#"
12097 applied-code-action-1-command
12098 applied-code-action-1-edit
12099 applied-formatting
12100 one
12101 two
12102 three
12103 edited
12104 "#
12105 .unindent()
12106 );
12107
12108 // The manual edit is undone first, because it is the last thing the user did
12109 // (even though the command completed afterwards).
12110 editor.undo(&Default::default(), window, cx);
12111 assert_eq!(
12112 editor.text(cx),
12113 r#"
12114 applied-code-action-1-command
12115 applied-code-action-1-edit
12116 applied-formatting
12117 one
12118 two
12119 three
12120 "#
12121 .unindent()
12122 );
12123
12124 // All the formatting (including the command, which completed after the manual edit)
12125 // is undone together.
12126 editor.undo(&Default::default(), window, cx);
12127 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12128 });
12129}
12130
12131#[gpui::test]
12132async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
12133 init_test(cx, |settings| {
12134 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
12135 Formatter::LanguageServer { name: None },
12136 ])))
12137 });
12138
12139 let fs = FakeFs::new(cx.executor());
12140 fs.insert_file(path!("/file.ts"), Default::default()).await;
12141
12142 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12143
12144 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12145 language_registry.add(Arc::new(Language::new(
12146 LanguageConfig {
12147 name: "TypeScript".into(),
12148 matcher: LanguageMatcher {
12149 path_suffixes: vec!["ts".to_string()],
12150 ..Default::default()
12151 },
12152 ..LanguageConfig::default()
12153 },
12154 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12155 )));
12156 update_test_language_settings(cx, |settings| {
12157 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
12158 });
12159 let mut fake_servers = language_registry.register_fake_lsp(
12160 "TypeScript",
12161 FakeLspAdapter {
12162 capabilities: lsp::ServerCapabilities {
12163 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12164 ..Default::default()
12165 },
12166 ..Default::default()
12167 },
12168 );
12169
12170 let buffer = project
12171 .update(cx, |project, cx| {
12172 project.open_local_buffer(path!("/file.ts"), cx)
12173 })
12174 .await
12175 .unwrap();
12176
12177 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12178 let (editor, cx) = cx.add_window_view(|window, cx| {
12179 build_editor_with_project(project.clone(), buffer, window, cx)
12180 });
12181 editor.update_in(cx, |editor, window, cx| {
12182 editor.set_text(
12183 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12184 window,
12185 cx,
12186 )
12187 });
12188
12189 cx.executor().start_waiting();
12190 let fake_server = fake_servers.next().await.unwrap();
12191
12192 let format = editor
12193 .update_in(cx, |editor, window, cx| {
12194 editor.perform_code_action_kind(
12195 project.clone(),
12196 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12197 window,
12198 cx,
12199 )
12200 })
12201 .unwrap();
12202 fake_server
12203 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
12204 assert_eq!(
12205 params.text_document.uri,
12206 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12207 );
12208 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12209 lsp::CodeAction {
12210 title: "Organize Imports".to_string(),
12211 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
12212 edit: Some(lsp::WorkspaceEdit {
12213 changes: Some(
12214 [(
12215 params.text_document.uri.clone(),
12216 vec![lsp::TextEdit::new(
12217 lsp::Range::new(
12218 lsp::Position::new(1, 0),
12219 lsp::Position::new(2, 0),
12220 ),
12221 "".to_string(),
12222 )],
12223 )]
12224 .into_iter()
12225 .collect(),
12226 ),
12227 ..Default::default()
12228 }),
12229 ..Default::default()
12230 },
12231 )]))
12232 })
12233 .next()
12234 .await;
12235 cx.executor().start_waiting();
12236 format.await;
12237 assert_eq!(
12238 editor.update(cx, |editor, cx| editor.text(cx)),
12239 "import { a } from 'module';\n\nconst x = a;\n"
12240 );
12241
12242 editor.update_in(cx, |editor, window, cx| {
12243 editor.set_text(
12244 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12245 window,
12246 cx,
12247 )
12248 });
12249 // Ensure we don't lock if code action hangs.
12250 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12251 move |params, _| async move {
12252 assert_eq!(
12253 params.text_document.uri,
12254 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12255 );
12256 futures::future::pending::<()>().await;
12257 unreachable!()
12258 },
12259 );
12260 let format = editor
12261 .update_in(cx, |editor, window, cx| {
12262 editor.perform_code_action_kind(
12263 project,
12264 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12265 window,
12266 cx,
12267 )
12268 })
12269 .unwrap();
12270 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
12271 cx.executor().start_waiting();
12272 format.await;
12273 assert_eq!(
12274 editor.update(cx, |editor, cx| editor.text(cx)),
12275 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
12276 );
12277}
12278
12279#[gpui::test]
12280async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
12281 init_test(cx, |_| {});
12282
12283 let mut cx = EditorLspTestContext::new_rust(
12284 lsp::ServerCapabilities {
12285 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12286 ..Default::default()
12287 },
12288 cx,
12289 )
12290 .await;
12291
12292 cx.set_state(indoc! {"
12293 one.twoˇ
12294 "});
12295
12296 // The format request takes a long time. When it completes, it inserts
12297 // a newline and an indent before the `.`
12298 cx.lsp
12299 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
12300 let executor = cx.background_executor().clone();
12301 async move {
12302 executor.timer(Duration::from_millis(100)).await;
12303 Ok(Some(vec![lsp::TextEdit {
12304 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
12305 new_text: "\n ".into(),
12306 }]))
12307 }
12308 });
12309
12310 // Submit a format request.
12311 let format_1 = cx
12312 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12313 .unwrap();
12314 cx.executor().run_until_parked();
12315
12316 // Submit a second format request.
12317 let format_2 = cx
12318 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12319 .unwrap();
12320 cx.executor().run_until_parked();
12321
12322 // Wait for both format requests to complete
12323 cx.executor().advance_clock(Duration::from_millis(200));
12324 cx.executor().start_waiting();
12325 format_1.await.unwrap();
12326 cx.executor().start_waiting();
12327 format_2.await.unwrap();
12328
12329 // The formatting edits only happens once.
12330 cx.assert_editor_state(indoc! {"
12331 one
12332 .twoˇ
12333 "});
12334}
12335
12336#[gpui::test]
12337async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
12338 init_test(cx, |settings| {
12339 settings.defaults.formatter = Some(SelectedFormatter::Auto)
12340 });
12341
12342 let mut cx = EditorLspTestContext::new_rust(
12343 lsp::ServerCapabilities {
12344 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12345 ..Default::default()
12346 },
12347 cx,
12348 )
12349 .await;
12350
12351 // Set up a buffer white some trailing whitespace and no trailing newline.
12352 cx.set_state(
12353 &[
12354 "one ", //
12355 "twoˇ", //
12356 "three ", //
12357 "four", //
12358 ]
12359 .join("\n"),
12360 );
12361
12362 // Submit a format request.
12363 let format = cx
12364 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12365 .unwrap();
12366
12367 // Record which buffer changes have been sent to the language server
12368 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
12369 cx.lsp
12370 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
12371 let buffer_changes = buffer_changes.clone();
12372 move |params, _| {
12373 buffer_changes.lock().extend(
12374 params
12375 .content_changes
12376 .into_iter()
12377 .map(|e| (e.range.unwrap(), e.text)),
12378 );
12379 }
12380 });
12381
12382 // Handle formatting requests to the language server.
12383 cx.lsp
12384 .set_request_handler::<lsp::request::Formatting, _, _>({
12385 let buffer_changes = buffer_changes.clone();
12386 move |_, _| {
12387 // When formatting is requested, trailing whitespace has already been stripped,
12388 // and the trailing newline has already been added.
12389 assert_eq!(
12390 &buffer_changes.lock()[1..],
12391 &[
12392 (
12393 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
12394 "".into()
12395 ),
12396 (
12397 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
12398 "".into()
12399 ),
12400 (
12401 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
12402 "\n".into()
12403 ),
12404 ]
12405 );
12406
12407 // Insert blank lines between each line of the buffer.
12408 async move {
12409 Ok(Some(vec![
12410 lsp::TextEdit {
12411 range: lsp::Range::new(
12412 lsp::Position::new(1, 0),
12413 lsp::Position::new(1, 0),
12414 ),
12415 new_text: "\n".into(),
12416 },
12417 lsp::TextEdit {
12418 range: lsp::Range::new(
12419 lsp::Position::new(2, 0),
12420 lsp::Position::new(2, 0),
12421 ),
12422 new_text: "\n".into(),
12423 },
12424 ]))
12425 }
12426 }
12427 });
12428
12429 // After formatting the buffer, the trailing whitespace is stripped,
12430 // a newline is appended, and the edits provided by the language server
12431 // have been applied.
12432 format.await.unwrap();
12433 cx.assert_editor_state(
12434 &[
12435 "one", //
12436 "", //
12437 "twoˇ", //
12438 "", //
12439 "three", //
12440 "four", //
12441 "", //
12442 ]
12443 .join("\n"),
12444 );
12445
12446 // Undoing the formatting undoes the trailing whitespace removal, the
12447 // trailing newline, and the LSP edits.
12448 cx.update_buffer(|buffer, cx| buffer.undo(cx));
12449 cx.assert_editor_state(
12450 &[
12451 "one ", //
12452 "twoˇ", //
12453 "three ", //
12454 "four", //
12455 ]
12456 .join("\n"),
12457 );
12458}
12459
12460#[gpui::test]
12461async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
12462 cx: &mut TestAppContext,
12463) {
12464 init_test(cx, |_| {});
12465
12466 cx.update(|cx| {
12467 cx.update_global::<SettingsStore, _>(|settings, cx| {
12468 settings.update_user_settings(cx, |settings| {
12469 settings.editor.auto_signature_help = Some(true);
12470 });
12471 });
12472 });
12473
12474 let mut cx = EditorLspTestContext::new_rust(
12475 lsp::ServerCapabilities {
12476 signature_help_provider: Some(lsp::SignatureHelpOptions {
12477 ..Default::default()
12478 }),
12479 ..Default::default()
12480 },
12481 cx,
12482 )
12483 .await;
12484
12485 let language = Language::new(
12486 LanguageConfig {
12487 name: "Rust".into(),
12488 brackets: BracketPairConfig {
12489 pairs: vec![
12490 BracketPair {
12491 start: "{".to_string(),
12492 end: "}".to_string(),
12493 close: true,
12494 surround: true,
12495 newline: true,
12496 },
12497 BracketPair {
12498 start: "(".to_string(),
12499 end: ")".to_string(),
12500 close: true,
12501 surround: true,
12502 newline: true,
12503 },
12504 BracketPair {
12505 start: "/*".to_string(),
12506 end: " */".to_string(),
12507 close: true,
12508 surround: true,
12509 newline: true,
12510 },
12511 BracketPair {
12512 start: "[".to_string(),
12513 end: "]".to_string(),
12514 close: false,
12515 surround: false,
12516 newline: true,
12517 },
12518 BracketPair {
12519 start: "\"".to_string(),
12520 end: "\"".to_string(),
12521 close: true,
12522 surround: true,
12523 newline: false,
12524 },
12525 BracketPair {
12526 start: "<".to_string(),
12527 end: ">".to_string(),
12528 close: false,
12529 surround: true,
12530 newline: true,
12531 },
12532 ],
12533 ..Default::default()
12534 },
12535 autoclose_before: "})]".to_string(),
12536 ..Default::default()
12537 },
12538 Some(tree_sitter_rust::LANGUAGE.into()),
12539 );
12540 let language = Arc::new(language);
12541
12542 cx.language_registry().add(language.clone());
12543 cx.update_buffer(|buffer, cx| {
12544 buffer.set_language(Some(language), cx);
12545 });
12546
12547 cx.set_state(
12548 &r#"
12549 fn main() {
12550 sampleˇ
12551 }
12552 "#
12553 .unindent(),
12554 );
12555
12556 cx.update_editor(|editor, window, cx| {
12557 editor.handle_input("(", window, cx);
12558 });
12559 cx.assert_editor_state(
12560 &"
12561 fn main() {
12562 sample(ˇ)
12563 }
12564 "
12565 .unindent(),
12566 );
12567
12568 let mocked_response = lsp::SignatureHelp {
12569 signatures: vec![lsp::SignatureInformation {
12570 label: "fn sample(param1: u8, param2: u8)".to_string(),
12571 documentation: None,
12572 parameters: Some(vec![
12573 lsp::ParameterInformation {
12574 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12575 documentation: None,
12576 },
12577 lsp::ParameterInformation {
12578 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12579 documentation: None,
12580 },
12581 ]),
12582 active_parameter: None,
12583 }],
12584 active_signature: Some(0),
12585 active_parameter: Some(0),
12586 };
12587 handle_signature_help_request(&mut cx, mocked_response).await;
12588
12589 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12590 .await;
12591
12592 cx.editor(|editor, _, _| {
12593 let signature_help_state = editor.signature_help_state.popover().cloned();
12594 let signature = signature_help_state.unwrap();
12595 assert_eq!(
12596 signature.signatures[signature.current_signature].label,
12597 "fn sample(param1: u8, param2: u8)"
12598 );
12599 });
12600}
12601
12602#[gpui::test]
12603async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
12604 init_test(cx, |_| {});
12605
12606 cx.update(|cx| {
12607 cx.update_global::<SettingsStore, _>(|settings, cx| {
12608 settings.update_user_settings(cx, |settings| {
12609 settings.editor.auto_signature_help = Some(false);
12610 settings.editor.show_signature_help_after_edits = Some(false);
12611 });
12612 });
12613 });
12614
12615 let mut cx = EditorLspTestContext::new_rust(
12616 lsp::ServerCapabilities {
12617 signature_help_provider: Some(lsp::SignatureHelpOptions {
12618 ..Default::default()
12619 }),
12620 ..Default::default()
12621 },
12622 cx,
12623 )
12624 .await;
12625
12626 let language = Language::new(
12627 LanguageConfig {
12628 name: "Rust".into(),
12629 brackets: BracketPairConfig {
12630 pairs: vec![
12631 BracketPair {
12632 start: "{".to_string(),
12633 end: "}".to_string(),
12634 close: true,
12635 surround: true,
12636 newline: true,
12637 },
12638 BracketPair {
12639 start: "(".to_string(),
12640 end: ")".to_string(),
12641 close: true,
12642 surround: true,
12643 newline: true,
12644 },
12645 BracketPair {
12646 start: "/*".to_string(),
12647 end: " */".to_string(),
12648 close: true,
12649 surround: true,
12650 newline: true,
12651 },
12652 BracketPair {
12653 start: "[".to_string(),
12654 end: "]".to_string(),
12655 close: false,
12656 surround: false,
12657 newline: true,
12658 },
12659 BracketPair {
12660 start: "\"".to_string(),
12661 end: "\"".to_string(),
12662 close: true,
12663 surround: true,
12664 newline: false,
12665 },
12666 BracketPair {
12667 start: "<".to_string(),
12668 end: ">".to_string(),
12669 close: false,
12670 surround: true,
12671 newline: true,
12672 },
12673 ],
12674 ..Default::default()
12675 },
12676 autoclose_before: "})]".to_string(),
12677 ..Default::default()
12678 },
12679 Some(tree_sitter_rust::LANGUAGE.into()),
12680 );
12681 let language = Arc::new(language);
12682
12683 cx.language_registry().add(language.clone());
12684 cx.update_buffer(|buffer, cx| {
12685 buffer.set_language(Some(language), cx);
12686 });
12687
12688 // Ensure that signature_help is not called when no signature help is enabled.
12689 cx.set_state(
12690 &r#"
12691 fn main() {
12692 sampleˇ
12693 }
12694 "#
12695 .unindent(),
12696 );
12697 cx.update_editor(|editor, window, cx| {
12698 editor.handle_input("(", window, cx);
12699 });
12700 cx.assert_editor_state(
12701 &"
12702 fn main() {
12703 sample(ˇ)
12704 }
12705 "
12706 .unindent(),
12707 );
12708 cx.editor(|editor, _, _| {
12709 assert!(editor.signature_help_state.task().is_none());
12710 });
12711
12712 let mocked_response = lsp::SignatureHelp {
12713 signatures: vec![lsp::SignatureInformation {
12714 label: "fn sample(param1: u8, param2: u8)".to_string(),
12715 documentation: None,
12716 parameters: Some(vec![
12717 lsp::ParameterInformation {
12718 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12719 documentation: None,
12720 },
12721 lsp::ParameterInformation {
12722 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12723 documentation: None,
12724 },
12725 ]),
12726 active_parameter: None,
12727 }],
12728 active_signature: Some(0),
12729 active_parameter: Some(0),
12730 };
12731
12732 // Ensure that signature_help is called when enabled afte edits
12733 cx.update(|_, cx| {
12734 cx.update_global::<SettingsStore, _>(|settings, cx| {
12735 settings.update_user_settings(cx, |settings| {
12736 settings.editor.auto_signature_help = Some(false);
12737 settings.editor.show_signature_help_after_edits = Some(true);
12738 });
12739 });
12740 });
12741 cx.set_state(
12742 &r#"
12743 fn main() {
12744 sampleˇ
12745 }
12746 "#
12747 .unindent(),
12748 );
12749 cx.update_editor(|editor, window, cx| {
12750 editor.handle_input("(", window, cx);
12751 });
12752 cx.assert_editor_state(
12753 &"
12754 fn main() {
12755 sample(ˇ)
12756 }
12757 "
12758 .unindent(),
12759 );
12760 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12761 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12762 .await;
12763 cx.update_editor(|editor, _, _| {
12764 let signature_help_state = editor.signature_help_state.popover().cloned();
12765 assert!(signature_help_state.is_some());
12766 let signature = signature_help_state.unwrap();
12767 assert_eq!(
12768 signature.signatures[signature.current_signature].label,
12769 "fn sample(param1: u8, param2: u8)"
12770 );
12771 editor.signature_help_state = SignatureHelpState::default();
12772 });
12773
12774 // Ensure that signature_help is called when auto signature help override is enabled
12775 cx.update(|_, cx| {
12776 cx.update_global::<SettingsStore, _>(|settings, cx| {
12777 settings.update_user_settings(cx, |settings| {
12778 settings.editor.auto_signature_help = Some(true);
12779 settings.editor.show_signature_help_after_edits = Some(false);
12780 });
12781 });
12782 });
12783 cx.set_state(
12784 &r#"
12785 fn main() {
12786 sampleˇ
12787 }
12788 "#
12789 .unindent(),
12790 );
12791 cx.update_editor(|editor, window, cx| {
12792 editor.handle_input("(", window, cx);
12793 });
12794 cx.assert_editor_state(
12795 &"
12796 fn main() {
12797 sample(ˇ)
12798 }
12799 "
12800 .unindent(),
12801 );
12802 handle_signature_help_request(&mut cx, mocked_response).await;
12803 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12804 .await;
12805 cx.editor(|editor, _, _| {
12806 let signature_help_state = editor.signature_help_state.popover().cloned();
12807 assert!(signature_help_state.is_some());
12808 let signature = signature_help_state.unwrap();
12809 assert_eq!(
12810 signature.signatures[signature.current_signature].label,
12811 "fn sample(param1: u8, param2: u8)"
12812 );
12813 });
12814}
12815
12816#[gpui::test]
12817async fn test_signature_help(cx: &mut TestAppContext) {
12818 init_test(cx, |_| {});
12819 cx.update(|cx| {
12820 cx.update_global::<SettingsStore, _>(|settings, cx| {
12821 settings.update_user_settings(cx, |settings| {
12822 settings.editor.auto_signature_help = Some(true);
12823 });
12824 });
12825 });
12826
12827 let mut cx = EditorLspTestContext::new_rust(
12828 lsp::ServerCapabilities {
12829 signature_help_provider: Some(lsp::SignatureHelpOptions {
12830 ..Default::default()
12831 }),
12832 ..Default::default()
12833 },
12834 cx,
12835 )
12836 .await;
12837
12838 // A test that directly calls `show_signature_help`
12839 cx.update_editor(|editor, window, cx| {
12840 editor.show_signature_help(&ShowSignatureHelp, window, cx);
12841 });
12842
12843 let mocked_response = lsp::SignatureHelp {
12844 signatures: vec![lsp::SignatureInformation {
12845 label: "fn sample(param1: u8, param2: u8)".to_string(),
12846 documentation: None,
12847 parameters: Some(vec![
12848 lsp::ParameterInformation {
12849 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12850 documentation: None,
12851 },
12852 lsp::ParameterInformation {
12853 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12854 documentation: None,
12855 },
12856 ]),
12857 active_parameter: None,
12858 }],
12859 active_signature: Some(0),
12860 active_parameter: Some(0),
12861 };
12862 handle_signature_help_request(&mut cx, mocked_response).await;
12863
12864 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12865 .await;
12866
12867 cx.editor(|editor, _, _| {
12868 let signature_help_state = editor.signature_help_state.popover().cloned();
12869 assert!(signature_help_state.is_some());
12870 let signature = signature_help_state.unwrap();
12871 assert_eq!(
12872 signature.signatures[signature.current_signature].label,
12873 "fn sample(param1: u8, param2: u8)"
12874 );
12875 });
12876
12877 // When exiting outside from inside the brackets, `signature_help` is closed.
12878 cx.set_state(indoc! {"
12879 fn main() {
12880 sample(ˇ);
12881 }
12882
12883 fn sample(param1: u8, param2: u8) {}
12884 "});
12885
12886 cx.update_editor(|editor, window, cx| {
12887 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12888 s.select_ranges([0..0])
12889 });
12890 });
12891
12892 let mocked_response = lsp::SignatureHelp {
12893 signatures: Vec::new(),
12894 active_signature: None,
12895 active_parameter: None,
12896 };
12897 handle_signature_help_request(&mut cx, mocked_response).await;
12898
12899 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
12900 .await;
12901
12902 cx.editor(|editor, _, _| {
12903 assert!(!editor.signature_help_state.is_shown());
12904 });
12905
12906 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
12907 cx.set_state(indoc! {"
12908 fn main() {
12909 sample(ˇ);
12910 }
12911
12912 fn sample(param1: u8, param2: u8) {}
12913 "});
12914
12915 let mocked_response = lsp::SignatureHelp {
12916 signatures: vec![lsp::SignatureInformation {
12917 label: "fn sample(param1: u8, param2: u8)".to_string(),
12918 documentation: None,
12919 parameters: Some(vec![
12920 lsp::ParameterInformation {
12921 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12922 documentation: None,
12923 },
12924 lsp::ParameterInformation {
12925 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12926 documentation: None,
12927 },
12928 ]),
12929 active_parameter: None,
12930 }],
12931 active_signature: Some(0),
12932 active_parameter: Some(0),
12933 };
12934 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12935 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12936 .await;
12937 cx.editor(|editor, _, _| {
12938 assert!(editor.signature_help_state.is_shown());
12939 });
12940
12941 // Restore the popover with more parameter input
12942 cx.set_state(indoc! {"
12943 fn main() {
12944 sample(param1, param2ˇ);
12945 }
12946
12947 fn sample(param1: u8, param2: u8) {}
12948 "});
12949
12950 let mocked_response = lsp::SignatureHelp {
12951 signatures: vec![lsp::SignatureInformation {
12952 label: "fn sample(param1: u8, param2: u8)".to_string(),
12953 documentation: None,
12954 parameters: Some(vec![
12955 lsp::ParameterInformation {
12956 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12957 documentation: None,
12958 },
12959 lsp::ParameterInformation {
12960 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12961 documentation: None,
12962 },
12963 ]),
12964 active_parameter: None,
12965 }],
12966 active_signature: Some(0),
12967 active_parameter: Some(1),
12968 };
12969 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12970 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12971 .await;
12972
12973 // When selecting a range, the popover is gone.
12974 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
12975 cx.update_editor(|editor, window, cx| {
12976 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12977 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
12978 })
12979 });
12980 cx.assert_editor_state(indoc! {"
12981 fn main() {
12982 sample(param1, «ˇparam2»);
12983 }
12984
12985 fn sample(param1: u8, param2: u8) {}
12986 "});
12987 cx.editor(|editor, _, _| {
12988 assert!(!editor.signature_help_state.is_shown());
12989 });
12990
12991 // When unselecting again, the popover is back if within the brackets.
12992 cx.update_editor(|editor, window, cx| {
12993 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12994 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
12995 })
12996 });
12997 cx.assert_editor_state(indoc! {"
12998 fn main() {
12999 sample(param1, ˇparam2);
13000 }
13001
13002 fn sample(param1: u8, param2: u8) {}
13003 "});
13004 handle_signature_help_request(&mut cx, mocked_response).await;
13005 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13006 .await;
13007 cx.editor(|editor, _, _| {
13008 assert!(editor.signature_help_state.is_shown());
13009 });
13010
13011 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
13012 cx.update_editor(|editor, window, cx| {
13013 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13014 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
13015 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13016 })
13017 });
13018 cx.assert_editor_state(indoc! {"
13019 fn main() {
13020 sample(param1, ˇparam2);
13021 }
13022
13023 fn sample(param1: u8, param2: u8) {}
13024 "});
13025
13026 let mocked_response = lsp::SignatureHelp {
13027 signatures: vec![lsp::SignatureInformation {
13028 label: "fn sample(param1: u8, param2: u8)".to_string(),
13029 documentation: None,
13030 parameters: Some(vec![
13031 lsp::ParameterInformation {
13032 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13033 documentation: None,
13034 },
13035 lsp::ParameterInformation {
13036 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13037 documentation: None,
13038 },
13039 ]),
13040 active_parameter: None,
13041 }],
13042 active_signature: Some(0),
13043 active_parameter: Some(1),
13044 };
13045 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13046 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13047 .await;
13048 cx.update_editor(|editor, _, cx| {
13049 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
13050 });
13051 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13052 .await;
13053 cx.update_editor(|editor, window, cx| {
13054 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13055 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13056 })
13057 });
13058 cx.assert_editor_state(indoc! {"
13059 fn main() {
13060 sample(param1, «ˇparam2»);
13061 }
13062
13063 fn sample(param1: u8, param2: u8) {}
13064 "});
13065 cx.update_editor(|editor, window, cx| {
13066 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13067 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13068 })
13069 });
13070 cx.assert_editor_state(indoc! {"
13071 fn main() {
13072 sample(param1, ˇparam2);
13073 }
13074
13075 fn sample(param1: u8, param2: u8) {}
13076 "});
13077 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
13078 .await;
13079}
13080
13081#[gpui::test]
13082async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
13083 init_test(cx, |_| {});
13084
13085 let mut cx = EditorLspTestContext::new_rust(
13086 lsp::ServerCapabilities {
13087 signature_help_provider: Some(lsp::SignatureHelpOptions {
13088 ..Default::default()
13089 }),
13090 ..Default::default()
13091 },
13092 cx,
13093 )
13094 .await;
13095
13096 cx.set_state(indoc! {"
13097 fn main() {
13098 overloadedˇ
13099 }
13100 "});
13101
13102 cx.update_editor(|editor, window, cx| {
13103 editor.handle_input("(", window, cx);
13104 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13105 });
13106
13107 // Mock response with 3 signatures
13108 let mocked_response = lsp::SignatureHelp {
13109 signatures: vec![
13110 lsp::SignatureInformation {
13111 label: "fn overloaded(x: i32)".to_string(),
13112 documentation: None,
13113 parameters: Some(vec![lsp::ParameterInformation {
13114 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13115 documentation: None,
13116 }]),
13117 active_parameter: None,
13118 },
13119 lsp::SignatureInformation {
13120 label: "fn overloaded(x: i32, y: i32)".to_string(),
13121 documentation: None,
13122 parameters: Some(vec![
13123 lsp::ParameterInformation {
13124 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13125 documentation: None,
13126 },
13127 lsp::ParameterInformation {
13128 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13129 documentation: None,
13130 },
13131 ]),
13132 active_parameter: None,
13133 },
13134 lsp::SignatureInformation {
13135 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
13136 documentation: None,
13137 parameters: Some(vec![
13138 lsp::ParameterInformation {
13139 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13140 documentation: None,
13141 },
13142 lsp::ParameterInformation {
13143 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13144 documentation: None,
13145 },
13146 lsp::ParameterInformation {
13147 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
13148 documentation: None,
13149 },
13150 ]),
13151 active_parameter: None,
13152 },
13153 ],
13154 active_signature: Some(1),
13155 active_parameter: Some(0),
13156 };
13157 handle_signature_help_request(&mut cx, mocked_response).await;
13158
13159 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13160 .await;
13161
13162 // Verify we have multiple signatures and the right one is selected
13163 cx.editor(|editor, _, _| {
13164 let popover = editor.signature_help_state.popover().cloned().unwrap();
13165 assert_eq!(popover.signatures.len(), 3);
13166 // active_signature was 1, so that should be the current
13167 assert_eq!(popover.current_signature, 1);
13168 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
13169 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
13170 assert_eq!(
13171 popover.signatures[2].label,
13172 "fn overloaded(x: i32, y: i32, z: i32)"
13173 );
13174 });
13175
13176 // Test navigation functionality
13177 cx.update_editor(|editor, window, cx| {
13178 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13179 });
13180
13181 cx.editor(|editor, _, _| {
13182 let popover = editor.signature_help_state.popover().cloned().unwrap();
13183 assert_eq!(popover.current_signature, 2);
13184 });
13185
13186 // Test wrap around
13187 cx.update_editor(|editor, window, cx| {
13188 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13189 });
13190
13191 cx.editor(|editor, _, _| {
13192 let popover = editor.signature_help_state.popover().cloned().unwrap();
13193 assert_eq!(popover.current_signature, 0);
13194 });
13195
13196 // Test previous navigation
13197 cx.update_editor(|editor, window, cx| {
13198 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
13199 });
13200
13201 cx.editor(|editor, _, _| {
13202 let popover = editor.signature_help_state.popover().cloned().unwrap();
13203 assert_eq!(popover.current_signature, 2);
13204 });
13205}
13206
13207#[gpui::test]
13208async fn test_completion_mode(cx: &mut TestAppContext) {
13209 init_test(cx, |_| {});
13210 let mut cx = EditorLspTestContext::new_rust(
13211 lsp::ServerCapabilities {
13212 completion_provider: Some(lsp::CompletionOptions {
13213 resolve_provider: Some(true),
13214 ..Default::default()
13215 }),
13216 ..Default::default()
13217 },
13218 cx,
13219 )
13220 .await;
13221
13222 struct Run {
13223 run_description: &'static str,
13224 initial_state: String,
13225 buffer_marked_text: String,
13226 completion_label: &'static str,
13227 completion_text: &'static str,
13228 expected_with_insert_mode: String,
13229 expected_with_replace_mode: String,
13230 expected_with_replace_subsequence_mode: String,
13231 expected_with_replace_suffix_mode: String,
13232 }
13233
13234 let runs = [
13235 Run {
13236 run_description: "Start of word matches completion text",
13237 initial_state: "before ediˇ after".into(),
13238 buffer_marked_text: "before <edi|> after".into(),
13239 completion_label: "editor",
13240 completion_text: "editor",
13241 expected_with_insert_mode: "before editorˇ after".into(),
13242 expected_with_replace_mode: "before editorˇ after".into(),
13243 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13244 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13245 },
13246 Run {
13247 run_description: "Accept same text at the middle of the word",
13248 initial_state: "before ediˇtor after".into(),
13249 buffer_marked_text: "before <edi|tor> after".into(),
13250 completion_label: "editor",
13251 completion_text: "editor",
13252 expected_with_insert_mode: "before editorˇtor after".into(),
13253 expected_with_replace_mode: "before editorˇ after".into(),
13254 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13255 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13256 },
13257 Run {
13258 run_description: "End of word matches completion text -- cursor at end",
13259 initial_state: "before torˇ after".into(),
13260 buffer_marked_text: "before <tor|> after".into(),
13261 completion_label: "editor",
13262 completion_text: "editor",
13263 expected_with_insert_mode: "before editorˇ after".into(),
13264 expected_with_replace_mode: "before editorˇ after".into(),
13265 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13266 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13267 },
13268 Run {
13269 run_description: "End of word matches completion text -- cursor at start",
13270 initial_state: "before ˇtor after".into(),
13271 buffer_marked_text: "before <|tor> after".into(),
13272 completion_label: "editor",
13273 completion_text: "editor",
13274 expected_with_insert_mode: "before editorˇtor after".into(),
13275 expected_with_replace_mode: "before editorˇ after".into(),
13276 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13277 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13278 },
13279 Run {
13280 run_description: "Prepend text containing whitespace",
13281 initial_state: "pˇfield: bool".into(),
13282 buffer_marked_text: "<p|field>: bool".into(),
13283 completion_label: "pub ",
13284 completion_text: "pub ",
13285 expected_with_insert_mode: "pub ˇfield: bool".into(),
13286 expected_with_replace_mode: "pub ˇ: bool".into(),
13287 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
13288 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
13289 },
13290 Run {
13291 run_description: "Add element to start of list",
13292 initial_state: "[element_ˇelement_2]".into(),
13293 buffer_marked_text: "[<element_|element_2>]".into(),
13294 completion_label: "element_1",
13295 completion_text: "element_1",
13296 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
13297 expected_with_replace_mode: "[element_1ˇ]".into(),
13298 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
13299 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
13300 },
13301 Run {
13302 run_description: "Add element to start of list -- first and second elements are equal",
13303 initial_state: "[elˇelement]".into(),
13304 buffer_marked_text: "[<el|element>]".into(),
13305 completion_label: "element",
13306 completion_text: "element",
13307 expected_with_insert_mode: "[elementˇelement]".into(),
13308 expected_with_replace_mode: "[elementˇ]".into(),
13309 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
13310 expected_with_replace_suffix_mode: "[elementˇ]".into(),
13311 },
13312 Run {
13313 run_description: "Ends with matching suffix",
13314 initial_state: "SubˇError".into(),
13315 buffer_marked_text: "<Sub|Error>".into(),
13316 completion_label: "SubscriptionError",
13317 completion_text: "SubscriptionError",
13318 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
13319 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13320 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13321 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
13322 },
13323 Run {
13324 run_description: "Suffix is a subsequence -- contiguous",
13325 initial_state: "SubˇErr".into(),
13326 buffer_marked_text: "<Sub|Err>".into(),
13327 completion_label: "SubscriptionError",
13328 completion_text: "SubscriptionError",
13329 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
13330 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13331 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13332 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
13333 },
13334 Run {
13335 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
13336 initial_state: "Suˇscrirr".into(),
13337 buffer_marked_text: "<Su|scrirr>".into(),
13338 completion_label: "SubscriptionError",
13339 completion_text: "SubscriptionError",
13340 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
13341 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13342 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13343 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
13344 },
13345 Run {
13346 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
13347 initial_state: "foo(indˇix)".into(),
13348 buffer_marked_text: "foo(<ind|ix>)".into(),
13349 completion_label: "node_index",
13350 completion_text: "node_index",
13351 expected_with_insert_mode: "foo(node_indexˇix)".into(),
13352 expected_with_replace_mode: "foo(node_indexˇ)".into(),
13353 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
13354 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
13355 },
13356 Run {
13357 run_description: "Replace range ends before cursor - should extend to cursor",
13358 initial_state: "before editˇo after".into(),
13359 buffer_marked_text: "before <{ed}>it|o after".into(),
13360 completion_label: "editor",
13361 completion_text: "editor",
13362 expected_with_insert_mode: "before editorˇo after".into(),
13363 expected_with_replace_mode: "before editorˇo after".into(),
13364 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
13365 expected_with_replace_suffix_mode: "before editorˇo after".into(),
13366 },
13367 Run {
13368 run_description: "Uses label for suffix matching",
13369 initial_state: "before ediˇtor after".into(),
13370 buffer_marked_text: "before <edi|tor> after".into(),
13371 completion_label: "editor",
13372 completion_text: "editor()",
13373 expected_with_insert_mode: "before editor()ˇtor after".into(),
13374 expected_with_replace_mode: "before editor()ˇ after".into(),
13375 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
13376 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
13377 },
13378 Run {
13379 run_description: "Case insensitive subsequence and suffix matching",
13380 initial_state: "before EDiˇtoR after".into(),
13381 buffer_marked_text: "before <EDi|toR> after".into(),
13382 completion_label: "editor",
13383 completion_text: "editor",
13384 expected_with_insert_mode: "before editorˇtoR after".into(),
13385 expected_with_replace_mode: "before editorˇ after".into(),
13386 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13387 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13388 },
13389 ];
13390
13391 for run in runs {
13392 let run_variations = [
13393 (LspInsertMode::Insert, run.expected_with_insert_mode),
13394 (LspInsertMode::Replace, run.expected_with_replace_mode),
13395 (
13396 LspInsertMode::ReplaceSubsequence,
13397 run.expected_with_replace_subsequence_mode,
13398 ),
13399 (
13400 LspInsertMode::ReplaceSuffix,
13401 run.expected_with_replace_suffix_mode,
13402 ),
13403 ];
13404
13405 for (lsp_insert_mode, expected_text) in run_variations {
13406 eprintln!(
13407 "run = {:?}, mode = {lsp_insert_mode:.?}",
13408 run.run_description,
13409 );
13410
13411 update_test_language_settings(&mut cx, |settings| {
13412 settings.defaults.completions = Some(CompletionSettingsContent {
13413 lsp_insert_mode: Some(lsp_insert_mode),
13414 words: Some(WordsCompletionMode::Disabled),
13415 words_min_length: Some(0),
13416 ..Default::default()
13417 });
13418 });
13419
13420 cx.set_state(&run.initial_state);
13421 cx.update_editor(|editor, window, cx| {
13422 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13423 });
13424
13425 let counter = Arc::new(AtomicUsize::new(0));
13426 handle_completion_request_with_insert_and_replace(
13427 &mut cx,
13428 &run.buffer_marked_text,
13429 vec![(run.completion_label, run.completion_text)],
13430 counter.clone(),
13431 )
13432 .await;
13433 cx.condition(|editor, _| editor.context_menu_visible())
13434 .await;
13435 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13436
13437 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13438 editor
13439 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13440 .unwrap()
13441 });
13442 cx.assert_editor_state(&expected_text);
13443 handle_resolve_completion_request(&mut cx, None).await;
13444 apply_additional_edits.await.unwrap();
13445 }
13446 }
13447}
13448
13449#[gpui::test]
13450async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
13451 init_test(cx, |_| {});
13452 let mut cx = EditorLspTestContext::new_rust(
13453 lsp::ServerCapabilities {
13454 completion_provider: Some(lsp::CompletionOptions {
13455 resolve_provider: Some(true),
13456 ..Default::default()
13457 }),
13458 ..Default::default()
13459 },
13460 cx,
13461 )
13462 .await;
13463
13464 let initial_state = "SubˇError";
13465 let buffer_marked_text = "<Sub|Error>";
13466 let completion_text = "SubscriptionError";
13467 let expected_with_insert_mode = "SubscriptionErrorˇError";
13468 let expected_with_replace_mode = "SubscriptionErrorˇ";
13469
13470 update_test_language_settings(&mut cx, |settings| {
13471 settings.defaults.completions = Some(CompletionSettingsContent {
13472 words: Some(WordsCompletionMode::Disabled),
13473 words_min_length: Some(0),
13474 // set the opposite here to ensure that the action is overriding the default behavior
13475 lsp_insert_mode: Some(LspInsertMode::Insert),
13476 ..Default::default()
13477 });
13478 });
13479
13480 cx.set_state(initial_state);
13481 cx.update_editor(|editor, window, cx| {
13482 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13483 });
13484
13485 let counter = Arc::new(AtomicUsize::new(0));
13486 handle_completion_request_with_insert_and_replace(
13487 &mut cx,
13488 buffer_marked_text,
13489 vec![(completion_text, completion_text)],
13490 counter.clone(),
13491 )
13492 .await;
13493 cx.condition(|editor, _| editor.context_menu_visible())
13494 .await;
13495 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13496
13497 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13498 editor
13499 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13500 .unwrap()
13501 });
13502 cx.assert_editor_state(expected_with_replace_mode);
13503 handle_resolve_completion_request(&mut cx, None).await;
13504 apply_additional_edits.await.unwrap();
13505
13506 update_test_language_settings(&mut cx, |settings| {
13507 settings.defaults.completions = Some(CompletionSettingsContent {
13508 words: Some(WordsCompletionMode::Disabled),
13509 words_min_length: Some(0),
13510 // set the opposite here to ensure that the action is overriding the default behavior
13511 lsp_insert_mode: Some(LspInsertMode::Replace),
13512 ..Default::default()
13513 });
13514 });
13515
13516 cx.set_state(initial_state);
13517 cx.update_editor(|editor, window, cx| {
13518 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13519 });
13520 handle_completion_request_with_insert_and_replace(
13521 &mut cx,
13522 buffer_marked_text,
13523 vec![(completion_text, completion_text)],
13524 counter.clone(),
13525 )
13526 .await;
13527 cx.condition(|editor, _| editor.context_menu_visible())
13528 .await;
13529 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
13530
13531 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13532 editor
13533 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
13534 .unwrap()
13535 });
13536 cx.assert_editor_state(expected_with_insert_mode);
13537 handle_resolve_completion_request(&mut cx, None).await;
13538 apply_additional_edits.await.unwrap();
13539}
13540
13541#[gpui::test]
13542async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
13543 init_test(cx, |_| {});
13544 let mut cx = EditorLspTestContext::new_rust(
13545 lsp::ServerCapabilities {
13546 completion_provider: Some(lsp::CompletionOptions {
13547 resolve_provider: Some(true),
13548 ..Default::default()
13549 }),
13550 ..Default::default()
13551 },
13552 cx,
13553 )
13554 .await;
13555
13556 // scenario: surrounding text matches completion text
13557 let completion_text = "to_offset";
13558 let initial_state = indoc! {"
13559 1. buf.to_offˇsuffix
13560 2. buf.to_offˇsuf
13561 3. buf.to_offˇfix
13562 4. buf.to_offˇ
13563 5. into_offˇensive
13564 6. ˇsuffix
13565 7. let ˇ //
13566 8. aaˇzz
13567 9. buf.to_off«zzzzzˇ»suffix
13568 10. buf.«ˇzzzzz»suffix
13569 11. to_off«ˇzzzzz»
13570
13571 buf.to_offˇsuffix // newest cursor
13572 "};
13573 let completion_marked_buffer = indoc! {"
13574 1. buf.to_offsuffix
13575 2. buf.to_offsuf
13576 3. buf.to_offfix
13577 4. buf.to_off
13578 5. into_offensive
13579 6. suffix
13580 7. let //
13581 8. aazz
13582 9. buf.to_offzzzzzsuffix
13583 10. buf.zzzzzsuffix
13584 11. to_offzzzzz
13585
13586 buf.<to_off|suffix> // newest cursor
13587 "};
13588 let expected = indoc! {"
13589 1. buf.to_offsetˇ
13590 2. buf.to_offsetˇsuf
13591 3. buf.to_offsetˇfix
13592 4. buf.to_offsetˇ
13593 5. into_offsetˇensive
13594 6. to_offsetˇsuffix
13595 7. let to_offsetˇ //
13596 8. aato_offsetˇzz
13597 9. buf.to_offsetˇ
13598 10. buf.to_offsetˇsuffix
13599 11. to_offsetˇ
13600
13601 buf.to_offsetˇ // newest cursor
13602 "};
13603 cx.set_state(initial_state);
13604 cx.update_editor(|editor, window, cx| {
13605 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13606 });
13607 handle_completion_request_with_insert_and_replace(
13608 &mut cx,
13609 completion_marked_buffer,
13610 vec![(completion_text, completion_text)],
13611 Arc::new(AtomicUsize::new(0)),
13612 )
13613 .await;
13614 cx.condition(|editor, _| editor.context_menu_visible())
13615 .await;
13616 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13617 editor
13618 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13619 .unwrap()
13620 });
13621 cx.assert_editor_state(expected);
13622 handle_resolve_completion_request(&mut cx, None).await;
13623 apply_additional_edits.await.unwrap();
13624
13625 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
13626 let completion_text = "foo_and_bar";
13627 let initial_state = indoc! {"
13628 1. ooanbˇ
13629 2. zooanbˇ
13630 3. ooanbˇz
13631 4. zooanbˇz
13632 5. ooanˇ
13633 6. oanbˇ
13634
13635 ooanbˇ
13636 "};
13637 let completion_marked_buffer = indoc! {"
13638 1. ooanb
13639 2. zooanb
13640 3. ooanbz
13641 4. zooanbz
13642 5. ooan
13643 6. oanb
13644
13645 <ooanb|>
13646 "};
13647 let expected = indoc! {"
13648 1. foo_and_barˇ
13649 2. zfoo_and_barˇ
13650 3. foo_and_barˇz
13651 4. zfoo_and_barˇz
13652 5. ooanfoo_and_barˇ
13653 6. oanbfoo_and_barˇ
13654
13655 foo_and_barˇ
13656 "};
13657 cx.set_state(initial_state);
13658 cx.update_editor(|editor, window, cx| {
13659 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13660 });
13661 handle_completion_request_with_insert_and_replace(
13662 &mut cx,
13663 completion_marked_buffer,
13664 vec![(completion_text, completion_text)],
13665 Arc::new(AtomicUsize::new(0)),
13666 )
13667 .await;
13668 cx.condition(|editor, _| editor.context_menu_visible())
13669 .await;
13670 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13671 editor
13672 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13673 .unwrap()
13674 });
13675 cx.assert_editor_state(expected);
13676 handle_resolve_completion_request(&mut cx, None).await;
13677 apply_additional_edits.await.unwrap();
13678
13679 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
13680 // (expects the same as if it was inserted at the end)
13681 let completion_text = "foo_and_bar";
13682 let initial_state = indoc! {"
13683 1. ooˇanb
13684 2. zooˇanb
13685 3. ooˇanbz
13686 4. zooˇanbz
13687
13688 ooˇanb
13689 "};
13690 let completion_marked_buffer = indoc! {"
13691 1. ooanb
13692 2. zooanb
13693 3. ooanbz
13694 4. zooanbz
13695
13696 <oo|anb>
13697 "};
13698 let expected = indoc! {"
13699 1. foo_and_barˇ
13700 2. zfoo_and_barˇ
13701 3. foo_and_barˇz
13702 4. zfoo_and_barˇz
13703
13704 foo_and_barˇ
13705 "};
13706 cx.set_state(initial_state);
13707 cx.update_editor(|editor, window, cx| {
13708 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13709 });
13710 handle_completion_request_with_insert_and_replace(
13711 &mut cx,
13712 completion_marked_buffer,
13713 vec![(completion_text, completion_text)],
13714 Arc::new(AtomicUsize::new(0)),
13715 )
13716 .await;
13717 cx.condition(|editor, _| editor.context_menu_visible())
13718 .await;
13719 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13720 editor
13721 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13722 .unwrap()
13723 });
13724 cx.assert_editor_state(expected);
13725 handle_resolve_completion_request(&mut cx, None).await;
13726 apply_additional_edits.await.unwrap();
13727}
13728
13729// This used to crash
13730#[gpui::test]
13731async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
13732 init_test(cx, |_| {});
13733
13734 let buffer_text = indoc! {"
13735 fn main() {
13736 10.satu;
13737
13738 //
13739 // separate cursors so they open in different excerpts (manually reproducible)
13740 //
13741
13742 10.satu20;
13743 }
13744 "};
13745 let multibuffer_text_with_selections = indoc! {"
13746 fn main() {
13747 10.satuˇ;
13748
13749 //
13750
13751 //
13752
13753 10.satuˇ20;
13754 }
13755 "};
13756 let expected_multibuffer = indoc! {"
13757 fn main() {
13758 10.saturating_sub()ˇ;
13759
13760 //
13761
13762 //
13763
13764 10.saturating_sub()ˇ;
13765 }
13766 "};
13767
13768 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
13769 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
13770
13771 let fs = FakeFs::new(cx.executor());
13772 fs.insert_tree(
13773 path!("/a"),
13774 json!({
13775 "main.rs": buffer_text,
13776 }),
13777 )
13778 .await;
13779
13780 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13781 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13782 language_registry.add(rust_lang());
13783 let mut fake_servers = language_registry.register_fake_lsp(
13784 "Rust",
13785 FakeLspAdapter {
13786 capabilities: lsp::ServerCapabilities {
13787 completion_provider: Some(lsp::CompletionOptions {
13788 resolve_provider: None,
13789 ..lsp::CompletionOptions::default()
13790 }),
13791 ..lsp::ServerCapabilities::default()
13792 },
13793 ..FakeLspAdapter::default()
13794 },
13795 );
13796 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13797 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13798 let buffer = project
13799 .update(cx, |project, cx| {
13800 project.open_local_buffer(path!("/a/main.rs"), cx)
13801 })
13802 .await
13803 .unwrap();
13804
13805 let multi_buffer = cx.new(|cx| {
13806 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
13807 multi_buffer.push_excerpts(
13808 buffer.clone(),
13809 [ExcerptRange::new(0..first_excerpt_end)],
13810 cx,
13811 );
13812 multi_buffer.push_excerpts(
13813 buffer.clone(),
13814 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
13815 cx,
13816 );
13817 multi_buffer
13818 });
13819
13820 let editor = workspace
13821 .update(cx, |_, window, cx| {
13822 cx.new(|cx| {
13823 Editor::new(
13824 EditorMode::Full {
13825 scale_ui_elements_with_buffer_font_size: false,
13826 show_active_line_background: false,
13827 sized_by_content: false,
13828 },
13829 multi_buffer.clone(),
13830 Some(project.clone()),
13831 window,
13832 cx,
13833 )
13834 })
13835 })
13836 .unwrap();
13837
13838 let pane = workspace
13839 .update(cx, |workspace, _, _| workspace.active_pane().clone())
13840 .unwrap();
13841 pane.update_in(cx, |pane, window, cx| {
13842 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
13843 });
13844
13845 let fake_server = fake_servers.next().await.unwrap();
13846
13847 editor.update_in(cx, |editor, window, cx| {
13848 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13849 s.select_ranges([
13850 Point::new(1, 11)..Point::new(1, 11),
13851 Point::new(7, 11)..Point::new(7, 11),
13852 ])
13853 });
13854
13855 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
13856 });
13857
13858 editor.update_in(cx, |editor, window, cx| {
13859 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13860 });
13861
13862 fake_server
13863 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13864 let completion_item = lsp::CompletionItem {
13865 label: "saturating_sub()".into(),
13866 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
13867 lsp::InsertReplaceEdit {
13868 new_text: "saturating_sub()".to_owned(),
13869 insert: lsp::Range::new(
13870 lsp::Position::new(7, 7),
13871 lsp::Position::new(7, 11),
13872 ),
13873 replace: lsp::Range::new(
13874 lsp::Position::new(7, 7),
13875 lsp::Position::new(7, 13),
13876 ),
13877 },
13878 )),
13879 ..lsp::CompletionItem::default()
13880 };
13881
13882 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
13883 })
13884 .next()
13885 .await
13886 .unwrap();
13887
13888 cx.condition(&editor, |editor, _| editor.context_menu_visible())
13889 .await;
13890
13891 editor
13892 .update_in(cx, |editor, window, cx| {
13893 editor
13894 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13895 .unwrap()
13896 })
13897 .await
13898 .unwrap();
13899
13900 editor.update(cx, |editor, cx| {
13901 assert_text_with_selections(editor, expected_multibuffer, cx);
13902 })
13903}
13904
13905#[gpui::test]
13906async fn test_completion(cx: &mut TestAppContext) {
13907 init_test(cx, |_| {});
13908
13909 let mut cx = EditorLspTestContext::new_rust(
13910 lsp::ServerCapabilities {
13911 completion_provider: Some(lsp::CompletionOptions {
13912 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
13913 resolve_provider: Some(true),
13914 ..Default::default()
13915 }),
13916 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13917 ..Default::default()
13918 },
13919 cx,
13920 )
13921 .await;
13922 let counter = Arc::new(AtomicUsize::new(0));
13923
13924 cx.set_state(indoc! {"
13925 oneˇ
13926 two
13927 three
13928 "});
13929 cx.simulate_keystroke(".");
13930 handle_completion_request(
13931 indoc! {"
13932 one.|<>
13933 two
13934 three
13935 "},
13936 vec!["first_completion", "second_completion"],
13937 true,
13938 counter.clone(),
13939 &mut cx,
13940 )
13941 .await;
13942 cx.condition(|editor, _| editor.context_menu_visible())
13943 .await;
13944 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13945
13946 let _handler = handle_signature_help_request(
13947 &mut cx,
13948 lsp::SignatureHelp {
13949 signatures: vec![lsp::SignatureInformation {
13950 label: "test signature".to_string(),
13951 documentation: None,
13952 parameters: Some(vec![lsp::ParameterInformation {
13953 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
13954 documentation: None,
13955 }]),
13956 active_parameter: None,
13957 }],
13958 active_signature: None,
13959 active_parameter: None,
13960 },
13961 );
13962 cx.update_editor(|editor, window, cx| {
13963 assert!(
13964 !editor.signature_help_state.is_shown(),
13965 "No signature help was called for"
13966 );
13967 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13968 });
13969 cx.run_until_parked();
13970 cx.update_editor(|editor, _, _| {
13971 assert!(
13972 !editor.signature_help_state.is_shown(),
13973 "No signature help should be shown when completions menu is open"
13974 );
13975 });
13976
13977 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13978 editor.context_menu_next(&Default::default(), window, cx);
13979 editor
13980 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13981 .unwrap()
13982 });
13983 cx.assert_editor_state(indoc! {"
13984 one.second_completionˇ
13985 two
13986 three
13987 "});
13988
13989 handle_resolve_completion_request(
13990 &mut cx,
13991 Some(vec![
13992 (
13993 //This overlaps with the primary completion edit which is
13994 //misbehavior from the LSP spec, test that we filter it out
13995 indoc! {"
13996 one.second_ˇcompletion
13997 two
13998 threeˇ
13999 "},
14000 "overlapping additional edit",
14001 ),
14002 (
14003 indoc! {"
14004 one.second_completion
14005 two
14006 threeˇ
14007 "},
14008 "\nadditional edit",
14009 ),
14010 ]),
14011 )
14012 .await;
14013 apply_additional_edits.await.unwrap();
14014 cx.assert_editor_state(indoc! {"
14015 one.second_completionˇ
14016 two
14017 three
14018 additional edit
14019 "});
14020
14021 cx.set_state(indoc! {"
14022 one.second_completion
14023 twoˇ
14024 threeˇ
14025 additional edit
14026 "});
14027 cx.simulate_keystroke(" ");
14028 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14029 cx.simulate_keystroke("s");
14030 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14031
14032 cx.assert_editor_state(indoc! {"
14033 one.second_completion
14034 two sˇ
14035 three sˇ
14036 additional edit
14037 "});
14038 handle_completion_request(
14039 indoc! {"
14040 one.second_completion
14041 two s
14042 three <s|>
14043 additional edit
14044 "},
14045 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14046 true,
14047 counter.clone(),
14048 &mut cx,
14049 )
14050 .await;
14051 cx.condition(|editor, _| editor.context_menu_visible())
14052 .await;
14053 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14054
14055 cx.simulate_keystroke("i");
14056
14057 handle_completion_request(
14058 indoc! {"
14059 one.second_completion
14060 two si
14061 three <si|>
14062 additional edit
14063 "},
14064 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14065 true,
14066 counter.clone(),
14067 &mut cx,
14068 )
14069 .await;
14070 cx.condition(|editor, _| editor.context_menu_visible())
14071 .await;
14072 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14073
14074 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14075 editor
14076 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14077 .unwrap()
14078 });
14079 cx.assert_editor_state(indoc! {"
14080 one.second_completion
14081 two sixth_completionˇ
14082 three sixth_completionˇ
14083 additional edit
14084 "});
14085
14086 apply_additional_edits.await.unwrap();
14087
14088 update_test_language_settings(&mut cx, |settings| {
14089 settings.defaults.show_completions_on_input = Some(false);
14090 });
14091 cx.set_state("editorˇ");
14092 cx.simulate_keystroke(".");
14093 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14094 cx.simulate_keystrokes("c l o");
14095 cx.assert_editor_state("editor.cloˇ");
14096 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14097 cx.update_editor(|editor, window, cx| {
14098 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14099 });
14100 handle_completion_request(
14101 "editor.<clo|>",
14102 vec!["close", "clobber"],
14103 true,
14104 counter.clone(),
14105 &mut cx,
14106 )
14107 .await;
14108 cx.condition(|editor, _| editor.context_menu_visible())
14109 .await;
14110 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
14111
14112 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14113 editor
14114 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14115 .unwrap()
14116 });
14117 cx.assert_editor_state("editor.clobberˇ");
14118 handle_resolve_completion_request(&mut cx, None).await;
14119 apply_additional_edits.await.unwrap();
14120}
14121
14122#[gpui::test]
14123async fn test_completion_reuse(cx: &mut TestAppContext) {
14124 init_test(cx, |_| {});
14125
14126 let mut cx = EditorLspTestContext::new_rust(
14127 lsp::ServerCapabilities {
14128 completion_provider: Some(lsp::CompletionOptions {
14129 trigger_characters: Some(vec![".".to_string()]),
14130 ..Default::default()
14131 }),
14132 ..Default::default()
14133 },
14134 cx,
14135 )
14136 .await;
14137
14138 let counter = Arc::new(AtomicUsize::new(0));
14139 cx.set_state("objˇ");
14140 cx.simulate_keystroke(".");
14141
14142 // Initial completion request returns complete results
14143 let is_incomplete = false;
14144 handle_completion_request(
14145 "obj.|<>",
14146 vec!["a", "ab", "abc"],
14147 is_incomplete,
14148 counter.clone(),
14149 &mut cx,
14150 )
14151 .await;
14152 cx.run_until_parked();
14153 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14154 cx.assert_editor_state("obj.ˇ");
14155 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14156
14157 // Type "a" - filters existing completions
14158 cx.simulate_keystroke("a");
14159 cx.run_until_parked();
14160 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14161 cx.assert_editor_state("obj.aˇ");
14162 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14163
14164 // Type "b" - filters existing completions
14165 cx.simulate_keystroke("b");
14166 cx.run_until_parked();
14167 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14168 cx.assert_editor_state("obj.abˇ");
14169 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14170
14171 // Type "c" - filters existing completions
14172 cx.simulate_keystroke("c");
14173 cx.run_until_parked();
14174 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14175 cx.assert_editor_state("obj.abcˇ");
14176 check_displayed_completions(vec!["abc"], &mut cx);
14177
14178 // Backspace to delete "c" - filters existing completions
14179 cx.update_editor(|editor, window, cx| {
14180 editor.backspace(&Backspace, window, cx);
14181 });
14182 cx.run_until_parked();
14183 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14184 cx.assert_editor_state("obj.abˇ");
14185 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14186
14187 // Moving cursor to the left dismisses menu.
14188 cx.update_editor(|editor, window, cx| {
14189 editor.move_left(&MoveLeft, window, cx);
14190 });
14191 cx.run_until_parked();
14192 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14193 cx.assert_editor_state("obj.aˇb");
14194 cx.update_editor(|editor, _, _| {
14195 assert_eq!(editor.context_menu_visible(), false);
14196 });
14197
14198 // Type "b" - new request
14199 cx.simulate_keystroke("b");
14200 let is_incomplete = false;
14201 handle_completion_request(
14202 "obj.<ab|>a",
14203 vec!["ab", "abc"],
14204 is_incomplete,
14205 counter.clone(),
14206 &mut cx,
14207 )
14208 .await;
14209 cx.run_until_parked();
14210 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14211 cx.assert_editor_state("obj.abˇb");
14212 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14213
14214 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
14215 cx.update_editor(|editor, window, cx| {
14216 editor.backspace(&Backspace, window, cx);
14217 });
14218 let is_incomplete = false;
14219 handle_completion_request(
14220 "obj.<a|>b",
14221 vec!["a", "ab", "abc"],
14222 is_incomplete,
14223 counter.clone(),
14224 &mut cx,
14225 )
14226 .await;
14227 cx.run_until_parked();
14228 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14229 cx.assert_editor_state("obj.aˇb");
14230 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14231
14232 // Backspace to delete "a" - dismisses menu.
14233 cx.update_editor(|editor, window, cx| {
14234 editor.backspace(&Backspace, window, cx);
14235 });
14236 cx.run_until_parked();
14237 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14238 cx.assert_editor_state("obj.ˇb");
14239 cx.update_editor(|editor, _, _| {
14240 assert_eq!(editor.context_menu_visible(), false);
14241 });
14242}
14243
14244#[gpui::test]
14245async fn test_word_completion(cx: &mut TestAppContext) {
14246 let lsp_fetch_timeout_ms = 10;
14247 init_test(cx, |language_settings| {
14248 language_settings.defaults.completions = Some(CompletionSettingsContent {
14249 words_min_length: Some(0),
14250 lsp_fetch_timeout_ms: Some(10),
14251 lsp_insert_mode: Some(LspInsertMode::Insert),
14252 ..Default::default()
14253 });
14254 });
14255
14256 let mut cx = EditorLspTestContext::new_rust(
14257 lsp::ServerCapabilities {
14258 completion_provider: Some(lsp::CompletionOptions {
14259 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14260 ..lsp::CompletionOptions::default()
14261 }),
14262 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14263 ..lsp::ServerCapabilities::default()
14264 },
14265 cx,
14266 )
14267 .await;
14268
14269 let throttle_completions = Arc::new(AtomicBool::new(false));
14270
14271 let lsp_throttle_completions = throttle_completions.clone();
14272 let _completion_requests_handler =
14273 cx.lsp
14274 .server
14275 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
14276 let lsp_throttle_completions = lsp_throttle_completions.clone();
14277 let cx = cx.clone();
14278 async move {
14279 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
14280 cx.background_executor()
14281 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
14282 .await;
14283 }
14284 Ok(Some(lsp::CompletionResponse::Array(vec![
14285 lsp::CompletionItem {
14286 label: "first".into(),
14287 ..lsp::CompletionItem::default()
14288 },
14289 lsp::CompletionItem {
14290 label: "last".into(),
14291 ..lsp::CompletionItem::default()
14292 },
14293 ])))
14294 }
14295 });
14296
14297 cx.set_state(indoc! {"
14298 oneˇ
14299 two
14300 three
14301 "});
14302 cx.simulate_keystroke(".");
14303 cx.executor().run_until_parked();
14304 cx.condition(|editor, _| editor.context_menu_visible())
14305 .await;
14306 cx.update_editor(|editor, window, cx| {
14307 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14308 {
14309 assert_eq!(
14310 completion_menu_entries(menu),
14311 &["first", "last"],
14312 "When LSP server is fast to reply, no fallback word completions are used"
14313 );
14314 } else {
14315 panic!("expected completion menu to be open");
14316 }
14317 editor.cancel(&Cancel, window, cx);
14318 });
14319 cx.executor().run_until_parked();
14320 cx.condition(|editor, _| !editor.context_menu_visible())
14321 .await;
14322
14323 throttle_completions.store(true, atomic::Ordering::Release);
14324 cx.simulate_keystroke(".");
14325 cx.executor()
14326 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
14327 cx.executor().run_until_parked();
14328 cx.condition(|editor, _| editor.context_menu_visible())
14329 .await;
14330 cx.update_editor(|editor, _, _| {
14331 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14332 {
14333 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
14334 "When LSP server is slow, document words can be shown instead, if configured accordingly");
14335 } else {
14336 panic!("expected completion menu to be open");
14337 }
14338 });
14339}
14340
14341#[gpui::test]
14342async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
14343 init_test(cx, |language_settings| {
14344 language_settings.defaults.completions = Some(CompletionSettingsContent {
14345 words: Some(WordsCompletionMode::Enabled),
14346 words_min_length: Some(0),
14347 lsp_insert_mode: Some(LspInsertMode::Insert),
14348 ..Default::default()
14349 });
14350 });
14351
14352 let mut cx = EditorLspTestContext::new_rust(
14353 lsp::ServerCapabilities {
14354 completion_provider: Some(lsp::CompletionOptions {
14355 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14356 ..lsp::CompletionOptions::default()
14357 }),
14358 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14359 ..lsp::ServerCapabilities::default()
14360 },
14361 cx,
14362 )
14363 .await;
14364
14365 let _completion_requests_handler =
14366 cx.lsp
14367 .server
14368 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14369 Ok(Some(lsp::CompletionResponse::Array(vec![
14370 lsp::CompletionItem {
14371 label: "first".into(),
14372 ..lsp::CompletionItem::default()
14373 },
14374 lsp::CompletionItem {
14375 label: "last".into(),
14376 ..lsp::CompletionItem::default()
14377 },
14378 ])))
14379 });
14380
14381 cx.set_state(indoc! {"ˇ
14382 first
14383 last
14384 second
14385 "});
14386 cx.simulate_keystroke(".");
14387 cx.executor().run_until_parked();
14388 cx.condition(|editor, _| editor.context_menu_visible())
14389 .await;
14390 cx.update_editor(|editor, _, _| {
14391 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14392 {
14393 assert_eq!(
14394 completion_menu_entries(menu),
14395 &["first", "last", "second"],
14396 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
14397 );
14398 } else {
14399 panic!("expected completion menu to be open");
14400 }
14401 });
14402}
14403
14404#[gpui::test]
14405async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
14406 init_test(cx, |language_settings| {
14407 language_settings.defaults.completions = Some(CompletionSettingsContent {
14408 words: Some(WordsCompletionMode::Disabled),
14409 words_min_length: Some(0),
14410 lsp_insert_mode: Some(LspInsertMode::Insert),
14411 ..Default::default()
14412 });
14413 });
14414
14415 let mut cx = EditorLspTestContext::new_rust(
14416 lsp::ServerCapabilities {
14417 completion_provider: Some(lsp::CompletionOptions {
14418 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14419 ..lsp::CompletionOptions::default()
14420 }),
14421 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14422 ..lsp::ServerCapabilities::default()
14423 },
14424 cx,
14425 )
14426 .await;
14427
14428 let _completion_requests_handler =
14429 cx.lsp
14430 .server
14431 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14432 panic!("LSP completions should not be queried when dealing with word completions")
14433 });
14434
14435 cx.set_state(indoc! {"ˇ
14436 first
14437 last
14438 second
14439 "});
14440 cx.update_editor(|editor, window, cx| {
14441 editor.show_word_completions(&ShowWordCompletions, window, cx);
14442 });
14443 cx.executor().run_until_parked();
14444 cx.condition(|editor, _| editor.context_menu_visible())
14445 .await;
14446 cx.update_editor(|editor, _, _| {
14447 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14448 {
14449 assert_eq!(
14450 completion_menu_entries(menu),
14451 &["first", "last", "second"],
14452 "`ShowWordCompletions` action should show word completions"
14453 );
14454 } else {
14455 panic!("expected completion menu to be open");
14456 }
14457 });
14458
14459 cx.simulate_keystroke("l");
14460 cx.executor().run_until_parked();
14461 cx.condition(|editor, _| editor.context_menu_visible())
14462 .await;
14463 cx.update_editor(|editor, _, _| {
14464 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14465 {
14466 assert_eq!(
14467 completion_menu_entries(menu),
14468 &["last"],
14469 "After showing word completions, further editing should filter them and not query the LSP"
14470 );
14471 } else {
14472 panic!("expected completion menu to be open");
14473 }
14474 });
14475}
14476
14477#[gpui::test]
14478async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
14479 init_test(cx, |language_settings| {
14480 language_settings.defaults.completions = Some(CompletionSettingsContent {
14481 words_min_length: Some(0),
14482 lsp: Some(false),
14483 lsp_insert_mode: Some(LspInsertMode::Insert),
14484 ..Default::default()
14485 });
14486 });
14487
14488 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14489
14490 cx.set_state(indoc! {"ˇ
14491 0_usize
14492 let
14493 33
14494 4.5f32
14495 "});
14496 cx.update_editor(|editor, window, cx| {
14497 editor.show_completions(&ShowCompletions::default(), window, cx);
14498 });
14499 cx.executor().run_until_parked();
14500 cx.condition(|editor, _| editor.context_menu_visible())
14501 .await;
14502 cx.update_editor(|editor, window, cx| {
14503 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14504 {
14505 assert_eq!(
14506 completion_menu_entries(menu),
14507 &["let"],
14508 "With no digits in the completion query, no digits should be in the word completions"
14509 );
14510 } else {
14511 panic!("expected completion menu to be open");
14512 }
14513 editor.cancel(&Cancel, window, cx);
14514 });
14515
14516 cx.set_state(indoc! {"3ˇ
14517 0_usize
14518 let
14519 3
14520 33.35f32
14521 "});
14522 cx.update_editor(|editor, window, cx| {
14523 editor.show_completions(&ShowCompletions::default(), window, cx);
14524 });
14525 cx.executor().run_until_parked();
14526 cx.condition(|editor, _| editor.context_menu_visible())
14527 .await;
14528 cx.update_editor(|editor, _, _| {
14529 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14530 {
14531 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
14532 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
14533 } else {
14534 panic!("expected completion menu to be open");
14535 }
14536 });
14537}
14538
14539#[gpui::test]
14540async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
14541 init_test(cx, |language_settings| {
14542 language_settings.defaults.completions = Some(CompletionSettingsContent {
14543 words: Some(WordsCompletionMode::Enabled),
14544 words_min_length: Some(3),
14545 lsp_insert_mode: Some(LspInsertMode::Insert),
14546 ..Default::default()
14547 });
14548 });
14549
14550 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14551 cx.set_state(indoc! {"ˇ
14552 wow
14553 wowen
14554 wowser
14555 "});
14556 cx.simulate_keystroke("w");
14557 cx.executor().run_until_parked();
14558 cx.update_editor(|editor, _, _| {
14559 if editor.context_menu.borrow_mut().is_some() {
14560 panic!(
14561 "expected completion menu to be hidden, as words completion threshold is not met"
14562 );
14563 }
14564 });
14565
14566 cx.update_editor(|editor, window, cx| {
14567 editor.show_word_completions(&ShowWordCompletions, window, cx);
14568 });
14569 cx.executor().run_until_parked();
14570 cx.update_editor(|editor, window, cx| {
14571 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14572 {
14573 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");
14574 } else {
14575 panic!("expected completion menu to be open after the word completions are called with an action");
14576 }
14577
14578 editor.cancel(&Cancel, window, cx);
14579 });
14580 cx.update_editor(|editor, _, _| {
14581 if editor.context_menu.borrow_mut().is_some() {
14582 panic!("expected completion menu to be hidden after canceling");
14583 }
14584 });
14585
14586 cx.simulate_keystroke("o");
14587 cx.executor().run_until_parked();
14588 cx.update_editor(|editor, _, _| {
14589 if editor.context_menu.borrow_mut().is_some() {
14590 panic!(
14591 "expected completion menu to be hidden, as words completion threshold is not met still"
14592 );
14593 }
14594 });
14595
14596 cx.simulate_keystroke("w");
14597 cx.executor().run_until_parked();
14598 cx.update_editor(|editor, _, _| {
14599 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14600 {
14601 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
14602 } else {
14603 panic!("expected completion menu to be open after the word completions threshold is met");
14604 }
14605 });
14606}
14607
14608#[gpui::test]
14609async fn test_word_completions_disabled(cx: &mut TestAppContext) {
14610 init_test(cx, |language_settings| {
14611 language_settings.defaults.completions = Some(CompletionSettingsContent {
14612 words: Some(WordsCompletionMode::Enabled),
14613 words_min_length: Some(0),
14614 lsp_insert_mode: Some(LspInsertMode::Insert),
14615 ..Default::default()
14616 });
14617 });
14618
14619 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14620 cx.update_editor(|editor, _, _| {
14621 editor.disable_word_completions();
14622 });
14623 cx.set_state(indoc! {"ˇ
14624 wow
14625 wowen
14626 wowser
14627 "});
14628 cx.simulate_keystroke("w");
14629 cx.executor().run_until_parked();
14630 cx.update_editor(|editor, _, _| {
14631 if editor.context_menu.borrow_mut().is_some() {
14632 panic!(
14633 "expected completion menu to be hidden, as words completion are disabled for this editor"
14634 );
14635 }
14636 });
14637
14638 cx.update_editor(|editor, window, cx| {
14639 editor.show_word_completions(&ShowWordCompletions, window, cx);
14640 });
14641 cx.executor().run_until_parked();
14642 cx.update_editor(|editor, _, _| {
14643 if editor.context_menu.borrow_mut().is_some() {
14644 panic!(
14645 "expected completion menu to be hidden even if called for explicitly, as words completion are disabled for this editor"
14646 );
14647 }
14648 });
14649}
14650
14651fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
14652 let position = || lsp::Position {
14653 line: params.text_document_position.position.line,
14654 character: params.text_document_position.position.character,
14655 };
14656 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14657 range: lsp::Range {
14658 start: position(),
14659 end: position(),
14660 },
14661 new_text: text.to_string(),
14662 }))
14663}
14664
14665#[gpui::test]
14666async fn test_multiline_completion(cx: &mut TestAppContext) {
14667 init_test(cx, |_| {});
14668
14669 let fs = FakeFs::new(cx.executor());
14670 fs.insert_tree(
14671 path!("/a"),
14672 json!({
14673 "main.ts": "a",
14674 }),
14675 )
14676 .await;
14677
14678 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14679 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14680 let typescript_language = Arc::new(Language::new(
14681 LanguageConfig {
14682 name: "TypeScript".into(),
14683 matcher: LanguageMatcher {
14684 path_suffixes: vec!["ts".to_string()],
14685 ..LanguageMatcher::default()
14686 },
14687 line_comments: vec!["// ".into()],
14688 ..LanguageConfig::default()
14689 },
14690 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
14691 ));
14692 language_registry.add(typescript_language.clone());
14693 let mut fake_servers = language_registry.register_fake_lsp(
14694 "TypeScript",
14695 FakeLspAdapter {
14696 capabilities: lsp::ServerCapabilities {
14697 completion_provider: Some(lsp::CompletionOptions {
14698 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14699 ..lsp::CompletionOptions::default()
14700 }),
14701 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14702 ..lsp::ServerCapabilities::default()
14703 },
14704 // Emulate vtsls label generation
14705 label_for_completion: Some(Box::new(|item, _| {
14706 let text = if let Some(description) = item
14707 .label_details
14708 .as_ref()
14709 .and_then(|label_details| label_details.description.as_ref())
14710 {
14711 format!("{} {}", item.label, description)
14712 } else if let Some(detail) = &item.detail {
14713 format!("{} {}", item.label, detail)
14714 } else {
14715 item.label.clone()
14716 };
14717 let len = text.len();
14718 Some(language::CodeLabel {
14719 text,
14720 runs: Vec::new(),
14721 filter_range: 0..len,
14722 })
14723 })),
14724 ..FakeLspAdapter::default()
14725 },
14726 );
14727 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14728 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14729 let worktree_id = workspace
14730 .update(cx, |workspace, _window, cx| {
14731 workspace.project().update(cx, |project, cx| {
14732 project.worktrees(cx).next().unwrap().read(cx).id()
14733 })
14734 })
14735 .unwrap();
14736 let _buffer = project
14737 .update(cx, |project, cx| {
14738 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
14739 })
14740 .await
14741 .unwrap();
14742 let editor = workspace
14743 .update(cx, |workspace, window, cx| {
14744 workspace.open_path((worktree_id, rel_path("main.ts")), None, true, window, cx)
14745 })
14746 .unwrap()
14747 .await
14748 .unwrap()
14749 .downcast::<Editor>()
14750 .unwrap();
14751 let fake_server = fake_servers.next().await.unwrap();
14752
14753 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
14754 let multiline_label_2 = "a\nb\nc\n";
14755 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
14756 let multiline_description = "d\ne\nf\n";
14757 let multiline_detail_2 = "g\nh\ni\n";
14758
14759 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
14760 move |params, _| async move {
14761 Ok(Some(lsp::CompletionResponse::Array(vec![
14762 lsp::CompletionItem {
14763 label: multiline_label.to_string(),
14764 text_edit: gen_text_edit(¶ms, "new_text_1"),
14765 ..lsp::CompletionItem::default()
14766 },
14767 lsp::CompletionItem {
14768 label: "single line label 1".to_string(),
14769 detail: Some(multiline_detail.to_string()),
14770 text_edit: gen_text_edit(¶ms, "new_text_2"),
14771 ..lsp::CompletionItem::default()
14772 },
14773 lsp::CompletionItem {
14774 label: "single line label 2".to_string(),
14775 label_details: Some(lsp::CompletionItemLabelDetails {
14776 description: Some(multiline_description.to_string()),
14777 detail: None,
14778 }),
14779 text_edit: gen_text_edit(¶ms, "new_text_2"),
14780 ..lsp::CompletionItem::default()
14781 },
14782 lsp::CompletionItem {
14783 label: multiline_label_2.to_string(),
14784 detail: Some(multiline_detail_2.to_string()),
14785 text_edit: gen_text_edit(¶ms, "new_text_3"),
14786 ..lsp::CompletionItem::default()
14787 },
14788 lsp::CompletionItem {
14789 label: "Label with many spaces and \t but without newlines".to_string(),
14790 detail: Some(
14791 "Details with many spaces and \t but without newlines".to_string(),
14792 ),
14793 text_edit: gen_text_edit(¶ms, "new_text_4"),
14794 ..lsp::CompletionItem::default()
14795 },
14796 ])))
14797 },
14798 );
14799
14800 editor.update_in(cx, |editor, window, cx| {
14801 cx.focus_self(window);
14802 editor.move_to_end(&MoveToEnd, window, cx);
14803 editor.handle_input(".", window, cx);
14804 });
14805 cx.run_until_parked();
14806 completion_handle.next().await.unwrap();
14807
14808 editor.update(cx, |editor, _| {
14809 assert!(editor.context_menu_visible());
14810 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14811 {
14812 let completion_labels = menu
14813 .completions
14814 .borrow()
14815 .iter()
14816 .map(|c| c.label.text.clone())
14817 .collect::<Vec<_>>();
14818 assert_eq!(
14819 completion_labels,
14820 &[
14821 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
14822 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
14823 "single line label 2 d e f ",
14824 "a b c g h i ",
14825 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
14826 ],
14827 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
14828 );
14829
14830 for completion in menu
14831 .completions
14832 .borrow()
14833 .iter() {
14834 assert_eq!(
14835 completion.label.filter_range,
14836 0..completion.label.text.len(),
14837 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
14838 );
14839 }
14840 } else {
14841 panic!("expected completion menu to be open");
14842 }
14843 });
14844}
14845
14846#[gpui::test]
14847async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
14848 init_test(cx, |_| {});
14849 let mut cx = EditorLspTestContext::new_rust(
14850 lsp::ServerCapabilities {
14851 completion_provider: Some(lsp::CompletionOptions {
14852 trigger_characters: Some(vec![".".to_string()]),
14853 ..Default::default()
14854 }),
14855 ..Default::default()
14856 },
14857 cx,
14858 )
14859 .await;
14860 cx.lsp
14861 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14862 Ok(Some(lsp::CompletionResponse::Array(vec![
14863 lsp::CompletionItem {
14864 label: "first".into(),
14865 ..Default::default()
14866 },
14867 lsp::CompletionItem {
14868 label: "last".into(),
14869 ..Default::default()
14870 },
14871 ])))
14872 });
14873 cx.set_state("variableˇ");
14874 cx.simulate_keystroke(".");
14875 cx.executor().run_until_parked();
14876
14877 cx.update_editor(|editor, _, _| {
14878 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14879 {
14880 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
14881 } else {
14882 panic!("expected completion menu to be open");
14883 }
14884 });
14885
14886 cx.update_editor(|editor, window, cx| {
14887 editor.move_page_down(&MovePageDown::default(), window, cx);
14888 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14889 {
14890 assert!(
14891 menu.selected_item == 1,
14892 "expected PageDown to select the last item from the context menu"
14893 );
14894 } else {
14895 panic!("expected completion menu to stay open after PageDown");
14896 }
14897 });
14898
14899 cx.update_editor(|editor, window, cx| {
14900 editor.move_page_up(&MovePageUp::default(), window, cx);
14901 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14902 {
14903 assert!(
14904 menu.selected_item == 0,
14905 "expected PageUp to select the first item from the context menu"
14906 );
14907 } else {
14908 panic!("expected completion menu to stay open after PageUp");
14909 }
14910 });
14911}
14912
14913#[gpui::test]
14914async fn test_as_is_completions(cx: &mut TestAppContext) {
14915 init_test(cx, |_| {});
14916 let mut cx = EditorLspTestContext::new_rust(
14917 lsp::ServerCapabilities {
14918 completion_provider: Some(lsp::CompletionOptions {
14919 ..Default::default()
14920 }),
14921 ..Default::default()
14922 },
14923 cx,
14924 )
14925 .await;
14926 cx.lsp
14927 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14928 Ok(Some(lsp::CompletionResponse::Array(vec![
14929 lsp::CompletionItem {
14930 label: "unsafe".into(),
14931 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14932 range: lsp::Range {
14933 start: lsp::Position {
14934 line: 1,
14935 character: 2,
14936 },
14937 end: lsp::Position {
14938 line: 1,
14939 character: 3,
14940 },
14941 },
14942 new_text: "unsafe".to_string(),
14943 })),
14944 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
14945 ..Default::default()
14946 },
14947 ])))
14948 });
14949 cx.set_state("fn a() {}\n nˇ");
14950 cx.executor().run_until_parked();
14951 cx.update_editor(|editor, window, cx| {
14952 editor.show_completions(
14953 &ShowCompletions {
14954 trigger: Some("\n".into()),
14955 },
14956 window,
14957 cx,
14958 );
14959 });
14960 cx.executor().run_until_parked();
14961
14962 cx.update_editor(|editor, window, cx| {
14963 editor.confirm_completion(&Default::default(), window, cx)
14964 });
14965 cx.executor().run_until_parked();
14966 cx.assert_editor_state("fn a() {}\n unsafeˇ");
14967}
14968
14969#[gpui::test]
14970async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
14971 init_test(cx, |_| {});
14972 let language =
14973 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
14974 let mut cx = EditorLspTestContext::new(
14975 language,
14976 lsp::ServerCapabilities {
14977 completion_provider: Some(lsp::CompletionOptions {
14978 ..lsp::CompletionOptions::default()
14979 }),
14980 ..lsp::ServerCapabilities::default()
14981 },
14982 cx,
14983 )
14984 .await;
14985
14986 cx.set_state(
14987 "#ifndef BAR_H
14988#define BAR_H
14989
14990#include <stdbool.h>
14991
14992int fn_branch(bool do_branch1, bool do_branch2);
14993
14994#endif // BAR_H
14995ˇ",
14996 );
14997 cx.executor().run_until_parked();
14998 cx.update_editor(|editor, window, cx| {
14999 editor.handle_input("#", window, cx);
15000 });
15001 cx.executor().run_until_parked();
15002 cx.update_editor(|editor, window, cx| {
15003 editor.handle_input("i", window, cx);
15004 });
15005 cx.executor().run_until_parked();
15006 cx.update_editor(|editor, window, cx| {
15007 editor.handle_input("n", window, cx);
15008 });
15009 cx.executor().run_until_parked();
15010 cx.assert_editor_state(
15011 "#ifndef BAR_H
15012#define BAR_H
15013
15014#include <stdbool.h>
15015
15016int fn_branch(bool do_branch1, bool do_branch2);
15017
15018#endif // BAR_H
15019#inˇ",
15020 );
15021
15022 cx.lsp
15023 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15024 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15025 is_incomplete: false,
15026 item_defaults: None,
15027 items: vec![lsp::CompletionItem {
15028 kind: Some(lsp::CompletionItemKind::SNIPPET),
15029 label_details: Some(lsp::CompletionItemLabelDetails {
15030 detail: Some("header".to_string()),
15031 description: None,
15032 }),
15033 label: " include".to_string(),
15034 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15035 range: lsp::Range {
15036 start: lsp::Position {
15037 line: 8,
15038 character: 1,
15039 },
15040 end: lsp::Position {
15041 line: 8,
15042 character: 1,
15043 },
15044 },
15045 new_text: "include \"$0\"".to_string(),
15046 })),
15047 sort_text: Some("40b67681include".to_string()),
15048 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15049 filter_text: Some("include".to_string()),
15050 insert_text: Some("include \"$0\"".to_string()),
15051 ..lsp::CompletionItem::default()
15052 }],
15053 })))
15054 });
15055 cx.update_editor(|editor, window, cx| {
15056 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15057 });
15058 cx.executor().run_until_parked();
15059 cx.update_editor(|editor, window, cx| {
15060 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15061 });
15062 cx.executor().run_until_parked();
15063 cx.assert_editor_state(
15064 "#ifndef BAR_H
15065#define BAR_H
15066
15067#include <stdbool.h>
15068
15069int fn_branch(bool do_branch1, bool do_branch2);
15070
15071#endif // BAR_H
15072#include \"ˇ\"",
15073 );
15074
15075 cx.lsp
15076 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15077 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15078 is_incomplete: true,
15079 item_defaults: None,
15080 items: vec![lsp::CompletionItem {
15081 kind: Some(lsp::CompletionItemKind::FILE),
15082 label: "AGL/".to_string(),
15083 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15084 range: lsp::Range {
15085 start: lsp::Position {
15086 line: 8,
15087 character: 10,
15088 },
15089 end: lsp::Position {
15090 line: 8,
15091 character: 11,
15092 },
15093 },
15094 new_text: "AGL/".to_string(),
15095 })),
15096 sort_text: Some("40b67681AGL/".to_string()),
15097 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15098 filter_text: Some("AGL/".to_string()),
15099 insert_text: Some("AGL/".to_string()),
15100 ..lsp::CompletionItem::default()
15101 }],
15102 })))
15103 });
15104 cx.update_editor(|editor, window, cx| {
15105 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15106 });
15107 cx.executor().run_until_parked();
15108 cx.update_editor(|editor, window, cx| {
15109 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15110 });
15111 cx.executor().run_until_parked();
15112 cx.assert_editor_state(
15113 r##"#ifndef BAR_H
15114#define BAR_H
15115
15116#include <stdbool.h>
15117
15118int fn_branch(bool do_branch1, bool do_branch2);
15119
15120#endif // BAR_H
15121#include "AGL/ˇ"##,
15122 );
15123
15124 cx.update_editor(|editor, window, cx| {
15125 editor.handle_input("\"", window, cx);
15126 });
15127 cx.executor().run_until_parked();
15128 cx.assert_editor_state(
15129 r##"#ifndef BAR_H
15130#define BAR_H
15131
15132#include <stdbool.h>
15133
15134int fn_branch(bool do_branch1, bool do_branch2);
15135
15136#endif // BAR_H
15137#include "AGL/"ˇ"##,
15138 );
15139}
15140
15141#[gpui::test]
15142async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
15143 init_test(cx, |_| {});
15144
15145 let mut cx = EditorLspTestContext::new_rust(
15146 lsp::ServerCapabilities {
15147 completion_provider: Some(lsp::CompletionOptions {
15148 trigger_characters: Some(vec![".".to_string()]),
15149 resolve_provider: Some(true),
15150 ..Default::default()
15151 }),
15152 ..Default::default()
15153 },
15154 cx,
15155 )
15156 .await;
15157
15158 cx.set_state("fn main() { let a = 2ˇ; }");
15159 cx.simulate_keystroke(".");
15160 let completion_item = lsp::CompletionItem {
15161 label: "Some".into(),
15162 kind: Some(lsp::CompletionItemKind::SNIPPET),
15163 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15164 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15165 kind: lsp::MarkupKind::Markdown,
15166 value: "```rust\nSome(2)\n```".to_string(),
15167 })),
15168 deprecated: Some(false),
15169 sort_text: Some("Some".to_string()),
15170 filter_text: Some("Some".to_string()),
15171 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15172 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15173 range: lsp::Range {
15174 start: lsp::Position {
15175 line: 0,
15176 character: 22,
15177 },
15178 end: lsp::Position {
15179 line: 0,
15180 character: 22,
15181 },
15182 },
15183 new_text: "Some(2)".to_string(),
15184 })),
15185 additional_text_edits: Some(vec![lsp::TextEdit {
15186 range: lsp::Range {
15187 start: lsp::Position {
15188 line: 0,
15189 character: 20,
15190 },
15191 end: lsp::Position {
15192 line: 0,
15193 character: 22,
15194 },
15195 },
15196 new_text: "".to_string(),
15197 }]),
15198 ..Default::default()
15199 };
15200
15201 let closure_completion_item = completion_item.clone();
15202 let counter = Arc::new(AtomicUsize::new(0));
15203 let counter_clone = counter.clone();
15204 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15205 let task_completion_item = closure_completion_item.clone();
15206 counter_clone.fetch_add(1, atomic::Ordering::Release);
15207 async move {
15208 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15209 is_incomplete: true,
15210 item_defaults: None,
15211 items: vec![task_completion_item],
15212 })))
15213 }
15214 });
15215
15216 cx.condition(|editor, _| editor.context_menu_visible())
15217 .await;
15218 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
15219 assert!(request.next().await.is_some());
15220 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15221
15222 cx.simulate_keystrokes("S o m");
15223 cx.condition(|editor, _| editor.context_menu_visible())
15224 .await;
15225 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
15226 assert!(request.next().await.is_some());
15227 assert!(request.next().await.is_some());
15228 assert!(request.next().await.is_some());
15229 request.close();
15230 assert!(request.next().await.is_none());
15231 assert_eq!(
15232 counter.load(atomic::Ordering::Acquire),
15233 4,
15234 "With the completions menu open, only one LSP request should happen per input"
15235 );
15236}
15237
15238#[gpui::test]
15239async fn test_toggle_comment(cx: &mut TestAppContext) {
15240 init_test(cx, |_| {});
15241 let mut cx = EditorTestContext::new(cx).await;
15242 let language = Arc::new(Language::new(
15243 LanguageConfig {
15244 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15245 ..Default::default()
15246 },
15247 Some(tree_sitter_rust::LANGUAGE.into()),
15248 ));
15249 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15250
15251 // If multiple selections intersect a line, the line is only toggled once.
15252 cx.set_state(indoc! {"
15253 fn a() {
15254 «//b();
15255 ˇ»// «c();
15256 //ˇ» d();
15257 }
15258 "});
15259
15260 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15261
15262 cx.assert_editor_state(indoc! {"
15263 fn a() {
15264 «b();
15265 c();
15266 ˇ» d();
15267 }
15268 "});
15269
15270 // The comment prefix is inserted at the same column for every line in a
15271 // selection.
15272 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15273
15274 cx.assert_editor_state(indoc! {"
15275 fn a() {
15276 // «b();
15277 // c();
15278 ˇ»// d();
15279 }
15280 "});
15281
15282 // If a selection ends at the beginning of a line, that line is not toggled.
15283 cx.set_selections_state(indoc! {"
15284 fn a() {
15285 // b();
15286 «// c();
15287 ˇ» // d();
15288 }
15289 "});
15290
15291 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), 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 span a single line and is empty, the line is toggled.
15302 cx.set_state(indoc! {"
15303 fn a() {
15304 a();
15305 b();
15306 ˇ
15307 }
15308 "});
15309
15310 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15311
15312 cx.assert_editor_state(indoc! {"
15313 fn a() {
15314 a();
15315 b();
15316 //•ˇ
15317 }
15318 "});
15319
15320 // If a selection span multiple lines, empty lines are not toggled.
15321 cx.set_state(indoc! {"
15322 fn a() {
15323 «a();
15324
15325 c();ˇ»
15326 }
15327 "});
15328
15329 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15330
15331 cx.assert_editor_state(indoc! {"
15332 fn a() {
15333 // «a();
15334
15335 // c();ˇ»
15336 }
15337 "});
15338
15339 // If a selection includes multiple comment prefixes, all lines are uncommented.
15340 cx.set_state(indoc! {"
15341 fn a() {
15342 «// a();
15343 /// b();
15344 //! c();ˇ»
15345 }
15346 "});
15347
15348 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15349
15350 cx.assert_editor_state(indoc! {"
15351 fn a() {
15352 «a();
15353 b();
15354 c();ˇ»
15355 }
15356 "});
15357}
15358
15359#[gpui::test]
15360async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
15361 init_test(cx, |_| {});
15362 let mut cx = EditorTestContext::new(cx).await;
15363 let language = Arc::new(Language::new(
15364 LanguageConfig {
15365 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15366 ..Default::default()
15367 },
15368 Some(tree_sitter_rust::LANGUAGE.into()),
15369 ));
15370 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15371
15372 let toggle_comments = &ToggleComments {
15373 advance_downwards: false,
15374 ignore_indent: true,
15375 };
15376
15377 // If multiple selections intersect a line, the line is only toggled once.
15378 cx.set_state(indoc! {"
15379 fn a() {
15380 // «b();
15381 // c();
15382 // ˇ» d();
15383 }
15384 "});
15385
15386 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15387
15388 cx.assert_editor_state(indoc! {"
15389 fn a() {
15390 «b();
15391 c();
15392 ˇ» d();
15393 }
15394 "});
15395
15396 // The comment prefix is inserted at the beginning of each line
15397 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15398
15399 cx.assert_editor_state(indoc! {"
15400 fn a() {
15401 // «b();
15402 // c();
15403 // ˇ» d();
15404 }
15405 "});
15406
15407 // If a selection ends at the beginning of a line, that line is not toggled.
15408 cx.set_selections_state(indoc! {"
15409 fn a() {
15410 // b();
15411 // «c();
15412 ˇ»// d();
15413 }
15414 "});
15415
15416 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15417
15418 cx.assert_editor_state(indoc! {"
15419 fn a() {
15420 // b();
15421 «c();
15422 ˇ»// d();
15423 }
15424 "});
15425
15426 // If a selection span a single line and is empty, the line is toggled.
15427 cx.set_state(indoc! {"
15428 fn a() {
15429 a();
15430 b();
15431 ˇ
15432 }
15433 "});
15434
15435 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15436
15437 cx.assert_editor_state(indoc! {"
15438 fn a() {
15439 a();
15440 b();
15441 //ˇ
15442 }
15443 "});
15444
15445 // If a selection span multiple lines, empty lines are not toggled.
15446 cx.set_state(indoc! {"
15447 fn a() {
15448 «a();
15449
15450 c();ˇ»
15451 }
15452 "});
15453
15454 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15455
15456 cx.assert_editor_state(indoc! {"
15457 fn a() {
15458 // «a();
15459
15460 // c();ˇ»
15461 }
15462 "});
15463
15464 // If a selection includes multiple comment prefixes, all lines are uncommented.
15465 cx.set_state(indoc! {"
15466 fn a() {
15467 // «a();
15468 /// b();
15469 //! c();ˇ»
15470 }
15471 "});
15472
15473 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15474
15475 cx.assert_editor_state(indoc! {"
15476 fn a() {
15477 «a();
15478 b();
15479 c();ˇ»
15480 }
15481 "});
15482}
15483
15484#[gpui::test]
15485async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
15486 init_test(cx, |_| {});
15487
15488 let language = Arc::new(Language::new(
15489 LanguageConfig {
15490 line_comments: vec!["// ".into()],
15491 ..Default::default()
15492 },
15493 Some(tree_sitter_rust::LANGUAGE.into()),
15494 ));
15495
15496 let mut cx = EditorTestContext::new(cx).await;
15497
15498 cx.language_registry().add(language.clone());
15499 cx.update_buffer(|buffer, cx| {
15500 buffer.set_language(Some(language), cx);
15501 });
15502
15503 let toggle_comments = &ToggleComments {
15504 advance_downwards: true,
15505 ignore_indent: false,
15506 };
15507
15508 // Single cursor on one line -> advance
15509 // Cursor moves horizontally 3 characters as well on non-blank line
15510 cx.set_state(indoc!(
15511 "fn a() {
15512 ˇdog();
15513 cat();
15514 }"
15515 ));
15516 cx.update_editor(|editor, window, cx| {
15517 editor.toggle_comments(toggle_comments, window, cx);
15518 });
15519 cx.assert_editor_state(indoc!(
15520 "fn a() {
15521 // dog();
15522 catˇ();
15523 }"
15524 ));
15525
15526 // Single selection on one line -> don't advance
15527 cx.set_state(indoc!(
15528 "fn a() {
15529 «dog()ˇ»;
15530 cat();
15531 }"
15532 ));
15533 cx.update_editor(|editor, window, cx| {
15534 editor.toggle_comments(toggle_comments, window, cx);
15535 });
15536 cx.assert_editor_state(indoc!(
15537 "fn a() {
15538 // «dog()ˇ»;
15539 cat();
15540 }"
15541 ));
15542
15543 // Multiple cursors on one line -> advance
15544 cx.set_state(indoc!(
15545 "fn a() {
15546 ˇdˇog();
15547 cat();
15548 }"
15549 ));
15550 cx.update_editor(|editor, window, cx| {
15551 editor.toggle_comments(toggle_comments, window, cx);
15552 });
15553 cx.assert_editor_state(indoc!(
15554 "fn a() {
15555 // dog();
15556 catˇ(ˇ);
15557 }"
15558 ));
15559
15560 // Multiple cursors on one line, with selection -> don't advance
15561 cx.set_state(indoc!(
15562 "fn a() {
15563 ˇdˇog«()ˇ»;
15564 cat();
15565 }"
15566 ));
15567 cx.update_editor(|editor, window, cx| {
15568 editor.toggle_comments(toggle_comments, window, cx);
15569 });
15570 cx.assert_editor_state(indoc!(
15571 "fn a() {
15572 // ˇdˇog«()ˇ»;
15573 cat();
15574 }"
15575 ));
15576
15577 // Single cursor on one line -> advance
15578 // Cursor moves to column 0 on blank line
15579 cx.set_state(indoc!(
15580 "fn a() {
15581 ˇdog();
15582
15583 cat();
15584 }"
15585 ));
15586 cx.update_editor(|editor, window, cx| {
15587 editor.toggle_comments(toggle_comments, window, cx);
15588 });
15589 cx.assert_editor_state(indoc!(
15590 "fn a() {
15591 // dog();
15592 ˇ
15593 cat();
15594 }"
15595 ));
15596
15597 // Single cursor on one line -> advance
15598 // Cursor starts and ends at column 0
15599 cx.set_state(indoc!(
15600 "fn a() {
15601 ˇ dog();
15602 cat();
15603 }"
15604 ));
15605 cx.update_editor(|editor, window, cx| {
15606 editor.toggle_comments(toggle_comments, window, cx);
15607 });
15608 cx.assert_editor_state(indoc!(
15609 "fn a() {
15610 // dog();
15611 ˇ cat();
15612 }"
15613 ));
15614}
15615
15616#[gpui::test]
15617async fn test_toggle_block_comment(cx: &mut TestAppContext) {
15618 init_test(cx, |_| {});
15619
15620 let mut cx = EditorTestContext::new(cx).await;
15621
15622 let html_language = Arc::new(
15623 Language::new(
15624 LanguageConfig {
15625 name: "HTML".into(),
15626 block_comment: Some(BlockCommentConfig {
15627 start: "<!-- ".into(),
15628 prefix: "".into(),
15629 end: " -->".into(),
15630 tab_size: 0,
15631 }),
15632 ..Default::default()
15633 },
15634 Some(tree_sitter_html::LANGUAGE.into()),
15635 )
15636 .with_injection_query(
15637 r#"
15638 (script_element
15639 (raw_text) @injection.content
15640 (#set! injection.language "javascript"))
15641 "#,
15642 )
15643 .unwrap(),
15644 );
15645
15646 let javascript_language = Arc::new(Language::new(
15647 LanguageConfig {
15648 name: "JavaScript".into(),
15649 line_comments: vec!["// ".into()],
15650 ..Default::default()
15651 },
15652 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
15653 ));
15654
15655 cx.language_registry().add(html_language.clone());
15656 cx.language_registry().add(javascript_language);
15657 cx.update_buffer(|buffer, cx| {
15658 buffer.set_language(Some(html_language), cx);
15659 });
15660
15661 // Toggle comments for empty selections
15662 cx.set_state(
15663 &r#"
15664 <p>A</p>ˇ
15665 <p>B</p>ˇ
15666 <p>C</p>ˇ
15667 "#
15668 .unindent(),
15669 );
15670 cx.update_editor(|editor, window, cx| {
15671 editor.toggle_comments(&ToggleComments::default(), window, cx)
15672 });
15673 cx.assert_editor_state(
15674 &r#"
15675 <!-- <p>A</p>ˇ -->
15676 <!-- <p>B</p>ˇ -->
15677 <!-- <p>C</p>ˇ -->
15678 "#
15679 .unindent(),
15680 );
15681 cx.update_editor(|editor, window, cx| {
15682 editor.toggle_comments(&ToggleComments::default(), window, cx)
15683 });
15684 cx.assert_editor_state(
15685 &r#"
15686 <p>A</p>ˇ
15687 <p>B</p>ˇ
15688 <p>C</p>ˇ
15689 "#
15690 .unindent(),
15691 );
15692
15693 // Toggle comments for mixture of empty and non-empty selections, where
15694 // multiple selections occupy a given line.
15695 cx.set_state(
15696 &r#"
15697 <p>A«</p>
15698 <p>ˇ»B</p>ˇ
15699 <p>C«</p>
15700 <p>ˇ»D</p>ˇ
15701 "#
15702 .unindent(),
15703 );
15704
15705 cx.update_editor(|editor, window, cx| {
15706 editor.toggle_comments(&ToggleComments::default(), window, cx)
15707 });
15708 cx.assert_editor_state(
15709 &r#"
15710 <!-- <p>A«</p>
15711 <p>ˇ»B</p>ˇ -->
15712 <!-- <p>C«</p>
15713 <p>ˇ»D</p>ˇ -->
15714 "#
15715 .unindent(),
15716 );
15717 cx.update_editor(|editor, window, cx| {
15718 editor.toggle_comments(&ToggleComments::default(), window, cx)
15719 });
15720 cx.assert_editor_state(
15721 &r#"
15722 <p>A«</p>
15723 <p>ˇ»B</p>ˇ
15724 <p>C«</p>
15725 <p>ˇ»D</p>ˇ
15726 "#
15727 .unindent(),
15728 );
15729
15730 // Toggle comments when different languages are active for different
15731 // selections.
15732 cx.set_state(
15733 &r#"
15734 ˇ<script>
15735 ˇvar x = new Y();
15736 ˇ</script>
15737 "#
15738 .unindent(),
15739 );
15740 cx.executor().run_until_parked();
15741 cx.update_editor(|editor, window, cx| {
15742 editor.toggle_comments(&ToggleComments::default(), window, cx)
15743 });
15744 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
15745 // Uncommenting and commenting from this position brings in even more wrong artifacts.
15746 cx.assert_editor_state(
15747 &r#"
15748 <!-- ˇ<script> -->
15749 // ˇvar x = new Y();
15750 <!-- ˇ</script> -->
15751 "#
15752 .unindent(),
15753 );
15754}
15755
15756#[gpui::test]
15757fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
15758 init_test(cx, |_| {});
15759
15760 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15761 let multibuffer = cx.new(|cx| {
15762 let mut multibuffer = MultiBuffer::new(ReadWrite);
15763 multibuffer.push_excerpts(
15764 buffer.clone(),
15765 [
15766 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
15767 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
15768 ],
15769 cx,
15770 );
15771 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
15772 multibuffer
15773 });
15774
15775 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
15776 editor.update_in(cx, |editor, window, cx| {
15777 assert_eq!(editor.text(cx), "aaaa\nbbbb");
15778 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15779 s.select_ranges([
15780 Point::new(0, 0)..Point::new(0, 0),
15781 Point::new(1, 0)..Point::new(1, 0),
15782 ])
15783 });
15784
15785 editor.handle_input("X", window, cx);
15786 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
15787 assert_eq!(
15788 editor.selections.ranges(cx),
15789 [
15790 Point::new(0, 1)..Point::new(0, 1),
15791 Point::new(1, 1)..Point::new(1, 1),
15792 ]
15793 );
15794
15795 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
15796 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15797 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
15798 });
15799 editor.backspace(&Default::default(), window, cx);
15800 assert_eq!(editor.text(cx), "Xa\nbbb");
15801 assert_eq!(
15802 editor.selections.ranges(cx),
15803 [Point::new(1, 0)..Point::new(1, 0)]
15804 );
15805
15806 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15807 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
15808 });
15809 editor.backspace(&Default::default(), window, cx);
15810 assert_eq!(editor.text(cx), "X\nbb");
15811 assert_eq!(
15812 editor.selections.ranges(cx),
15813 [Point::new(0, 1)..Point::new(0, 1)]
15814 );
15815 });
15816}
15817
15818#[gpui::test]
15819fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
15820 init_test(cx, |_| {});
15821
15822 let markers = vec![('[', ']').into(), ('(', ')').into()];
15823 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
15824 indoc! {"
15825 [aaaa
15826 (bbbb]
15827 cccc)",
15828 },
15829 markers.clone(),
15830 );
15831 let excerpt_ranges = markers.into_iter().map(|marker| {
15832 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
15833 ExcerptRange::new(context)
15834 });
15835 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
15836 let multibuffer = cx.new(|cx| {
15837 let mut multibuffer = MultiBuffer::new(ReadWrite);
15838 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
15839 multibuffer
15840 });
15841
15842 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
15843 editor.update_in(cx, |editor, window, cx| {
15844 let (expected_text, selection_ranges) = marked_text_ranges(
15845 indoc! {"
15846 aaaa
15847 bˇbbb
15848 bˇbbˇb
15849 cccc"
15850 },
15851 true,
15852 );
15853 assert_eq!(editor.text(cx), expected_text);
15854 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15855 s.select_ranges(selection_ranges)
15856 });
15857
15858 editor.handle_input("X", window, cx);
15859
15860 let (expected_text, expected_selections) = marked_text_ranges(
15861 indoc! {"
15862 aaaa
15863 bXˇbbXb
15864 bXˇbbXˇb
15865 cccc"
15866 },
15867 false,
15868 );
15869 assert_eq!(editor.text(cx), expected_text);
15870 assert_eq!(editor.selections.ranges(cx), expected_selections);
15871
15872 editor.newline(&Newline, window, cx);
15873 let (expected_text, expected_selections) = marked_text_ranges(
15874 indoc! {"
15875 aaaa
15876 bX
15877 ˇbbX
15878 b
15879 bX
15880 ˇbbX
15881 ˇb
15882 cccc"
15883 },
15884 false,
15885 );
15886 assert_eq!(editor.text(cx), expected_text);
15887 assert_eq!(editor.selections.ranges(cx), expected_selections);
15888 });
15889}
15890
15891#[gpui::test]
15892fn test_refresh_selections(cx: &mut TestAppContext) {
15893 init_test(cx, |_| {});
15894
15895 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15896 let mut excerpt1_id = None;
15897 let multibuffer = cx.new(|cx| {
15898 let mut multibuffer = MultiBuffer::new(ReadWrite);
15899 excerpt1_id = multibuffer
15900 .push_excerpts(
15901 buffer.clone(),
15902 [
15903 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
15904 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
15905 ],
15906 cx,
15907 )
15908 .into_iter()
15909 .next();
15910 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
15911 multibuffer
15912 });
15913
15914 let editor = cx.add_window(|window, cx| {
15915 let mut editor = build_editor(multibuffer.clone(), window, cx);
15916 let snapshot = editor.snapshot(window, cx);
15917 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15918 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
15919 });
15920 editor.begin_selection(
15921 Point::new(2, 1).to_display_point(&snapshot),
15922 true,
15923 1,
15924 window,
15925 cx,
15926 );
15927 assert_eq!(
15928 editor.selections.ranges(cx),
15929 [
15930 Point::new(1, 3)..Point::new(1, 3),
15931 Point::new(2, 1)..Point::new(2, 1),
15932 ]
15933 );
15934 editor
15935 });
15936
15937 // Refreshing selections is a no-op when excerpts haven't changed.
15938 _ = editor.update(cx, |editor, window, cx| {
15939 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
15940 assert_eq!(
15941 editor.selections.ranges(cx),
15942 [
15943 Point::new(1, 3)..Point::new(1, 3),
15944 Point::new(2, 1)..Point::new(2, 1),
15945 ]
15946 );
15947 });
15948
15949 multibuffer.update(cx, |multibuffer, cx| {
15950 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
15951 });
15952 _ = editor.update(cx, |editor, window, cx| {
15953 // Removing an excerpt causes the first selection to become degenerate.
15954 assert_eq!(
15955 editor.selections.ranges(cx),
15956 [
15957 Point::new(0, 0)..Point::new(0, 0),
15958 Point::new(0, 1)..Point::new(0, 1)
15959 ]
15960 );
15961
15962 // Refreshing selections will relocate the first selection to the original buffer
15963 // location.
15964 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
15965 assert_eq!(
15966 editor.selections.ranges(cx),
15967 [
15968 Point::new(0, 1)..Point::new(0, 1),
15969 Point::new(0, 3)..Point::new(0, 3)
15970 ]
15971 );
15972 assert!(editor.selections.pending_anchor().is_some());
15973 });
15974}
15975
15976#[gpui::test]
15977fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
15978 init_test(cx, |_| {});
15979
15980 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15981 let mut excerpt1_id = None;
15982 let multibuffer = cx.new(|cx| {
15983 let mut multibuffer = MultiBuffer::new(ReadWrite);
15984 excerpt1_id = multibuffer
15985 .push_excerpts(
15986 buffer.clone(),
15987 [
15988 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
15989 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
15990 ],
15991 cx,
15992 )
15993 .into_iter()
15994 .next();
15995 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
15996 multibuffer
15997 });
15998
15999 let editor = cx.add_window(|window, cx| {
16000 let mut editor = build_editor(multibuffer.clone(), window, cx);
16001 let snapshot = editor.snapshot(window, cx);
16002 editor.begin_selection(
16003 Point::new(1, 3).to_display_point(&snapshot),
16004 false,
16005 1,
16006 window,
16007 cx,
16008 );
16009 assert_eq!(
16010 editor.selections.ranges(cx),
16011 [Point::new(1, 3)..Point::new(1, 3)]
16012 );
16013 editor
16014 });
16015
16016 multibuffer.update(cx, |multibuffer, cx| {
16017 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16018 });
16019 _ = editor.update(cx, |editor, window, cx| {
16020 assert_eq!(
16021 editor.selections.ranges(cx),
16022 [Point::new(0, 0)..Point::new(0, 0)]
16023 );
16024
16025 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
16026 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16027 assert_eq!(
16028 editor.selections.ranges(cx),
16029 [Point::new(0, 3)..Point::new(0, 3)]
16030 );
16031 assert!(editor.selections.pending_anchor().is_some());
16032 });
16033}
16034
16035#[gpui::test]
16036async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
16037 init_test(cx, |_| {});
16038
16039 let language = Arc::new(
16040 Language::new(
16041 LanguageConfig {
16042 brackets: BracketPairConfig {
16043 pairs: vec![
16044 BracketPair {
16045 start: "{".to_string(),
16046 end: "}".to_string(),
16047 close: true,
16048 surround: true,
16049 newline: true,
16050 },
16051 BracketPair {
16052 start: "/* ".to_string(),
16053 end: " */".to_string(),
16054 close: true,
16055 surround: true,
16056 newline: true,
16057 },
16058 ],
16059 ..Default::default()
16060 },
16061 ..Default::default()
16062 },
16063 Some(tree_sitter_rust::LANGUAGE.into()),
16064 )
16065 .with_indents_query("")
16066 .unwrap(),
16067 );
16068
16069 let text = concat!(
16070 "{ }\n", //
16071 " x\n", //
16072 " /* */\n", //
16073 "x\n", //
16074 "{{} }\n", //
16075 );
16076
16077 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
16078 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16079 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16080 editor
16081 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
16082 .await;
16083
16084 editor.update_in(cx, |editor, window, cx| {
16085 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16086 s.select_display_ranges([
16087 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
16088 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
16089 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
16090 ])
16091 });
16092 editor.newline(&Newline, window, cx);
16093
16094 assert_eq!(
16095 editor.buffer().read(cx).read(cx).text(),
16096 concat!(
16097 "{ \n", // Suppress rustfmt
16098 "\n", //
16099 "}\n", //
16100 " x\n", //
16101 " /* \n", //
16102 " \n", //
16103 " */\n", //
16104 "x\n", //
16105 "{{} \n", //
16106 "}\n", //
16107 )
16108 );
16109 });
16110}
16111
16112#[gpui::test]
16113fn test_highlighted_ranges(cx: &mut TestAppContext) {
16114 init_test(cx, |_| {});
16115
16116 let editor = cx.add_window(|window, cx| {
16117 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
16118 build_editor(buffer, window, cx)
16119 });
16120
16121 _ = editor.update(cx, |editor, window, cx| {
16122 struct Type1;
16123 struct Type2;
16124
16125 let buffer = editor.buffer.read(cx).snapshot(cx);
16126
16127 let anchor_range =
16128 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
16129
16130 editor.highlight_background::<Type1>(
16131 &[
16132 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
16133 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
16134 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
16135 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
16136 ],
16137 |_| Hsla::red(),
16138 cx,
16139 );
16140 editor.highlight_background::<Type2>(
16141 &[
16142 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
16143 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
16144 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
16145 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
16146 ],
16147 |_| Hsla::green(),
16148 cx,
16149 );
16150
16151 let snapshot = editor.snapshot(window, cx);
16152 let highlighted_ranges = editor.sorted_background_highlights_in_range(
16153 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
16154 &snapshot,
16155 cx.theme(),
16156 );
16157 assert_eq!(
16158 highlighted_ranges,
16159 &[
16160 (
16161 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
16162 Hsla::green(),
16163 ),
16164 (
16165 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
16166 Hsla::red(),
16167 ),
16168 (
16169 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
16170 Hsla::green(),
16171 ),
16172 (
16173 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16174 Hsla::red(),
16175 ),
16176 ]
16177 );
16178 assert_eq!(
16179 editor.sorted_background_highlights_in_range(
16180 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
16181 &snapshot,
16182 cx.theme(),
16183 ),
16184 &[(
16185 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16186 Hsla::red(),
16187 )]
16188 );
16189 });
16190}
16191
16192#[gpui::test]
16193async fn test_following(cx: &mut TestAppContext) {
16194 init_test(cx, |_| {});
16195
16196 let fs = FakeFs::new(cx.executor());
16197 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16198
16199 let buffer = project.update(cx, |project, cx| {
16200 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, false, cx);
16201 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
16202 });
16203 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
16204 let follower = cx.update(|cx| {
16205 cx.open_window(
16206 WindowOptions {
16207 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
16208 gpui::Point::new(px(0.), px(0.)),
16209 gpui::Point::new(px(10.), px(80.)),
16210 ))),
16211 ..Default::default()
16212 },
16213 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
16214 )
16215 .unwrap()
16216 });
16217
16218 let is_still_following = Rc::new(RefCell::new(true));
16219 let follower_edit_event_count = Rc::new(RefCell::new(0));
16220 let pending_update = Rc::new(RefCell::new(None));
16221 let leader_entity = leader.root(cx).unwrap();
16222 let follower_entity = follower.root(cx).unwrap();
16223 _ = follower.update(cx, {
16224 let update = pending_update.clone();
16225 let is_still_following = is_still_following.clone();
16226 let follower_edit_event_count = follower_edit_event_count.clone();
16227 |_, window, cx| {
16228 cx.subscribe_in(
16229 &leader_entity,
16230 window,
16231 move |_, leader, event, window, cx| {
16232 leader.read(cx).add_event_to_update_proto(
16233 event,
16234 &mut update.borrow_mut(),
16235 window,
16236 cx,
16237 );
16238 },
16239 )
16240 .detach();
16241
16242 cx.subscribe_in(
16243 &follower_entity,
16244 window,
16245 move |_, _, event: &EditorEvent, _window, _cx| {
16246 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
16247 *is_still_following.borrow_mut() = false;
16248 }
16249
16250 if let EditorEvent::BufferEdited = event {
16251 *follower_edit_event_count.borrow_mut() += 1;
16252 }
16253 },
16254 )
16255 .detach();
16256 }
16257 });
16258
16259 // Update the selections only
16260 _ = leader.update(cx, |leader, window, cx| {
16261 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16262 s.select_ranges([1..1])
16263 });
16264 });
16265 follower
16266 .update(cx, |follower, window, cx| {
16267 follower.apply_update_proto(
16268 &project,
16269 pending_update.borrow_mut().take().unwrap(),
16270 window,
16271 cx,
16272 )
16273 })
16274 .unwrap()
16275 .await
16276 .unwrap();
16277 _ = follower.update(cx, |follower, _, cx| {
16278 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
16279 });
16280 assert!(*is_still_following.borrow());
16281 assert_eq!(*follower_edit_event_count.borrow(), 0);
16282
16283 // Update the scroll position only
16284 _ = leader.update(cx, |leader, window, cx| {
16285 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16286 });
16287 follower
16288 .update(cx, |follower, window, cx| {
16289 follower.apply_update_proto(
16290 &project,
16291 pending_update.borrow_mut().take().unwrap(),
16292 window,
16293 cx,
16294 )
16295 })
16296 .unwrap()
16297 .await
16298 .unwrap();
16299 assert_eq!(
16300 follower
16301 .update(cx, |follower, _, cx| follower.scroll_position(cx))
16302 .unwrap(),
16303 gpui::Point::new(1.5, 3.5)
16304 );
16305 assert!(*is_still_following.borrow());
16306 assert_eq!(*follower_edit_event_count.borrow(), 0);
16307
16308 // Update the selections and scroll position. The follower's scroll position is updated
16309 // via autoscroll, not via the leader's exact scroll position.
16310 _ = leader.update(cx, |leader, window, cx| {
16311 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16312 s.select_ranges([0..0])
16313 });
16314 leader.request_autoscroll(Autoscroll::newest(), cx);
16315 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16316 });
16317 follower
16318 .update(cx, |follower, window, cx| {
16319 follower.apply_update_proto(
16320 &project,
16321 pending_update.borrow_mut().take().unwrap(),
16322 window,
16323 cx,
16324 )
16325 })
16326 .unwrap()
16327 .await
16328 .unwrap();
16329 _ = follower.update(cx, |follower, _, cx| {
16330 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
16331 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
16332 });
16333 assert!(*is_still_following.borrow());
16334
16335 // Creating a pending selection that precedes another selection
16336 _ = leader.update(cx, |leader, window, cx| {
16337 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16338 s.select_ranges([1..1])
16339 });
16340 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
16341 });
16342 follower
16343 .update(cx, |follower, window, cx| {
16344 follower.apply_update_proto(
16345 &project,
16346 pending_update.borrow_mut().take().unwrap(),
16347 window,
16348 cx,
16349 )
16350 })
16351 .unwrap()
16352 .await
16353 .unwrap();
16354 _ = follower.update(cx, |follower, _, cx| {
16355 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
16356 });
16357 assert!(*is_still_following.borrow());
16358
16359 // Extend the pending selection so that it surrounds another selection
16360 _ = leader.update(cx, |leader, window, cx| {
16361 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
16362 });
16363 follower
16364 .update(cx, |follower, window, cx| {
16365 follower.apply_update_proto(
16366 &project,
16367 pending_update.borrow_mut().take().unwrap(),
16368 window,
16369 cx,
16370 )
16371 })
16372 .unwrap()
16373 .await
16374 .unwrap();
16375 _ = follower.update(cx, |follower, _, cx| {
16376 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
16377 });
16378
16379 // Scrolling locally breaks the follow
16380 _ = follower.update(cx, |follower, window, cx| {
16381 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
16382 follower.set_scroll_anchor(
16383 ScrollAnchor {
16384 anchor: top_anchor,
16385 offset: gpui::Point::new(0.0, 0.5),
16386 },
16387 window,
16388 cx,
16389 );
16390 });
16391 assert!(!(*is_still_following.borrow()));
16392}
16393
16394#[gpui::test]
16395async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
16396 init_test(cx, |_| {});
16397
16398 let fs = FakeFs::new(cx.executor());
16399 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16400 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16401 let pane = workspace
16402 .update(cx, |workspace, _, _| workspace.active_pane().clone())
16403 .unwrap();
16404
16405 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16406
16407 let leader = pane.update_in(cx, |_, window, cx| {
16408 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
16409 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
16410 });
16411
16412 // Start following the editor when it has no excerpts.
16413 let mut state_message =
16414 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16415 let workspace_entity = workspace.root(cx).unwrap();
16416 let follower_1 = cx
16417 .update_window(*workspace.deref(), |_, window, cx| {
16418 Editor::from_state_proto(
16419 workspace_entity,
16420 ViewId {
16421 creator: CollaboratorId::PeerId(PeerId::default()),
16422 id: 0,
16423 },
16424 &mut state_message,
16425 window,
16426 cx,
16427 )
16428 })
16429 .unwrap()
16430 .unwrap()
16431 .await
16432 .unwrap();
16433
16434 let update_message = Rc::new(RefCell::new(None));
16435 follower_1.update_in(cx, {
16436 let update = update_message.clone();
16437 |_, window, cx| {
16438 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
16439 leader.read(cx).add_event_to_update_proto(
16440 event,
16441 &mut update.borrow_mut(),
16442 window,
16443 cx,
16444 );
16445 })
16446 .detach();
16447 }
16448 });
16449
16450 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
16451 (
16452 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, false, cx),
16453 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, false, cx),
16454 )
16455 });
16456
16457 // Insert some excerpts.
16458 leader.update(cx, |leader, cx| {
16459 leader.buffer.update(cx, |multibuffer, cx| {
16460 multibuffer.set_excerpts_for_path(
16461 PathKey::namespaced(1, "b.txt".into()),
16462 buffer_1.clone(),
16463 vec![
16464 Point::row_range(0..3),
16465 Point::row_range(1..6),
16466 Point::row_range(12..15),
16467 ],
16468 0,
16469 cx,
16470 );
16471 multibuffer.set_excerpts_for_path(
16472 PathKey::namespaced(1, "a.txt".into()),
16473 buffer_2.clone(),
16474 vec![Point::row_range(0..6), Point::row_range(8..12)],
16475 0,
16476 cx,
16477 );
16478 });
16479 });
16480
16481 // Apply the update of adding the excerpts.
16482 follower_1
16483 .update_in(cx, |follower, window, cx| {
16484 follower.apply_update_proto(
16485 &project,
16486 update_message.borrow().clone().unwrap(),
16487 window,
16488 cx,
16489 )
16490 })
16491 .await
16492 .unwrap();
16493 assert_eq!(
16494 follower_1.update(cx, |editor, cx| editor.text(cx)),
16495 leader.update(cx, |editor, cx| editor.text(cx))
16496 );
16497 update_message.borrow_mut().take();
16498
16499 // Start following separately after it already has excerpts.
16500 let mut state_message =
16501 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16502 let workspace_entity = workspace.root(cx).unwrap();
16503 let follower_2 = cx
16504 .update_window(*workspace.deref(), |_, window, cx| {
16505 Editor::from_state_proto(
16506 workspace_entity,
16507 ViewId {
16508 creator: CollaboratorId::PeerId(PeerId::default()),
16509 id: 0,
16510 },
16511 &mut state_message,
16512 window,
16513 cx,
16514 )
16515 })
16516 .unwrap()
16517 .unwrap()
16518 .await
16519 .unwrap();
16520 assert_eq!(
16521 follower_2.update(cx, |editor, cx| editor.text(cx)),
16522 leader.update(cx, |editor, cx| editor.text(cx))
16523 );
16524
16525 // Remove some excerpts.
16526 leader.update(cx, |leader, cx| {
16527 leader.buffer.update(cx, |multibuffer, cx| {
16528 let excerpt_ids = multibuffer.excerpt_ids();
16529 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
16530 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
16531 });
16532 });
16533
16534 // Apply the update of removing the excerpts.
16535 follower_1
16536 .update_in(cx, |follower, window, cx| {
16537 follower.apply_update_proto(
16538 &project,
16539 update_message.borrow().clone().unwrap(),
16540 window,
16541 cx,
16542 )
16543 })
16544 .await
16545 .unwrap();
16546 follower_2
16547 .update_in(cx, |follower, window, cx| {
16548 follower.apply_update_proto(
16549 &project,
16550 update_message.borrow().clone().unwrap(),
16551 window,
16552 cx,
16553 )
16554 })
16555 .await
16556 .unwrap();
16557 update_message.borrow_mut().take();
16558 assert_eq!(
16559 follower_1.update(cx, |editor, cx| editor.text(cx)),
16560 leader.update(cx, |editor, cx| editor.text(cx))
16561 );
16562}
16563
16564#[gpui::test]
16565async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16566 init_test(cx, |_| {});
16567
16568 let mut cx = EditorTestContext::new(cx).await;
16569 let lsp_store =
16570 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
16571
16572 cx.set_state(indoc! {"
16573 ˇfn func(abc def: i32) -> u32 {
16574 }
16575 "});
16576
16577 cx.update(|_, cx| {
16578 lsp_store.update(cx, |lsp_store, cx| {
16579 lsp_store
16580 .update_diagnostics(
16581 LanguageServerId(0),
16582 lsp::PublishDiagnosticsParams {
16583 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
16584 version: None,
16585 diagnostics: vec![
16586 lsp::Diagnostic {
16587 range: lsp::Range::new(
16588 lsp::Position::new(0, 11),
16589 lsp::Position::new(0, 12),
16590 ),
16591 severity: Some(lsp::DiagnosticSeverity::ERROR),
16592 ..Default::default()
16593 },
16594 lsp::Diagnostic {
16595 range: lsp::Range::new(
16596 lsp::Position::new(0, 12),
16597 lsp::Position::new(0, 15),
16598 ),
16599 severity: Some(lsp::DiagnosticSeverity::ERROR),
16600 ..Default::default()
16601 },
16602 lsp::Diagnostic {
16603 range: lsp::Range::new(
16604 lsp::Position::new(0, 25),
16605 lsp::Position::new(0, 28),
16606 ),
16607 severity: Some(lsp::DiagnosticSeverity::ERROR),
16608 ..Default::default()
16609 },
16610 ],
16611 },
16612 None,
16613 DiagnosticSourceKind::Pushed,
16614 &[],
16615 cx,
16616 )
16617 .unwrap()
16618 });
16619 });
16620
16621 executor.run_until_parked();
16622
16623 cx.update_editor(|editor, window, cx| {
16624 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16625 });
16626
16627 cx.assert_editor_state(indoc! {"
16628 fn func(abc def: i32) -> ˇu32 {
16629 }
16630 "});
16631
16632 cx.update_editor(|editor, window, cx| {
16633 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16634 });
16635
16636 cx.assert_editor_state(indoc! {"
16637 fn func(abc ˇdef: i32) -> u32 {
16638 }
16639 "});
16640
16641 cx.update_editor(|editor, window, cx| {
16642 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16643 });
16644
16645 cx.assert_editor_state(indoc! {"
16646 fn func(abcˇ def: i32) -> u32 {
16647 }
16648 "});
16649
16650 cx.update_editor(|editor, window, cx| {
16651 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16652 });
16653
16654 cx.assert_editor_state(indoc! {"
16655 fn func(abc def: i32) -> ˇu32 {
16656 }
16657 "});
16658}
16659
16660#[gpui::test]
16661async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16662 init_test(cx, |_| {});
16663
16664 let mut cx = EditorTestContext::new(cx).await;
16665
16666 let diff_base = r#"
16667 use some::mod;
16668
16669 const A: u32 = 42;
16670
16671 fn main() {
16672 println!("hello");
16673
16674 println!("world");
16675 }
16676 "#
16677 .unindent();
16678
16679 // Edits are modified, removed, modified, added
16680 cx.set_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.set_head_text(&diff_base);
16696 executor.run_until_parked();
16697
16698 cx.update_editor(|editor, window, cx| {
16699 //Wrap around the bottom of the buffer
16700 for _ in 0..3 {
16701 editor.go_to_next_hunk(&GoToHunk, window, cx);
16702 }
16703 });
16704
16705 cx.assert_editor_state(
16706 &r#"
16707 ˇuse some::modified;
16708
16709
16710 fn main() {
16711 println!("hello there");
16712
16713 println!("around the");
16714 println!("world");
16715 }
16716 "#
16717 .unindent(),
16718 );
16719
16720 cx.update_editor(|editor, window, cx| {
16721 //Wrap around the top of the buffer
16722 for _ in 0..2 {
16723 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16724 }
16725 });
16726
16727 cx.assert_editor_state(
16728 &r#"
16729 use some::modified;
16730
16731
16732 fn main() {
16733 ˇ println!("hello there");
16734
16735 println!("around the");
16736 println!("world");
16737 }
16738 "#
16739 .unindent(),
16740 );
16741
16742 cx.update_editor(|editor, window, cx| {
16743 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16744 });
16745
16746 cx.assert_editor_state(
16747 &r#"
16748 use some::modified;
16749
16750 ˇ
16751 fn main() {
16752 println!("hello there");
16753
16754 println!("around the");
16755 println!("world");
16756 }
16757 "#
16758 .unindent(),
16759 );
16760
16761 cx.update_editor(|editor, window, cx| {
16762 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16763 });
16764
16765 cx.assert_editor_state(
16766 &r#"
16767 ˇuse some::modified;
16768
16769
16770 fn main() {
16771 println!("hello there");
16772
16773 println!("around the");
16774 println!("world");
16775 }
16776 "#
16777 .unindent(),
16778 );
16779
16780 cx.update_editor(|editor, window, cx| {
16781 for _ in 0..2 {
16782 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16783 }
16784 });
16785
16786 cx.assert_editor_state(
16787 &r#"
16788 use some::modified;
16789
16790
16791 fn main() {
16792 ˇ println!("hello there");
16793
16794 println!("around the");
16795 println!("world");
16796 }
16797 "#
16798 .unindent(),
16799 );
16800
16801 cx.update_editor(|editor, window, cx| {
16802 editor.fold(&Fold, window, cx);
16803 });
16804
16805 cx.update_editor(|editor, window, cx| {
16806 editor.go_to_next_hunk(&GoToHunk, window, cx);
16807 });
16808
16809 cx.assert_editor_state(
16810 &r#"
16811 ˇuse some::modified;
16812
16813
16814 fn main() {
16815 println!("hello there");
16816
16817 println!("around the");
16818 println!("world");
16819 }
16820 "#
16821 .unindent(),
16822 );
16823}
16824
16825#[test]
16826fn test_split_words() {
16827 fn split(text: &str) -> Vec<&str> {
16828 split_words(text).collect()
16829 }
16830
16831 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
16832 assert_eq!(split("hello_world"), &["hello_", "world"]);
16833 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
16834 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
16835 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
16836 assert_eq!(split("helloworld"), &["helloworld"]);
16837
16838 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
16839}
16840
16841#[gpui::test]
16842async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
16843 init_test(cx, |_| {});
16844
16845 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
16846 let mut assert = |before, after| {
16847 let _state_context = cx.set_state(before);
16848 cx.run_until_parked();
16849 cx.update_editor(|editor, window, cx| {
16850 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
16851 });
16852 cx.run_until_parked();
16853 cx.assert_editor_state(after);
16854 };
16855
16856 // Outside bracket jumps to outside of matching bracket
16857 assert("console.logˇ(var);", "console.log(var)ˇ;");
16858 assert("console.log(var)ˇ;", "console.logˇ(var);");
16859
16860 // Inside bracket jumps to inside of matching bracket
16861 assert("console.log(ˇvar);", "console.log(varˇ);");
16862 assert("console.log(varˇ);", "console.log(ˇvar);");
16863
16864 // When outside a bracket and inside, favor jumping to the inside bracket
16865 assert(
16866 "console.log('foo', [1, 2, 3]ˇ);",
16867 "console.log(ˇ'foo', [1, 2, 3]);",
16868 );
16869 assert(
16870 "console.log(ˇ'foo', [1, 2, 3]);",
16871 "console.log('foo', [1, 2, 3]ˇ);",
16872 );
16873
16874 // Bias forward if two options are equally likely
16875 assert(
16876 "let result = curried_fun()ˇ();",
16877 "let result = curried_fun()()ˇ;",
16878 );
16879
16880 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
16881 assert(
16882 indoc! {"
16883 function test() {
16884 console.log('test')ˇ
16885 }"},
16886 indoc! {"
16887 function test() {
16888 console.logˇ('test')
16889 }"},
16890 );
16891}
16892
16893#[gpui::test]
16894async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
16895 init_test(cx, |_| {});
16896
16897 let fs = FakeFs::new(cx.executor());
16898 fs.insert_tree(
16899 path!("/a"),
16900 json!({
16901 "main.rs": "fn main() { let a = 5; }",
16902 "other.rs": "// Test file",
16903 }),
16904 )
16905 .await;
16906 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16907
16908 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
16909 language_registry.add(Arc::new(Language::new(
16910 LanguageConfig {
16911 name: "Rust".into(),
16912 matcher: LanguageMatcher {
16913 path_suffixes: vec!["rs".to_string()],
16914 ..Default::default()
16915 },
16916 brackets: BracketPairConfig {
16917 pairs: vec![BracketPair {
16918 start: "{".to_string(),
16919 end: "}".to_string(),
16920 close: true,
16921 surround: true,
16922 newline: true,
16923 }],
16924 disabled_scopes_by_bracket_ix: Vec::new(),
16925 },
16926 ..Default::default()
16927 },
16928 Some(tree_sitter_rust::LANGUAGE.into()),
16929 )));
16930 let mut fake_servers = language_registry.register_fake_lsp(
16931 "Rust",
16932 FakeLspAdapter {
16933 capabilities: lsp::ServerCapabilities {
16934 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
16935 first_trigger_character: "{".to_string(),
16936 more_trigger_character: None,
16937 }),
16938 ..Default::default()
16939 },
16940 ..Default::default()
16941 },
16942 );
16943
16944 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16945
16946 let cx = &mut VisualTestContext::from_window(*workspace, cx);
16947
16948 let worktree_id = workspace
16949 .update(cx, |workspace, _, cx| {
16950 workspace.project().update(cx, |project, cx| {
16951 project.worktrees(cx).next().unwrap().read(cx).id()
16952 })
16953 })
16954 .unwrap();
16955
16956 let buffer = project
16957 .update(cx, |project, cx| {
16958 project.open_local_buffer(path!("/a/main.rs"), cx)
16959 })
16960 .await
16961 .unwrap();
16962 let editor_handle = workspace
16963 .update(cx, |workspace, window, cx| {
16964 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
16965 })
16966 .unwrap()
16967 .await
16968 .unwrap()
16969 .downcast::<Editor>()
16970 .unwrap();
16971
16972 cx.executor().start_waiting();
16973 let fake_server = fake_servers.next().await.unwrap();
16974
16975 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
16976 |params, _| async move {
16977 assert_eq!(
16978 params.text_document_position.text_document.uri,
16979 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
16980 );
16981 assert_eq!(
16982 params.text_document_position.position,
16983 lsp::Position::new(0, 21),
16984 );
16985
16986 Ok(Some(vec![lsp::TextEdit {
16987 new_text: "]".to_string(),
16988 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
16989 }]))
16990 },
16991 );
16992
16993 editor_handle.update_in(cx, |editor, window, cx| {
16994 window.focus(&editor.focus_handle(cx));
16995 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16996 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
16997 });
16998 editor.handle_input("{", window, cx);
16999 });
17000
17001 cx.executor().run_until_parked();
17002
17003 buffer.update(cx, |buffer, _| {
17004 assert_eq!(
17005 buffer.text(),
17006 "fn main() { let a = {5}; }",
17007 "No extra braces from on type formatting should appear in the buffer"
17008 )
17009 });
17010}
17011
17012#[gpui::test(iterations = 20, seeds(31))]
17013async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
17014 init_test(cx, |_| {});
17015
17016 let mut cx = EditorLspTestContext::new_rust(
17017 lsp::ServerCapabilities {
17018 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
17019 first_trigger_character: ".".to_string(),
17020 more_trigger_character: None,
17021 }),
17022 ..Default::default()
17023 },
17024 cx,
17025 )
17026 .await;
17027
17028 cx.update_buffer(|buffer, _| {
17029 // This causes autoindent to be async.
17030 buffer.set_sync_parse_timeout(Duration::ZERO)
17031 });
17032
17033 cx.set_state("fn c() {\n d()ˇ\n}\n");
17034 cx.simulate_keystroke("\n");
17035 cx.run_until_parked();
17036
17037 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
17038 let mut request =
17039 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
17040 let buffer_cloned = buffer_cloned.clone();
17041 async move {
17042 buffer_cloned.update(&mut cx, |buffer, _| {
17043 assert_eq!(
17044 buffer.text(),
17045 "fn c() {\n d()\n .\n}\n",
17046 "OnTypeFormatting should triggered after autoindent applied"
17047 )
17048 })?;
17049
17050 Ok(Some(vec![]))
17051 }
17052 });
17053
17054 cx.simulate_keystroke(".");
17055 cx.run_until_parked();
17056
17057 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
17058 assert!(request.next().await.is_some());
17059 request.close();
17060 assert!(request.next().await.is_none());
17061}
17062
17063#[gpui::test]
17064async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
17065 init_test(cx, |_| {});
17066
17067 let fs = FakeFs::new(cx.executor());
17068 fs.insert_tree(
17069 path!("/a"),
17070 json!({
17071 "main.rs": "fn main() { let a = 5; }",
17072 "other.rs": "// Test file",
17073 }),
17074 )
17075 .await;
17076
17077 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17078
17079 let server_restarts = Arc::new(AtomicUsize::new(0));
17080 let closure_restarts = Arc::clone(&server_restarts);
17081 let language_server_name = "test language server";
17082 let language_name: LanguageName = "Rust".into();
17083
17084 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17085 language_registry.add(Arc::new(Language::new(
17086 LanguageConfig {
17087 name: language_name.clone(),
17088 matcher: LanguageMatcher {
17089 path_suffixes: vec!["rs".to_string()],
17090 ..Default::default()
17091 },
17092 ..Default::default()
17093 },
17094 Some(tree_sitter_rust::LANGUAGE.into()),
17095 )));
17096 let mut fake_servers = language_registry.register_fake_lsp(
17097 "Rust",
17098 FakeLspAdapter {
17099 name: language_server_name,
17100 initialization_options: Some(json!({
17101 "testOptionValue": true
17102 })),
17103 initializer: Some(Box::new(move |fake_server| {
17104 let task_restarts = Arc::clone(&closure_restarts);
17105 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
17106 task_restarts.fetch_add(1, atomic::Ordering::Release);
17107 futures::future::ready(Ok(()))
17108 });
17109 })),
17110 ..Default::default()
17111 },
17112 );
17113
17114 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17115 let _buffer = project
17116 .update(cx, |project, cx| {
17117 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
17118 })
17119 .await
17120 .unwrap();
17121 let _fake_server = fake_servers.next().await.unwrap();
17122 update_test_language_settings(cx, |language_settings| {
17123 language_settings.languages.0.insert(
17124 language_name.clone().0,
17125 LanguageSettingsContent {
17126 tab_size: NonZeroU32::new(8),
17127 ..Default::default()
17128 },
17129 );
17130 });
17131 cx.executor().run_until_parked();
17132 assert_eq!(
17133 server_restarts.load(atomic::Ordering::Acquire),
17134 0,
17135 "Should not restart LSP server on an unrelated change"
17136 );
17137
17138 update_test_project_settings(cx, |project_settings| {
17139 project_settings.lsp.insert(
17140 "Some other server name".into(),
17141 LspSettings {
17142 binary: None,
17143 settings: None,
17144 initialization_options: Some(json!({
17145 "some other init value": false
17146 })),
17147 enable_lsp_tasks: false,
17148 fetch: None,
17149 },
17150 );
17151 });
17152 cx.executor().run_until_parked();
17153 assert_eq!(
17154 server_restarts.load(atomic::Ordering::Acquire),
17155 0,
17156 "Should not restart LSP server on an unrelated LSP settings change"
17157 );
17158
17159 update_test_project_settings(cx, |project_settings| {
17160 project_settings.lsp.insert(
17161 language_server_name.into(),
17162 LspSettings {
17163 binary: None,
17164 settings: None,
17165 initialization_options: Some(json!({
17166 "anotherInitValue": false
17167 })),
17168 enable_lsp_tasks: false,
17169 fetch: None,
17170 },
17171 );
17172 });
17173 cx.executor().run_until_parked();
17174 assert_eq!(
17175 server_restarts.load(atomic::Ordering::Acquire),
17176 1,
17177 "Should restart LSP server on a related LSP settings change"
17178 );
17179
17180 update_test_project_settings(cx, |project_settings| {
17181 project_settings.lsp.insert(
17182 language_server_name.into(),
17183 LspSettings {
17184 binary: None,
17185 settings: None,
17186 initialization_options: Some(json!({
17187 "anotherInitValue": false
17188 })),
17189 enable_lsp_tasks: false,
17190 fetch: None,
17191 },
17192 );
17193 });
17194 cx.executor().run_until_parked();
17195 assert_eq!(
17196 server_restarts.load(atomic::Ordering::Acquire),
17197 1,
17198 "Should not restart LSP server on a related LSP settings change that is the same"
17199 );
17200
17201 update_test_project_settings(cx, |project_settings| {
17202 project_settings.lsp.insert(
17203 language_server_name.into(),
17204 LspSettings {
17205 binary: None,
17206 settings: None,
17207 initialization_options: None,
17208 enable_lsp_tasks: false,
17209 fetch: None,
17210 },
17211 );
17212 });
17213 cx.executor().run_until_parked();
17214 assert_eq!(
17215 server_restarts.load(atomic::Ordering::Acquire),
17216 2,
17217 "Should restart LSP server on another related LSP settings change"
17218 );
17219}
17220
17221#[gpui::test]
17222async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
17223 init_test(cx, |_| {});
17224
17225 let mut cx = EditorLspTestContext::new_rust(
17226 lsp::ServerCapabilities {
17227 completion_provider: Some(lsp::CompletionOptions {
17228 trigger_characters: Some(vec![".".to_string()]),
17229 resolve_provider: Some(true),
17230 ..Default::default()
17231 }),
17232 ..Default::default()
17233 },
17234 cx,
17235 )
17236 .await;
17237
17238 cx.set_state("fn main() { let a = 2ˇ; }");
17239 cx.simulate_keystroke(".");
17240 let completion_item = lsp::CompletionItem {
17241 label: "some".into(),
17242 kind: Some(lsp::CompletionItemKind::SNIPPET),
17243 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
17244 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
17245 kind: lsp::MarkupKind::Markdown,
17246 value: "```rust\nSome(2)\n```".to_string(),
17247 })),
17248 deprecated: Some(false),
17249 sort_text: Some("fffffff2".to_string()),
17250 filter_text: Some("some".to_string()),
17251 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
17252 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17253 range: lsp::Range {
17254 start: lsp::Position {
17255 line: 0,
17256 character: 22,
17257 },
17258 end: lsp::Position {
17259 line: 0,
17260 character: 22,
17261 },
17262 },
17263 new_text: "Some(2)".to_string(),
17264 })),
17265 additional_text_edits: Some(vec![lsp::TextEdit {
17266 range: lsp::Range {
17267 start: lsp::Position {
17268 line: 0,
17269 character: 20,
17270 },
17271 end: lsp::Position {
17272 line: 0,
17273 character: 22,
17274 },
17275 },
17276 new_text: "".to_string(),
17277 }]),
17278 ..Default::default()
17279 };
17280
17281 let closure_completion_item = completion_item.clone();
17282 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17283 let task_completion_item = closure_completion_item.clone();
17284 async move {
17285 Ok(Some(lsp::CompletionResponse::Array(vec![
17286 task_completion_item,
17287 ])))
17288 }
17289 });
17290
17291 request.next().await;
17292
17293 cx.condition(|editor, _| editor.context_menu_visible())
17294 .await;
17295 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
17296 editor
17297 .confirm_completion(&ConfirmCompletion::default(), window, cx)
17298 .unwrap()
17299 });
17300 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
17301
17302 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
17303 let task_completion_item = completion_item.clone();
17304 async move { Ok(task_completion_item) }
17305 })
17306 .next()
17307 .await
17308 .unwrap();
17309 apply_additional_edits.await.unwrap();
17310 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
17311}
17312
17313#[gpui::test]
17314async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
17315 init_test(cx, |_| {});
17316
17317 let mut cx = EditorLspTestContext::new_rust(
17318 lsp::ServerCapabilities {
17319 completion_provider: Some(lsp::CompletionOptions {
17320 trigger_characters: Some(vec![".".to_string()]),
17321 resolve_provider: Some(true),
17322 ..Default::default()
17323 }),
17324 ..Default::default()
17325 },
17326 cx,
17327 )
17328 .await;
17329
17330 cx.set_state("fn main() { let a = 2ˇ; }");
17331 cx.simulate_keystroke(".");
17332
17333 let item1 = lsp::CompletionItem {
17334 label: "method id()".to_string(),
17335 filter_text: Some("id".to_string()),
17336 detail: None,
17337 documentation: None,
17338 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17339 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17340 new_text: ".id".to_string(),
17341 })),
17342 ..lsp::CompletionItem::default()
17343 };
17344
17345 let item2 = lsp::CompletionItem {
17346 label: "other".to_string(),
17347 filter_text: Some("other".to_string()),
17348 detail: None,
17349 documentation: None,
17350 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17351 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17352 new_text: ".other".to_string(),
17353 })),
17354 ..lsp::CompletionItem::default()
17355 };
17356
17357 let item1 = item1.clone();
17358 cx.set_request_handler::<lsp::request::Completion, _, _>({
17359 let item1 = item1.clone();
17360 move |_, _, _| {
17361 let item1 = item1.clone();
17362 let item2 = item2.clone();
17363 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
17364 }
17365 })
17366 .next()
17367 .await;
17368
17369 cx.condition(|editor, _| editor.context_menu_visible())
17370 .await;
17371 cx.update_editor(|editor, _, _| {
17372 let context_menu = editor.context_menu.borrow_mut();
17373 let context_menu = context_menu
17374 .as_ref()
17375 .expect("Should have the context menu deployed");
17376 match context_menu {
17377 CodeContextMenu::Completions(completions_menu) => {
17378 let completions = completions_menu.completions.borrow_mut();
17379 assert_eq!(
17380 completions
17381 .iter()
17382 .map(|completion| &completion.label.text)
17383 .collect::<Vec<_>>(),
17384 vec!["method id()", "other"]
17385 )
17386 }
17387 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17388 }
17389 });
17390
17391 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
17392 let item1 = item1.clone();
17393 move |_, item_to_resolve, _| {
17394 let item1 = item1.clone();
17395 async move {
17396 if item1 == item_to_resolve {
17397 Ok(lsp::CompletionItem {
17398 label: "method id()".to_string(),
17399 filter_text: Some("id".to_string()),
17400 detail: Some("Now resolved!".to_string()),
17401 documentation: Some(lsp::Documentation::String("Docs".to_string())),
17402 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17403 range: lsp::Range::new(
17404 lsp::Position::new(0, 22),
17405 lsp::Position::new(0, 22),
17406 ),
17407 new_text: ".id".to_string(),
17408 })),
17409 ..lsp::CompletionItem::default()
17410 })
17411 } else {
17412 Ok(item_to_resolve)
17413 }
17414 }
17415 }
17416 })
17417 .next()
17418 .await
17419 .unwrap();
17420 cx.run_until_parked();
17421
17422 cx.update_editor(|editor, window, cx| {
17423 editor.context_menu_next(&Default::default(), window, cx);
17424 });
17425
17426 cx.update_editor(|editor, _, _| {
17427 let context_menu = editor.context_menu.borrow_mut();
17428 let context_menu = context_menu
17429 .as_ref()
17430 .expect("Should have the context menu deployed");
17431 match context_menu {
17432 CodeContextMenu::Completions(completions_menu) => {
17433 let completions = completions_menu.completions.borrow_mut();
17434 assert_eq!(
17435 completions
17436 .iter()
17437 .map(|completion| &completion.label.text)
17438 .collect::<Vec<_>>(),
17439 vec!["method id() Now resolved!", "other"],
17440 "Should update first completion label, but not second as the filter text did not match."
17441 );
17442 }
17443 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17444 }
17445 });
17446}
17447
17448#[gpui::test]
17449async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
17450 init_test(cx, |_| {});
17451 let mut cx = EditorLspTestContext::new_rust(
17452 lsp::ServerCapabilities {
17453 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
17454 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
17455 completion_provider: Some(lsp::CompletionOptions {
17456 resolve_provider: Some(true),
17457 ..Default::default()
17458 }),
17459 ..Default::default()
17460 },
17461 cx,
17462 )
17463 .await;
17464 cx.set_state(indoc! {"
17465 struct TestStruct {
17466 field: i32
17467 }
17468
17469 fn mainˇ() {
17470 let unused_var = 42;
17471 let test_struct = TestStruct { field: 42 };
17472 }
17473 "});
17474 let symbol_range = cx.lsp_range(indoc! {"
17475 struct TestStruct {
17476 field: i32
17477 }
17478
17479 «fn main»() {
17480 let unused_var = 42;
17481 let test_struct = TestStruct { field: 42 };
17482 }
17483 "});
17484 let mut hover_requests =
17485 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
17486 Ok(Some(lsp::Hover {
17487 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
17488 kind: lsp::MarkupKind::Markdown,
17489 value: "Function documentation".to_string(),
17490 }),
17491 range: Some(symbol_range),
17492 }))
17493 });
17494
17495 // Case 1: Test that code action menu hide hover popover
17496 cx.dispatch_action(Hover);
17497 hover_requests.next().await;
17498 cx.condition(|editor, _| editor.hover_state.visible()).await;
17499 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
17500 move |_, _, _| async move {
17501 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
17502 lsp::CodeAction {
17503 title: "Remove unused variable".to_string(),
17504 kind: Some(CodeActionKind::QUICKFIX),
17505 edit: Some(lsp::WorkspaceEdit {
17506 changes: Some(
17507 [(
17508 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
17509 vec![lsp::TextEdit {
17510 range: lsp::Range::new(
17511 lsp::Position::new(5, 4),
17512 lsp::Position::new(5, 27),
17513 ),
17514 new_text: "".to_string(),
17515 }],
17516 )]
17517 .into_iter()
17518 .collect(),
17519 ),
17520 ..Default::default()
17521 }),
17522 ..Default::default()
17523 },
17524 )]))
17525 },
17526 );
17527 cx.update_editor(|editor, window, cx| {
17528 editor.toggle_code_actions(
17529 &ToggleCodeActions {
17530 deployed_from: None,
17531 quick_launch: false,
17532 },
17533 window,
17534 cx,
17535 );
17536 });
17537 code_action_requests.next().await;
17538 cx.run_until_parked();
17539 cx.condition(|editor, _| editor.context_menu_visible())
17540 .await;
17541 cx.update_editor(|editor, _, _| {
17542 assert!(
17543 !editor.hover_state.visible(),
17544 "Hover popover should be hidden when code action menu is shown"
17545 );
17546 // Hide code actions
17547 editor.context_menu.take();
17548 });
17549
17550 // Case 2: Test that code completions hide hover popover
17551 cx.dispatch_action(Hover);
17552 hover_requests.next().await;
17553 cx.condition(|editor, _| editor.hover_state.visible()).await;
17554 let counter = Arc::new(AtomicUsize::new(0));
17555 let mut completion_requests =
17556 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17557 let counter = counter.clone();
17558 async move {
17559 counter.fetch_add(1, atomic::Ordering::Release);
17560 Ok(Some(lsp::CompletionResponse::Array(vec![
17561 lsp::CompletionItem {
17562 label: "main".into(),
17563 kind: Some(lsp::CompletionItemKind::FUNCTION),
17564 detail: Some("() -> ()".to_string()),
17565 ..Default::default()
17566 },
17567 lsp::CompletionItem {
17568 label: "TestStruct".into(),
17569 kind: Some(lsp::CompletionItemKind::STRUCT),
17570 detail: Some("struct TestStruct".to_string()),
17571 ..Default::default()
17572 },
17573 ])))
17574 }
17575 });
17576 cx.update_editor(|editor, window, cx| {
17577 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
17578 });
17579 completion_requests.next().await;
17580 cx.condition(|editor, _| editor.context_menu_visible())
17581 .await;
17582 cx.update_editor(|editor, _, _| {
17583 assert!(
17584 !editor.hover_state.visible(),
17585 "Hover popover should be hidden when completion menu is shown"
17586 );
17587 });
17588}
17589
17590#[gpui::test]
17591async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
17592 init_test(cx, |_| {});
17593
17594 let mut cx = EditorLspTestContext::new_rust(
17595 lsp::ServerCapabilities {
17596 completion_provider: Some(lsp::CompletionOptions {
17597 trigger_characters: Some(vec![".".to_string()]),
17598 resolve_provider: Some(true),
17599 ..Default::default()
17600 }),
17601 ..Default::default()
17602 },
17603 cx,
17604 )
17605 .await;
17606
17607 cx.set_state("fn main() { let a = 2ˇ; }");
17608 cx.simulate_keystroke(".");
17609
17610 let unresolved_item_1 = lsp::CompletionItem {
17611 label: "id".to_string(),
17612 filter_text: Some("id".to_string()),
17613 detail: None,
17614 documentation: None,
17615 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17616 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17617 new_text: ".id".to_string(),
17618 })),
17619 ..lsp::CompletionItem::default()
17620 };
17621 let resolved_item_1 = lsp::CompletionItem {
17622 additional_text_edits: Some(vec![lsp::TextEdit {
17623 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17624 new_text: "!!".to_string(),
17625 }]),
17626 ..unresolved_item_1.clone()
17627 };
17628 let unresolved_item_2 = lsp::CompletionItem {
17629 label: "other".to_string(),
17630 filter_text: Some("other".to_string()),
17631 detail: None,
17632 documentation: None,
17633 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17634 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17635 new_text: ".other".to_string(),
17636 })),
17637 ..lsp::CompletionItem::default()
17638 };
17639 let resolved_item_2 = lsp::CompletionItem {
17640 additional_text_edits: Some(vec![lsp::TextEdit {
17641 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17642 new_text: "??".to_string(),
17643 }]),
17644 ..unresolved_item_2.clone()
17645 };
17646
17647 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
17648 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
17649 cx.lsp
17650 .server
17651 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
17652 let unresolved_item_1 = unresolved_item_1.clone();
17653 let resolved_item_1 = resolved_item_1.clone();
17654 let unresolved_item_2 = unresolved_item_2.clone();
17655 let resolved_item_2 = resolved_item_2.clone();
17656 let resolve_requests_1 = resolve_requests_1.clone();
17657 let resolve_requests_2 = resolve_requests_2.clone();
17658 move |unresolved_request, _| {
17659 let unresolved_item_1 = unresolved_item_1.clone();
17660 let resolved_item_1 = resolved_item_1.clone();
17661 let unresolved_item_2 = unresolved_item_2.clone();
17662 let resolved_item_2 = resolved_item_2.clone();
17663 let resolve_requests_1 = resolve_requests_1.clone();
17664 let resolve_requests_2 = resolve_requests_2.clone();
17665 async move {
17666 if unresolved_request == unresolved_item_1 {
17667 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
17668 Ok(resolved_item_1.clone())
17669 } else if unresolved_request == unresolved_item_2 {
17670 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
17671 Ok(resolved_item_2.clone())
17672 } else {
17673 panic!("Unexpected completion item {unresolved_request:?}")
17674 }
17675 }
17676 }
17677 })
17678 .detach();
17679
17680 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17681 let unresolved_item_1 = unresolved_item_1.clone();
17682 let unresolved_item_2 = unresolved_item_2.clone();
17683 async move {
17684 Ok(Some(lsp::CompletionResponse::Array(vec![
17685 unresolved_item_1,
17686 unresolved_item_2,
17687 ])))
17688 }
17689 })
17690 .next()
17691 .await;
17692
17693 cx.condition(|editor, _| editor.context_menu_visible())
17694 .await;
17695 cx.update_editor(|editor, _, _| {
17696 let context_menu = editor.context_menu.borrow_mut();
17697 let context_menu = context_menu
17698 .as_ref()
17699 .expect("Should have the context menu deployed");
17700 match context_menu {
17701 CodeContextMenu::Completions(completions_menu) => {
17702 let completions = completions_menu.completions.borrow_mut();
17703 assert_eq!(
17704 completions
17705 .iter()
17706 .map(|completion| &completion.label.text)
17707 .collect::<Vec<_>>(),
17708 vec!["id", "other"]
17709 )
17710 }
17711 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17712 }
17713 });
17714 cx.run_until_parked();
17715
17716 cx.update_editor(|editor, window, cx| {
17717 editor.context_menu_next(&ContextMenuNext, window, cx);
17718 });
17719 cx.run_until_parked();
17720 cx.update_editor(|editor, window, cx| {
17721 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
17722 });
17723 cx.run_until_parked();
17724 cx.update_editor(|editor, window, cx| {
17725 editor.context_menu_next(&ContextMenuNext, window, cx);
17726 });
17727 cx.run_until_parked();
17728 cx.update_editor(|editor, window, cx| {
17729 editor
17730 .compose_completion(&ComposeCompletion::default(), window, cx)
17731 .expect("No task returned")
17732 })
17733 .await
17734 .expect("Completion failed");
17735 cx.run_until_parked();
17736
17737 cx.update_editor(|editor, _, cx| {
17738 assert_eq!(
17739 resolve_requests_1.load(atomic::Ordering::Acquire),
17740 1,
17741 "Should always resolve once despite multiple selections"
17742 );
17743 assert_eq!(
17744 resolve_requests_2.load(atomic::Ordering::Acquire),
17745 1,
17746 "Should always resolve once after multiple selections and applying the completion"
17747 );
17748 assert_eq!(
17749 editor.text(cx),
17750 "fn main() { let a = ??.other; }",
17751 "Should use resolved data when applying the completion"
17752 );
17753 });
17754}
17755
17756#[gpui::test]
17757async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
17758 init_test(cx, |_| {});
17759
17760 let item_0 = lsp::CompletionItem {
17761 label: "abs".into(),
17762 insert_text: Some("abs".into()),
17763 data: Some(json!({ "very": "special"})),
17764 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
17765 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
17766 lsp::InsertReplaceEdit {
17767 new_text: "abs".to_string(),
17768 insert: lsp::Range::default(),
17769 replace: lsp::Range::default(),
17770 },
17771 )),
17772 ..lsp::CompletionItem::default()
17773 };
17774 let items = iter::once(item_0.clone())
17775 .chain((11..51).map(|i| lsp::CompletionItem {
17776 label: format!("item_{}", i),
17777 insert_text: Some(format!("item_{}", i)),
17778 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
17779 ..lsp::CompletionItem::default()
17780 }))
17781 .collect::<Vec<_>>();
17782
17783 let default_commit_characters = vec!["?".to_string()];
17784 let default_data = json!({ "default": "data"});
17785 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
17786 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
17787 let default_edit_range = lsp::Range {
17788 start: lsp::Position {
17789 line: 0,
17790 character: 5,
17791 },
17792 end: lsp::Position {
17793 line: 0,
17794 character: 5,
17795 },
17796 };
17797
17798 let mut cx = EditorLspTestContext::new_rust(
17799 lsp::ServerCapabilities {
17800 completion_provider: Some(lsp::CompletionOptions {
17801 trigger_characters: Some(vec![".".to_string()]),
17802 resolve_provider: Some(true),
17803 ..Default::default()
17804 }),
17805 ..Default::default()
17806 },
17807 cx,
17808 )
17809 .await;
17810
17811 cx.set_state("fn main() { let a = 2ˇ; }");
17812 cx.simulate_keystroke(".");
17813
17814 let completion_data = default_data.clone();
17815 let completion_characters = default_commit_characters.clone();
17816 let completion_items = items.clone();
17817 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17818 let default_data = completion_data.clone();
17819 let default_commit_characters = completion_characters.clone();
17820 let items = completion_items.clone();
17821 async move {
17822 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
17823 items,
17824 item_defaults: Some(lsp::CompletionListItemDefaults {
17825 data: Some(default_data.clone()),
17826 commit_characters: Some(default_commit_characters.clone()),
17827 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
17828 default_edit_range,
17829 )),
17830 insert_text_format: Some(default_insert_text_format),
17831 insert_text_mode: Some(default_insert_text_mode),
17832 }),
17833 ..lsp::CompletionList::default()
17834 })))
17835 }
17836 })
17837 .next()
17838 .await;
17839
17840 let resolved_items = Arc::new(Mutex::new(Vec::new()));
17841 cx.lsp
17842 .server
17843 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
17844 let closure_resolved_items = resolved_items.clone();
17845 move |item_to_resolve, _| {
17846 let closure_resolved_items = closure_resolved_items.clone();
17847 async move {
17848 closure_resolved_items.lock().push(item_to_resolve.clone());
17849 Ok(item_to_resolve)
17850 }
17851 }
17852 })
17853 .detach();
17854
17855 cx.condition(|editor, _| editor.context_menu_visible())
17856 .await;
17857 cx.run_until_parked();
17858 cx.update_editor(|editor, _, _| {
17859 let menu = editor.context_menu.borrow_mut();
17860 match menu.as_ref().expect("should have the completions menu") {
17861 CodeContextMenu::Completions(completions_menu) => {
17862 assert_eq!(
17863 completions_menu
17864 .entries
17865 .borrow()
17866 .iter()
17867 .map(|mat| mat.string.clone())
17868 .collect::<Vec<String>>(),
17869 items
17870 .iter()
17871 .map(|completion| completion.label.clone())
17872 .collect::<Vec<String>>()
17873 );
17874 }
17875 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
17876 }
17877 });
17878 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
17879 // with 4 from the end.
17880 assert_eq!(
17881 *resolved_items.lock(),
17882 [&items[0..16], &items[items.len() - 4..items.len()]]
17883 .concat()
17884 .iter()
17885 .cloned()
17886 .map(|mut item| {
17887 if item.data.is_none() {
17888 item.data = Some(default_data.clone());
17889 }
17890 item
17891 })
17892 .collect::<Vec<lsp::CompletionItem>>(),
17893 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
17894 );
17895 resolved_items.lock().clear();
17896
17897 cx.update_editor(|editor, window, cx| {
17898 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
17899 });
17900 cx.run_until_parked();
17901 // Completions that have already been resolved are skipped.
17902 assert_eq!(
17903 *resolved_items.lock(),
17904 items[items.len() - 17..items.len() - 4]
17905 .iter()
17906 .cloned()
17907 .map(|mut item| {
17908 if item.data.is_none() {
17909 item.data = Some(default_data.clone());
17910 }
17911 item
17912 })
17913 .collect::<Vec<lsp::CompletionItem>>()
17914 );
17915 resolved_items.lock().clear();
17916}
17917
17918#[gpui::test]
17919async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
17920 init_test(cx, |_| {});
17921
17922 let mut cx = EditorLspTestContext::new(
17923 Language::new(
17924 LanguageConfig {
17925 matcher: LanguageMatcher {
17926 path_suffixes: vec!["jsx".into()],
17927 ..Default::default()
17928 },
17929 overrides: [(
17930 "element".into(),
17931 LanguageConfigOverride {
17932 completion_query_characters: Override::Set(['-'].into_iter().collect()),
17933 ..Default::default()
17934 },
17935 )]
17936 .into_iter()
17937 .collect(),
17938 ..Default::default()
17939 },
17940 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
17941 )
17942 .with_override_query("(jsx_self_closing_element) @element")
17943 .unwrap(),
17944 lsp::ServerCapabilities {
17945 completion_provider: Some(lsp::CompletionOptions {
17946 trigger_characters: Some(vec![":".to_string()]),
17947 ..Default::default()
17948 }),
17949 ..Default::default()
17950 },
17951 cx,
17952 )
17953 .await;
17954
17955 cx.lsp
17956 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
17957 Ok(Some(lsp::CompletionResponse::Array(vec![
17958 lsp::CompletionItem {
17959 label: "bg-blue".into(),
17960 ..Default::default()
17961 },
17962 lsp::CompletionItem {
17963 label: "bg-red".into(),
17964 ..Default::default()
17965 },
17966 lsp::CompletionItem {
17967 label: "bg-yellow".into(),
17968 ..Default::default()
17969 },
17970 ])))
17971 });
17972
17973 cx.set_state(r#"<p class="bgˇ" />"#);
17974
17975 // Trigger completion when typing a dash, because the dash is an extra
17976 // word character in the 'element' scope, which contains the cursor.
17977 cx.simulate_keystroke("-");
17978 cx.executor().run_until_parked();
17979 cx.update_editor(|editor, _, _| {
17980 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17981 {
17982 assert_eq!(
17983 completion_menu_entries(menu),
17984 &["bg-blue", "bg-red", "bg-yellow"]
17985 );
17986 } else {
17987 panic!("expected completion menu to be open");
17988 }
17989 });
17990
17991 cx.simulate_keystroke("l");
17992 cx.executor().run_until_parked();
17993 cx.update_editor(|editor, _, _| {
17994 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17995 {
17996 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
17997 } else {
17998 panic!("expected completion menu to be open");
17999 }
18000 });
18001
18002 // When filtering completions, consider the character after the '-' to
18003 // be the start of a subword.
18004 cx.set_state(r#"<p class="yelˇ" />"#);
18005 cx.simulate_keystroke("l");
18006 cx.executor().run_until_parked();
18007 cx.update_editor(|editor, _, _| {
18008 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18009 {
18010 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
18011 } else {
18012 panic!("expected completion menu to be open");
18013 }
18014 });
18015}
18016
18017fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
18018 let entries = menu.entries.borrow();
18019 entries.iter().map(|mat| mat.string.clone()).collect()
18020}
18021
18022#[gpui::test]
18023async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
18024 init_test(cx, |settings| {
18025 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
18026 Formatter::Prettier,
18027 )))
18028 });
18029
18030 let fs = FakeFs::new(cx.executor());
18031 fs.insert_file(path!("/file.ts"), Default::default()).await;
18032
18033 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
18034 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18035
18036 language_registry.add(Arc::new(Language::new(
18037 LanguageConfig {
18038 name: "TypeScript".into(),
18039 matcher: LanguageMatcher {
18040 path_suffixes: vec!["ts".to_string()],
18041 ..Default::default()
18042 },
18043 ..Default::default()
18044 },
18045 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
18046 )));
18047 update_test_language_settings(cx, |settings| {
18048 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
18049 });
18050
18051 let test_plugin = "test_plugin";
18052 let _ = language_registry.register_fake_lsp(
18053 "TypeScript",
18054 FakeLspAdapter {
18055 prettier_plugins: vec![test_plugin],
18056 ..Default::default()
18057 },
18058 );
18059
18060 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
18061 let buffer = project
18062 .update(cx, |project, cx| {
18063 project.open_local_buffer(path!("/file.ts"), cx)
18064 })
18065 .await
18066 .unwrap();
18067
18068 let buffer_text = "one\ntwo\nthree\n";
18069 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
18070 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
18071 editor.update_in(cx, |editor, window, cx| {
18072 editor.set_text(buffer_text, window, cx)
18073 });
18074
18075 editor
18076 .update_in(cx, |editor, window, cx| {
18077 editor.perform_format(
18078 project.clone(),
18079 FormatTrigger::Manual,
18080 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18081 window,
18082 cx,
18083 )
18084 })
18085 .unwrap()
18086 .await;
18087 assert_eq!(
18088 editor.update(cx, |editor, cx| editor.text(cx)),
18089 buffer_text.to_string() + prettier_format_suffix,
18090 "Test prettier formatting was not applied to the original buffer text",
18091 );
18092
18093 update_test_language_settings(cx, |settings| {
18094 settings.defaults.formatter = Some(SelectedFormatter::Auto)
18095 });
18096 let format = editor.update_in(cx, |editor, window, cx| {
18097 editor.perform_format(
18098 project.clone(),
18099 FormatTrigger::Manual,
18100 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18101 window,
18102 cx,
18103 )
18104 });
18105 format.await.unwrap();
18106 assert_eq!(
18107 editor.update(cx, |editor, cx| editor.text(cx)),
18108 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
18109 "Autoformatting (via test prettier) was not applied to the original buffer text",
18110 );
18111}
18112
18113#[gpui::test]
18114async fn test_addition_reverts(cx: &mut TestAppContext) {
18115 init_test(cx, |_| {});
18116 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18117 let base_text = indoc! {r#"
18118 struct Row;
18119 struct Row1;
18120 struct Row2;
18121
18122 struct Row4;
18123 struct Row5;
18124 struct Row6;
18125
18126 struct Row8;
18127 struct Row9;
18128 struct Row10;"#};
18129
18130 // When addition hunks are not adjacent to carets, no hunk revert is performed
18131 assert_hunk_revert(
18132 indoc! {r#"struct Row;
18133 struct Row1;
18134 struct Row1.1;
18135 struct Row1.2;
18136 struct Row2;ˇ
18137
18138 struct Row4;
18139 struct Row5;
18140 struct Row6;
18141
18142 struct Row8;
18143 ˇstruct Row9;
18144 struct Row9.1;
18145 struct Row9.2;
18146 struct Row9.3;
18147 struct Row10;"#},
18148 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18149 indoc! {r#"struct Row;
18150 struct Row1;
18151 struct Row1.1;
18152 struct Row1.2;
18153 struct Row2;ˇ
18154
18155 struct Row4;
18156 struct Row5;
18157 struct Row6;
18158
18159 struct Row8;
18160 ˇstruct Row9;
18161 struct Row9.1;
18162 struct Row9.2;
18163 struct Row9.3;
18164 struct Row10;"#},
18165 base_text,
18166 &mut cx,
18167 );
18168 // Same for selections
18169 assert_hunk_revert(
18170 indoc! {r#"struct Row;
18171 struct Row1;
18172 struct Row2;
18173 struct Row2.1;
18174 struct Row2.2;
18175 «ˇ
18176 struct Row4;
18177 struct» Row5;
18178 «struct Row6;
18179 ˇ»
18180 struct Row9.1;
18181 struct Row9.2;
18182 struct Row9.3;
18183 struct Row8;
18184 struct Row9;
18185 struct Row10;"#},
18186 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18187 indoc! {r#"struct Row;
18188 struct Row1;
18189 struct Row2;
18190 struct Row2.1;
18191 struct Row2.2;
18192 «ˇ
18193 struct Row4;
18194 struct» Row5;
18195 «struct Row6;
18196 ˇ»
18197 struct Row9.1;
18198 struct Row9.2;
18199 struct Row9.3;
18200 struct Row8;
18201 struct Row9;
18202 struct Row10;"#},
18203 base_text,
18204 &mut cx,
18205 );
18206
18207 // When carets and selections intersect the addition hunks, those are reverted.
18208 // Adjacent carets got merged.
18209 assert_hunk_revert(
18210 indoc! {r#"struct Row;
18211 ˇ// something on the top
18212 struct Row1;
18213 struct Row2;
18214 struct Roˇw3.1;
18215 struct Row2.2;
18216 struct Row2.3;ˇ
18217
18218 struct Row4;
18219 struct ˇRow5.1;
18220 struct Row5.2;
18221 struct «Rowˇ»5.3;
18222 struct Row5;
18223 struct Row6;
18224 ˇ
18225 struct Row9.1;
18226 struct «Rowˇ»9.2;
18227 struct «ˇRow»9.3;
18228 struct Row8;
18229 struct Row9;
18230 «ˇ// something on bottom»
18231 struct Row10;"#},
18232 vec![
18233 DiffHunkStatusKind::Added,
18234 DiffHunkStatusKind::Added,
18235 DiffHunkStatusKind::Added,
18236 DiffHunkStatusKind::Added,
18237 DiffHunkStatusKind::Added,
18238 ],
18239 indoc! {r#"struct Row;
18240 ˇstruct Row1;
18241 struct Row2;
18242 ˇ
18243 struct Row4;
18244 ˇstruct Row5;
18245 struct Row6;
18246 ˇ
18247 ˇstruct Row8;
18248 struct Row9;
18249 ˇstruct Row10;"#},
18250 base_text,
18251 &mut cx,
18252 );
18253}
18254
18255#[gpui::test]
18256async fn test_modification_reverts(cx: &mut TestAppContext) {
18257 init_test(cx, |_| {});
18258 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18259 let base_text = indoc! {r#"
18260 struct Row;
18261 struct Row1;
18262 struct Row2;
18263
18264 struct Row4;
18265 struct Row5;
18266 struct Row6;
18267
18268 struct Row8;
18269 struct Row9;
18270 struct Row10;"#};
18271
18272 // Modification hunks behave the same as the addition ones.
18273 assert_hunk_revert(
18274 indoc! {r#"struct Row;
18275 struct Row1;
18276 struct Row33;
18277 ˇ
18278 struct Row4;
18279 struct Row5;
18280 struct Row6;
18281 ˇ
18282 struct Row99;
18283 struct Row9;
18284 struct Row10;"#},
18285 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18286 indoc! {r#"struct Row;
18287 struct Row1;
18288 struct Row33;
18289 ˇ
18290 struct Row4;
18291 struct Row5;
18292 struct Row6;
18293 ˇ
18294 struct Row99;
18295 struct Row9;
18296 struct Row10;"#},
18297 base_text,
18298 &mut cx,
18299 );
18300 assert_hunk_revert(
18301 indoc! {r#"struct Row;
18302 struct Row1;
18303 struct Row33;
18304 «ˇ
18305 struct Row4;
18306 struct» Row5;
18307 «struct Row6;
18308 ˇ»
18309 struct Row99;
18310 struct Row9;
18311 struct Row10;"#},
18312 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18313 indoc! {r#"struct Row;
18314 struct Row1;
18315 struct Row33;
18316 «ˇ
18317 struct Row4;
18318 struct» Row5;
18319 «struct Row6;
18320 ˇ»
18321 struct Row99;
18322 struct Row9;
18323 struct Row10;"#},
18324 base_text,
18325 &mut cx,
18326 );
18327
18328 assert_hunk_revert(
18329 indoc! {r#"ˇstruct Row1.1;
18330 struct Row1;
18331 «ˇstr»uct Row22;
18332
18333 struct ˇRow44;
18334 struct Row5;
18335 struct «Rˇ»ow66;ˇ
18336
18337 «struˇ»ct Row88;
18338 struct Row9;
18339 struct Row1011;ˇ"#},
18340 vec![
18341 DiffHunkStatusKind::Modified,
18342 DiffHunkStatusKind::Modified,
18343 DiffHunkStatusKind::Modified,
18344 DiffHunkStatusKind::Modified,
18345 DiffHunkStatusKind::Modified,
18346 DiffHunkStatusKind::Modified,
18347 ],
18348 indoc! {r#"struct Row;
18349 ˇstruct Row1;
18350 struct Row2;
18351 ˇ
18352 struct Row4;
18353 ˇstruct Row5;
18354 struct Row6;
18355 ˇ
18356 struct Row8;
18357 ˇstruct Row9;
18358 struct Row10;ˇ"#},
18359 base_text,
18360 &mut cx,
18361 );
18362}
18363
18364#[gpui::test]
18365async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
18366 init_test(cx, |_| {});
18367 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18368 let base_text = indoc! {r#"
18369 one
18370
18371 two
18372 three
18373 "#};
18374
18375 cx.set_head_text(base_text);
18376 cx.set_state("\nˇ\n");
18377 cx.executor().run_until_parked();
18378 cx.update_editor(|editor, _window, cx| {
18379 editor.expand_selected_diff_hunks(cx);
18380 });
18381 cx.executor().run_until_parked();
18382 cx.update_editor(|editor, window, cx| {
18383 editor.backspace(&Default::default(), window, cx);
18384 });
18385 cx.run_until_parked();
18386 cx.assert_state_with_diff(
18387 indoc! {r#"
18388
18389 - two
18390 - threeˇ
18391 +
18392 "#}
18393 .to_string(),
18394 );
18395}
18396
18397#[gpui::test]
18398async fn test_deletion_reverts(cx: &mut TestAppContext) {
18399 init_test(cx, |_| {});
18400 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18401 let base_text = indoc! {r#"struct Row;
18402struct Row1;
18403struct Row2;
18404
18405struct Row4;
18406struct Row5;
18407struct Row6;
18408
18409struct Row8;
18410struct Row9;
18411struct Row10;"#};
18412
18413 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
18414 assert_hunk_revert(
18415 indoc! {r#"struct Row;
18416 struct Row2;
18417
18418 ˇstruct Row4;
18419 struct Row5;
18420 struct Row6;
18421 ˇ
18422 struct Row8;
18423 struct Row10;"#},
18424 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18425 indoc! {r#"struct Row;
18426 struct Row2;
18427
18428 ˇstruct Row4;
18429 struct Row5;
18430 struct Row6;
18431 ˇ
18432 struct Row8;
18433 struct Row10;"#},
18434 base_text,
18435 &mut cx,
18436 );
18437 assert_hunk_revert(
18438 indoc! {r#"struct Row;
18439 struct Row2;
18440
18441 «ˇstruct Row4;
18442 struct» Row5;
18443 «struct Row6;
18444 ˇ»
18445 struct Row8;
18446 struct Row10;"#},
18447 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18448 indoc! {r#"struct Row;
18449 struct Row2;
18450
18451 «ˇstruct Row4;
18452 struct» Row5;
18453 «struct Row6;
18454 ˇ»
18455 struct Row8;
18456 struct Row10;"#},
18457 base_text,
18458 &mut cx,
18459 );
18460
18461 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
18462 assert_hunk_revert(
18463 indoc! {r#"struct Row;
18464 ˇstruct Row2;
18465
18466 struct Row4;
18467 struct Row5;
18468 struct Row6;
18469
18470 struct Row8;ˇ
18471 struct Row10;"#},
18472 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18473 indoc! {r#"struct Row;
18474 struct Row1;
18475 ˇstruct Row2;
18476
18477 struct Row4;
18478 struct Row5;
18479 struct Row6;
18480
18481 struct Row8;ˇ
18482 struct Row9;
18483 struct Row10;"#},
18484 base_text,
18485 &mut cx,
18486 );
18487 assert_hunk_revert(
18488 indoc! {r#"struct Row;
18489 struct Row2«ˇ;
18490 struct Row4;
18491 struct» Row5;
18492 «struct Row6;
18493
18494 struct Row8;ˇ»
18495 struct Row10;"#},
18496 vec![
18497 DiffHunkStatusKind::Deleted,
18498 DiffHunkStatusKind::Deleted,
18499 DiffHunkStatusKind::Deleted,
18500 ],
18501 indoc! {r#"struct Row;
18502 struct Row1;
18503 struct Row2«ˇ;
18504
18505 struct Row4;
18506 struct» Row5;
18507 «struct Row6;
18508
18509 struct Row8;ˇ»
18510 struct Row9;
18511 struct Row10;"#},
18512 base_text,
18513 &mut cx,
18514 );
18515}
18516
18517#[gpui::test]
18518async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
18519 init_test(cx, |_| {});
18520
18521 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
18522 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
18523 let base_text_3 =
18524 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
18525
18526 let text_1 = edit_first_char_of_every_line(base_text_1);
18527 let text_2 = edit_first_char_of_every_line(base_text_2);
18528 let text_3 = edit_first_char_of_every_line(base_text_3);
18529
18530 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
18531 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
18532 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
18533
18534 let multibuffer = cx.new(|cx| {
18535 let mut multibuffer = MultiBuffer::new(ReadWrite);
18536 multibuffer.push_excerpts(
18537 buffer_1.clone(),
18538 [
18539 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18540 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18541 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18542 ],
18543 cx,
18544 );
18545 multibuffer.push_excerpts(
18546 buffer_2.clone(),
18547 [
18548 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18549 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18550 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18551 ],
18552 cx,
18553 );
18554 multibuffer.push_excerpts(
18555 buffer_3.clone(),
18556 [
18557 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18558 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18559 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18560 ],
18561 cx,
18562 );
18563 multibuffer
18564 });
18565
18566 let fs = FakeFs::new(cx.executor());
18567 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
18568 let (editor, cx) = cx
18569 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
18570 editor.update_in(cx, |editor, _window, cx| {
18571 for (buffer, diff_base) in [
18572 (buffer_1.clone(), base_text_1),
18573 (buffer_2.clone(), base_text_2),
18574 (buffer_3.clone(), base_text_3),
18575 ] {
18576 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
18577 editor
18578 .buffer
18579 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
18580 }
18581 });
18582 cx.executor().run_until_parked();
18583
18584 editor.update_in(cx, |editor, window, cx| {
18585 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}");
18586 editor.select_all(&SelectAll, window, cx);
18587 editor.git_restore(&Default::default(), window, cx);
18588 });
18589 cx.executor().run_until_parked();
18590
18591 // When all ranges are selected, all buffer hunks are reverted.
18592 editor.update(cx, |editor, cx| {
18593 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");
18594 });
18595 buffer_1.update(cx, |buffer, _| {
18596 assert_eq!(buffer.text(), base_text_1);
18597 });
18598 buffer_2.update(cx, |buffer, _| {
18599 assert_eq!(buffer.text(), base_text_2);
18600 });
18601 buffer_3.update(cx, |buffer, _| {
18602 assert_eq!(buffer.text(), base_text_3);
18603 });
18604
18605 editor.update_in(cx, |editor, window, cx| {
18606 editor.undo(&Default::default(), window, cx);
18607 });
18608
18609 editor.update_in(cx, |editor, window, cx| {
18610 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18611 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
18612 });
18613 editor.git_restore(&Default::default(), window, cx);
18614 });
18615
18616 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
18617 // but not affect buffer_2 and its related excerpts.
18618 editor.update(cx, |editor, cx| {
18619 assert_eq!(
18620 editor.text(cx),
18621 "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}"
18622 );
18623 });
18624 buffer_1.update(cx, |buffer, _| {
18625 assert_eq!(buffer.text(), base_text_1);
18626 });
18627 buffer_2.update(cx, |buffer, _| {
18628 assert_eq!(
18629 buffer.text(),
18630 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
18631 );
18632 });
18633 buffer_3.update(cx, |buffer, _| {
18634 assert_eq!(
18635 buffer.text(),
18636 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
18637 );
18638 });
18639
18640 fn edit_first_char_of_every_line(text: &str) -> String {
18641 text.split('\n')
18642 .map(|line| format!("X{}", &line[1..]))
18643 .collect::<Vec<_>>()
18644 .join("\n")
18645 }
18646}
18647
18648#[gpui::test]
18649async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
18650 init_test(cx, |_| {});
18651
18652 let cols = 4;
18653 let rows = 10;
18654 let sample_text_1 = sample_text(rows, cols, 'a');
18655 assert_eq!(
18656 sample_text_1,
18657 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
18658 );
18659 let sample_text_2 = sample_text(rows, cols, 'l');
18660 assert_eq!(
18661 sample_text_2,
18662 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
18663 );
18664 let sample_text_3 = sample_text(rows, cols, 'v');
18665 assert_eq!(
18666 sample_text_3,
18667 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
18668 );
18669
18670 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
18671 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
18672 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
18673
18674 let multi_buffer = cx.new(|cx| {
18675 let mut multibuffer = MultiBuffer::new(ReadWrite);
18676 multibuffer.push_excerpts(
18677 buffer_1.clone(),
18678 [
18679 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18680 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18681 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18682 ],
18683 cx,
18684 );
18685 multibuffer.push_excerpts(
18686 buffer_2.clone(),
18687 [
18688 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18689 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18690 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18691 ],
18692 cx,
18693 );
18694 multibuffer.push_excerpts(
18695 buffer_3.clone(),
18696 [
18697 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18698 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18699 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18700 ],
18701 cx,
18702 );
18703 multibuffer
18704 });
18705
18706 let fs = FakeFs::new(cx.executor());
18707 fs.insert_tree(
18708 "/a",
18709 json!({
18710 "main.rs": sample_text_1,
18711 "other.rs": sample_text_2,
18712 "lib.rs": sample_text_3,
18713 }),
18714 )
18715 .await;
18716 let project = Project::test(fs, ["/a".as_ref()], cx).await;
18717 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18718 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18719 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18720 Editor::new(
18721 EditorMode::full(),
18722 multi_buffer,
18723 Some(project.clone()),
18724 window,
18725 cx,
18726 )
18727 });
18728 let multibuffer_item_id = workspace
18729 .update(cx, |workspace, window, cx| {
18730 assert!(
18731 workspace.active_item(cx).is_none(),
18732 "active item should be None before the first item is added"
18733 );
18734 workspace.add_item_to_active_pane(
18735 Box::new(multi_buffer_editor.clone()),
18736 None,
18737 true,
18738 window,
18739 cx,
18740 );
18741 let active_item = workspace
18742 .active_item(cx)
18743 .expect("should have an active item after adding the multi buffer");
18744 assert!(
18745 !active_item.is_singleton(cx),
18746 "A multi buffer was expected to active after adding"
18747 );
18748 active_item.item_id()
18749 })
18750 .unwrap();
18751 cx.executor().run_until_parked();
18752
18753 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18754 editor.change_selections(
18755 SelectionEffects::scroll(Autoscroll::Next),
18756 window,
18757 cx,
18758 |s| s.select_ranges(Some(1..2)),
18759 );
18760 editor.open_excerpts(&OpenExcerpts, window, cx);
18761 });
18762 cx.executor().run_until_parked();
18763 let first_item_id = workspace
18764 .update(cx, |workspace, window, cx| {
18765 let active_item = workspace
18766 .active_item(cx)
18767 .expect("should have an active item after navigating into the 1st buffer");
18768 let first_item_id = active_item.item_id();
18769 assert_ne!(
18770 first_item_id, multibuffer_item_id,
18771 "Should navigate into the 1st buffer and activate it"
18772 );
18773 assert!(
18774 active_item.is_singleton(cx),
18775 "New active item should be a singleton buffer"
18776 );
18777 assert_eq!(
18778 active_item
18779 .act_as::<Editor>(cx)
18780 .expect("should have navigated into an editor for the 1st buffer")
18781 .read(cx)
18782 .text(cx),
18783 sample_text_1
18784 );
18785
18786 workspace
18787 .go_back(workspace.active_pane().downgrade(), window, cx)
18788 .detach_and_log_err(cx);
18789
18790 first_item_id
18791 })
18792 .unwrap();
18793 cx.executor().run_until_parked();
18794 workspace
18795 .update(cx, |workspace, _, cx| {
18796 let active_item = workspace
18797 .active_item(cx)
18798 .expect("should have an active item after navigating back");
18799 assert_eq!(
18800 active_item.item_id(),
18801 multibuffer_item_id,
18802 "Should navigate back to the multi buffer"
18803 );
18804 assert!(!active_item.is_singleton(cx));
18805 })
18806 .unwrap();
18807
18808 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18809 editor.change_selections(
18810 SelectionEffects::scroll(Autoscroll::Next),
18811 window,
18812 cx,
18813 |s| s.select_ranges(Some(39..40)),
18814 );
18815 editor.open_excerpts(&OpenExcerpts, window, cx);
18816 });
18817 cx.executor().run_until_parked();
18818 let second_item_id = workspace
18819 .update(cx, |workspace, window, cx| {
18820 let active_item = workspace
18821 .active_item(cx)
18822 .expect("should have an active item after navigating into the 2nd buffer");
18823 let second_item_id = active_item.item_id();
18824 assert_ne!(
18825 second_item_id, multibuffer_item_id,
18826 "Should navigate away from the multibuffer"
18827 );
18828 assert_ne!(
18829 second_item_id, first_item_id,
18830 "Should navigate into the 2nd buffer and activate it"
18831 );
18832 assert!(
18833 active_item.is_singleton(cx),
18834 "New active item should be a singleton buffer"
18835 );
18836 assert_eq!(
18837 active_item
18838 .act_as::<Editor>(cx)
18839 .expect("should have navigated into an editor")
18840 .read(cx)
18841 .text(cx),
18842 sample_text_2
18843 );
18844
18845 workspace
18846 .go_back(workspace.active_pane().downgrade(), window, cx)
18847 .detach_and_log_err(cx);
18848
18849 second_item_id
18850 })
18851 .unwrap();
18852 cx.executor().run_until_parked();
18853 workspace
18854 .update(cx, |workspace, _, cx| {
18855 let active_item = workspace
18856 .active_item(cx)
18857 .expect("should have an active item after navigating back from the 2nd buffer");
18858 assert_eq!(
18859 active_item.item_id(),
18860 multibuffer_item_id,
18861 "Should navigate back from the 2nd buffer to the multi buffer"
18862 );
18863 assert!(!active_item.is_singleton(cx));
18864 })
18865 .unwrap();
18866
18867 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18868 editor.change_selections(
18869 SelectionEffects::scroll(Autoscroll::Next),
18870 window,
18871 cx,
18872 |s| s.select_ranges(Some(70..70)),
18873 );
18874 editor.open_excerpts(&OpenExcerpts, window, cx);
18875 });
18876 cx.executor().run_until_parked();
18877 workspace
18878 .update(cx, |workspace, window, cx| {
18879 let active_item = workspace
18880 .active_item(cx)
18881 .expect("should have an active item after navigating into the 3rd buffer");
18882 let third_item_id = active_item.item_id();
18883 assert_ne!(
18884 third_item_id, multibuffer_item_id,
18885 "Should navigate into the 3rd buffer and activate it"
18886 );
18887 assert_ne!(third_item_id, first_item_id);
18888 assert_ne!(third_item_id, second_item_id);
18889 assert!(
18890 active_item.is_singleton(cx),
18891 "New active item should be a singleton buffer"
18892 );
18893 assert_eq!(
18894 active_item
18895 .act_as::<Editor>(cx)
18896 .expect("should have navigated into an editor")
18897 .read(cx)
18898 .text(cx),
18899 sample_text_3
18900 );
18901
18902 workspace
18903 .go_back(workspace.active_pane().downgrade(), window, cx)
18904 .detach_and_log_err(cx);
18905 })
18906 .unwrap();
18907 cx.executor().run_until_parked();
18908 workspace
18909 .update(cx, |workspace, _, cx| {
18910 let active_item = workspace
18911 .active_item(cx)
18912 .expect("should have an active item after navigating back from the 3rd buffer");
18913 assert_eq!(
18914 active_item.item_id(),
18915 multibuffer_item_id,
18916 "Should navigate back from the 3rd buffer to the multi buffer"
18917 );
18918 assert!(!active_item.is_singleton(cx));
18919 })
18920 .unwrap();
18921}
18922
18923#[gpui::test]
18924async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
18925 init_test(cx, |_| {});
18926
18927 let mut cx = EditorTestContext::new(cx).await;
18928
18929 let diff_base = r#"
18930 use some::mod;
18931
18932 const A: u32 = 42;
18933
18934 fn main() {
18935 println!("hello");
18936
18937 println!("world");
18938 }
18939 "#
18940 .unindent();
18941
18942 cx.set_state(
18943 &r#"
18944 use some::modified;
18945
18946 ˇ
18947 fn main() {
18948 println!("hello there");
18949
18950 println!("around the");
18951 println!("world");
18952 }
18953 "#
18954 .unindent(),
18955 );
18956
18957 cx.set_head_text(&diff_base);
18958 executor.run_until_parked();
18959
18960 cx.update_editor(|editor, window, cx| {
18961 editor.go_to_next_hunk(&GoToHunk, window, cx);
18962 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
18963 });
18964 executor.run_until_parked();
18965 cx.assert_state_with_diff(
18966 r#"
18967 use some::modified;
18968
18969
18970 fn main() {
18971 - println!("hello");
18972 + ˇ println!("hello there");
18973
18974 println!("around the");
18975 println!("world");
18976 }
18977 "#
18978 .unindent(),
18979 );
18980
18981 cx.update_editor(|editor, window, cx| {
18982 for _ in 0..2 {
18983 editor.go_to_next_hunk(&GoToHunk, window, cx);
18984 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
18985 }
18986 });
18987 executor.run_until_parked();
18988 cx.assert_state_with_diff(
18989 r#"
18990 - use some::mod;
18991 + ˇuse some::modified;
18992
18993
18994 fn main() {
18995 - println!("hello");
18996 + println!("hello there");
18997
18998 + println!("around the");
18999 println!("world");
19000 }
19001 "#
19002 .unindent(),
19003 );
19004
19005 cx.update_editor(|editor, window, cx| {
19006 editor.go_to_next_hunk(&GoToHunk, window, cx);
19007 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19008 });
19009 executor.run_until_parked();
19010 cx.assert_state_with_diff(
19011 r#"
19012 - use some::mod;
19013 + use some::modified;
19014
19015 - const A: u32 = 42;
19016 ˇ
19017 fn main() {
19018 - println!("hello");
19019 + println!("hello there");
19020
19021 + println!("around the");
19022 println!("world");
19023 }
19024 "#
19025 .unindent(),
19026 );
19027
19028 cx.update_editor(|editor, window, cx| {
19029 editor.cancel(&Cancel, window, cx);
19030 });
19031
19032 cx.assert_state_with_diff(
19033 r#"
19034 use some::modified;
19035
19036 ˇ
19037 fn main() {
19038 println!("hello there");
19039
19040 println!("around the");
19041 println!("world");
19042 }
19043 "#
19044 .unindent(),
19045 );
19046}
19047
19048#[gpui::test]
19049async fn test_diff_base_change_with_expanded_diff_hunks(
19050 executor: BackgroundExecutor,
19051 cx: &mut TestAppContext,
19052) {
19053 init_test(cx, |_| {});
19054
19055 let mut cx = EditorTestContext::new(cx).await;
19056
19057 let diff_base = r#"
19058 use some::mod1;
19059 use some::mod2;
19060
19061 const A: u32 = 42;
19062 const B: u32 = 42;
19063 const C: u32 = 42;
19064
19065 fn main() {
19066 println!("hello");
19067
19068 println!("world");
19069 }
19070 "#
19071 .unindent();
19072
19073 cx.set_state(
19074 &r#"
19075 use some::mod2;
19076
19077 const A: u32 = 42;
19078 const C: u32 = 42;
19079
19080 fn main(ˇ) {
19081 //println!("hello");
19082
19083 println!("world");
19084 //
19085 //
19086 }
19087 "#
19088 .unindent(),
19089 );
19090
19091 cx.set_head_text(&diff_base);
19092 executor.run_until_parked();
19093
19094 cx.update_editor(|editor, window, cx| {
19095 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19096 });
19097 executor.run_until_parked();
19098 cx.assert_state_with_diff(
19099 r#"
19100 - use some::mod1;
19101 use some::mod2;
19102
19103 const A: u32 = 42;
19104 - const B: u32 = 42;
19105 const C: u32 = 42;
19106
19107 fn main(ˇ) {
19108 - println!("hello");
19109 + //println!("hello");
19110
19111 println!("world");
19112 + //
19113 + //
19114 }
19115 "#
19116 .unindent(),
19117 );
19118
19119 cx.set_head_text("new diff base!");
19120 executor.run_until_parked();
19121 cx.assert_state_with_diff(
19122 r#"
19123 - new diff base!
19124 + use some::mod2;
19125 +
19126 + const A: u32 = 42;
19127 + const C: u32 = 42;
19128 +
19129 + fn main(ˇ) {
19130 + //println!("hello");
19131 +
19132 + println!("world");
19133 + //
19134 + //
19135 + }
19136 "#
19137 .unindent(),
19138 );
19139}
19140
19141#[gpui::test]
19142async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
19143 init_test(cx, |_| {});
19144
19145 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19146 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19147 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19148 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19149 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
19150 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
19151
19152 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
19153 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
19154 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
19155
19156 let multi_buffer = cx.new(|cx| {
19157 let mut multibuffer = MultiBuffer::new(ReadWrite);
19158 multibuffer.push_excerpts(
19159 buffer_1.clone(),
19160 [
19161 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19162 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19163 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19164 ],
19165 cx,
19166 );
19167 multibuffer.push_excerpts(
19168 buffer_2.clone(),
19169 [
19170 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19171 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19172 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19173 ],
19174 cx,
19175 );
19176 multibuffer.push_excerpts(
19177 buffer_3.clone(),
19178 [
19179 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19180 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19181 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19182 ],
19183 cx,
19184 );
19185 multibuffer
19186 });
19187
19188 let editor =
19189 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19190 editor
19191 .update(cx, |editor, _window, cx| {
19192 for (buffer, diff_base) in [
19193 (buffer_1.clone(), file_1_old),
19194 (buffer_2.clone(), file_2_old),
19195 (buffer_3.clone(), file_3_old),
19196 ] {
19197 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
19198 editor
19199 .buffer
19200 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
19201 }
19202 })
19203 .unwrap();
19204
19205 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19206 cx.run_until_parked();
19207
19208 cx.assert_editor_state(
19209 &"
19210 ˇaaa
19211 ccc
19212 ddd
19213
19214 ggg
19215 hhh
19216
19217
19218 lll
19219 mmm
19220 NNN
19221
19222 qqq
19223 rrr
19224
19225 uuu
19226 111
19227 222
19228 333
19229
19230 666
19231 777
19232
19233 000
19234 !!!"
19235 .unindent(),
19236 );
19237
19238 cx.update_editor(|editor, window, cx| {
19239 editor.select_all(&SelectAll, window, cx);
19240 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19241 });
19242 cx.executor().run_until_parked();
19243
19244 cx.assert_state_with_diff(
19245 "
19246 «aaa
19247 - bbb
19248 ccc
19249 ddd
19250
19251 ggg
19252 hhh
19253
19254
19255 lll
19256 mmm
19257 - nnn
19258 + NNN
19259
19260 qqq
19261 rrr
19262
19263 uuu
19264 111
19265 222
19266 333
19267
19268 + 666
19269 777
19270
19271 000
19272 !!!ˇ»"
19273 .unindent(),
19274 );
19275}
19276
19277#[gpui::test]
19278async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
19279 init_test(cx, |_| {});
19280
19281 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
19282 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
19283
19284 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
19285 let multi_buffer = cx.new(|cx| {
19286 let mut multibuffer = MultiBuffer::new(ReadWrite);
19287 multibuffer.push_excerpts(
19288 buffer.clone(),
19289 [
19290 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
19291 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
19292 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
19293 ],
19294 cx,
19295 );
19296 multibuffer
19297 });
19298
19299 let editor =
19300 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19301 editor
19302 .update(cx, |editor, _window, cx| {
19303 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
19304 editor
19305 .buffer
19306 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
19307 })
19308 .unwrap();
19309
19310 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19311 cx.run_until_parked();
19312
19313 cx.update_editor(|editor, window, cx| {
19314 editor.expand_all_diff_hunks(&Default::default(), window, cx)
19315 });
19316 cx.executor().run_until_parked();
19317
19318 // When the start of a hunk coincides with the start of its excerpt,
19319 // the hunk is expanded. When the start of a hunk is earlier than
19320 // the start of its excerpt, the hunk is not expanded.
19321 cx.assert_state_with_diff(
19322 "
19323 ˇaaa
19324 - bbb
19325 + BBB
19326
19327 - ddd
19328 - eee
19329 + DDD
19330 + EEE
19331 fff
19332
19333 iii
19334 "
19335 .unindent(),
19336 );
19337}
19338
19339#[gpui::test]
19340async fn test_edits_around_expanded_insertion_hunks(
19341 executor: BackgroundExecutor,
19342 cx: &mut TestAppContext,
19343) {
19344 init_test(cx, |_| {});
19345
19346 let mut cx = EditorTestContext::new(cx).await;
19347
19348 let diff_base = r#"
19349 use some::mod1;
19350 use some::mod2;
19351
19352 const A: u32 = 42;
19353
19354 fn main() {
19355 println!("hello");
19356
19357 println!("world");
19358 }
19359 "#
19360 .unindent();
19361 executor.run_until_parked();
19362 cx.set_state(
19363 &r#"
19364 use some::mod1;
19365 use some::mod2;
19366
19367 const A: u32 = 42;
19368 const B: u32 = 42;
19369 const C: u32 = 42;
19370 ˇ
19371
19372 fn main() {
19373 println!("hello");
19374
19375 println!("world");
19376 }
19377 "#
19378 .unindent(),
19379 );
19380
19381 cx.set_head_text(&diff_base);
19382 executor.run_until_parked();
19383
19384 cx.update_editor(|editor, window, cx| {
19385 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19386 });
19387 executor.run_until_parked();
19388
19389 cx.assert_state_with_diff(
19390 r#"
19391 use some::mod1;
19392 use some::mod2;
19393
19394 const A: u32 = 42;
19395 + const B: u32 = 42;
19396 + const C: u32 = 42;
19397 + ˇ
19398
19399 fn main() {
19400 println!("hello");
19401
19402 println!("world");
19403 }
19404 "#
19405 .unindent(),
19406 );
19407
19408 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
19409 executor.run_until_parked();
19410
19411 cx.assert_state_with_diff(
19412 r#"
19413 use some::mod1;
19414 use some::mod2;
19415
19416 const A: u32 = 42;
19417 + const B: u32 = 42;
19418 + const C: u32 = 42;
19419 + const D: u32 = 42;
19420 + ˇ
19421
19422 fn main() {
19423 println!("hello");
19424
19425 println!("world");
19426 }
19427 "#
19428 .unindent(),
19429 );
19430
19431 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
19432 executor.run_until_parked();
19433
19434 cx.assert_state_with_diff(
19435 r#"
19436 use some::mod1;
19437 use some::mod2;
19438
19439 const A: u32 = 42;
19440 + const B: u32 = 42;
19441 + const C: u32 = 42;
19442 + const D: u32 = 42;
19443 + const E: u32 = 42;
19444 + ˇ
19445
19446 fn main() {
19447 println!("hello");
19448
19449 println!("world");
19450 }
19451 "#
19452 .unindent(),
19453 );
19454
19455 cx.update_editor(|editor, window, cx| {
19456 editor.delete_line(&DeleteLine, window, cx);
19457 });
19458 executor.run_until_parked();
19459
19460 cx.assert_state_with_diff(
19461 r#"
19462 use some::mod1;
19463 use some::mod2;
19464
19465 const A: u32 = 42;
19466 + const B: u32 = 42;
19467 + const C: u32 = 42;
19468 + const D: u32 = 42;
19469 + const E: u32 = 42;
19470 ˇ
19471 fn main() {
19472 println!("hello");
19473
19474 println!("world");
19475 }
19476 "#
19477 .unindent(),
19478 );
19479
19480 cx.update_editor(|editor, window, cx| {
19481 editor.move_up(&MoveUp, window, cx);
19482 editor.delete_line(&DeleteLine, window, cx);
19483 editor.move_up(&MoveUp, window, cx);
19484 editor.delete_line(&DeleteLine, window, cx);
19485 editor.move_up(&MoveUp, window, cx);
19486 editor.delete_line(&DeleteLine, window, cx);
19487 });
19488 executor.run_until_parked();
19489 cx.assert_state_with_diff(
19490 r#"
19491 use some::mod1;
19492 use some::mod2;
19493
19494 const A: u32 = 42;
19495 + const B: u32 = 42;
19496 ˇ
19497 fn main() {
19498 println!("hello");
19499
19500 println!("world");
19501 }
19502 "#
19503 .unindent(),
19504 );
19505
19506 cx.update_editor(|editor, window, cx| {
19507 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
19508 editor.delete_line(&DeleteLine, window, cx);
19509 });
19510 executor.run_until_parked();
19511 cx.assert_state_with_diff(
19512 r#"
19513 ˇ
19514 fn main() {
19515 println!("hello");
19516
19517 println!("world");
19518 }
19519 "#
19520 .unindent(),
19521 );
19522}
19523
19524#[gpui::test]
19525async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
19526 init_test(cx, |_| {});
19527
19528 let mut cx = EditorTestContext::new(cx).await;
19529 cx.set_head_text(indoc! { "
19530 one
19531 two
19532 three
19533 four
19534 five
19535 "
19536 });
19537 cx.set_state(indoc! { "
19538 one
19539 ˇthree
19540 five
19541 "});
19542 cx.run_until_parked();
19543 cx.update_editor(|editor, window, cx| {
19544 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19545 });
19546 cx.assert_state_with_diff(
19547 indoc! { "
19548 one
19549 - two
19550 ˇthree
19551 - four
19552 five
19553 "}
19554 .to_string(),
19555 );
19556 cx.update_editor(|editor, window, cx| {
19557 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19558 });
19559
19560 cx.assert_state_with_diff(
19561 indoc! { "
19562 one
19563 ˇthree
19564 five
19565 "}
19566 .to_string(),
19567 );
19568
19569 cx.set_state(indoc! { "
19570 one
19571 ˇTWO
19572 three
19573 four
19574 five
19575 "});
19576 cx.run_until_parked();
19577 cx.update_editor(|editor, window, cx| {
19578 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19579 });
19580
19581 cx.assert_state_with_diff(
19582 indoc! { "
19583 one
19584 - two
19585 + ˇTWO
19586 three
19587 four
19588 five
19589 "}
19590 .to_string(),
19591 );
19592 cx.update_editor(|editor, window, cx| {
19593 editor.move_up(&Default::default(), window, cx);
19594 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19595 });
19596 cx.assert_state_with_diff(
19597 indoc! { "
19598 one
19599 ˇTWO
19600 three
19601 four
19602 five
19603 "}
19604 .to_string(),
19605 );
19606}
19607
19608#[gpui::test]
19609async fn test_edits_around_expanded_deletion_hunks(
19610 executor: BackgroundExecutor,
19611 cx: &mut TestAppContext,
19612) {
19613 init_test(cx, |_| {});
19614
19615 let mut cx = EditorTestContext::new(cx).await;
19616
19617 let diff_base = r#"
19618 use some::mod1;
19619 use some::mod2;
19620
19621 const A: u32 = 42;
19622 const B: u32 = 42;
19623 const C: u32 = 42;
19624
19625
19626 fn main() {
19627 println!("hello");
19628
19629 println!("world");
19630 }
19631 "#
19632 .unindent();
19633 executor.run_until_parked();
19634 cx.set_state(
19635 &r#"
19636 use some::mod1;
19637 use some::mod2;
19638
19639 ˇconst B: u32 = 42;
19640 const C: u32 = 42;
19641
19642
19643 fn main() {
19644 println!("hello");
19645
19646 println!("world");
19647 }
19648 "#
19649 .unindent(),
19650 );
19651
19652 cx.set_head_text(&diff_base);
19653 executor.run_until_parked();
19654
19655 cx.update_editor(|editor, window, cx| {
19656 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19657 });
19658 executor.run_until_parked();
19659
19660 cx.assert_state_with_diff(
19661 r#"
19662 use some::mod1;
19663 use some::mod2;
19664
19665 - const A: u32 = 42;
19666 ˇconst B: u32 = 42;
19667 const C: u32 = 42;
19668
19669
19670 fn main() {
19671 println!("hello");
19672
19673 println!("world");
19674 }
19675 "#
19676 .unindent(),
19677 );
19678
19679 cx.update_editor(|editor, window, cx| {
19680 editor.delete_line(&DeleteLine, window, cx);
19681 });
19682 executor.run_until_parked();
19683 cx.assert_state_with_diff(
19684 r#"
19685 use some::mod1;
19686 use some::mod2;
19687
19688 - const A: u32 = 42;
19689 - const B: u32 = 42;
19690 ˇconst C: u32 = 42;
19691
19692
19693 fn main() {
19694 println!("hello");
19695
19696 println!("world");
19697 }
19698 "#
19699 .unindent(),
19700 );
19701
19702 cx.update_editor(|editor, window, cx| {
19703 editor.delete_line(&DeleteLine, window, cx);
19704 });
19705 executor.run_until_parked();
19706 cx.assert_state_with_diff(
19707 r#"
19708 use some::mod1;
19709 use some::mod2;
19710
19711 - const A: u32 = 42;
19712 - const B: u32 = 42;
19713 - const C: u32 = 42;
19714 ˇ
19715
19716 fn main() {
19717 println!("hello");
19718
19719 println!("world");
19720 }
19721 "#
19722 .unindent(),
19723 );
19724
19725 cx.update_editor(|editor, window, cx| {
19726 editor.handle_input("replacement", window, cx);
19727 });
19728 executor.run_until_parked();
19729 cx.assert_state_with_diff(
19730 r#"
19731 use some::mod1;
19732 use some::mod2;
19733
19734 - const A: u32 = 42;
19735 - const B: u32 = 42;
19736 - const C: u32 = 42;
19737 -
19738 + replacementˇ
19739
19740 fn main() {
19741 println!("hello");
19742
19743 println!("world");
19744 }
19745 "#
19746 .unindent(),
19747 );
19748}
19749
19750#[gpui::test]
19751async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19752 init_test(cx, |_| {});
19753
19754 let mut cx = EditorTestContext::new(cx).await;
19755
19756 let base_text = r#"
19757 one
19758 two
19759 three
19760 four
19761 five
19762 "#
19763 .unindent();
19764 executor.run_until_parked();
19765 cx.set_state(
19766 &r#"
19767 one
19768 two
19769 fˇour
19770 five
19771 "#
19772 .unindent(),
19773 );
19774
19775 cx.set_head_text(&base_text);
19776 executor.run_until_parked();
19777
19778 cx.update_editor(|editor, window, cx| {
19779 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19780 });
19781 executor.run_until_parked();
19782
19783 cx.assert_state_with_diff(
19784 r#"
19785 one
19786 two
19787 - three
19788 fˇour
19789 five
19790 "#
19791 .unindent(),
19792 );
19793
19794 cx.update_editor(|editor, window, cx| {
19795 editor.backspace(&Backspace, window, cx);
19796 editor.backspace(&Backspace, window, cx);
19797 });
19798 executor.run_until_parked();
19799 cx.assert_state_with_diff(
19800 r#"
19801 one
19802 two
19803 - threeˇ
19804 - four
19805 + our
19806 five
19807 "#
19808 .unindent(),
19809 );
19810}
19811
19812#[gpui::test]
19813async fn test_edit_after_expanded_modification_hunk(
19814 executor: BackgroundExecutor,
19815 cx: &mut TestAppContext,
19816) {
19817 init_test(cx, |_| {});
19818
19819 let mut cx = EditorTestContext::new(cx).await;
19820
19821 let diff_base = r#"
19822 use some::mod1;
19823 use some::mod2;
19824
19825 const A: u32 = 42;
19826 const B: u32 = 42;
19827 const C: u32 = 42;
19828 const D: u32 = 42;
19829
19830
19831 fn main() {
19832 println!("hello");
19833
19834 println!("world");
19835 }"#
19836 .unindent();
19837
19838 cx.set_state(
19839 &r#"
19840 use some::mod1;
19841 use some::mod2;
19842
19843 const A: u32 = 42;
19844 const B: u32 = 42;
19845 const C: u32 = 43ˇ
19846 const D: u32 = 42;
19847
19848
19849 fn main() {
19850 println!("hello");
19851
19852 println!("world");
19853 }"#
19854 .unindent(),
19855 );
19856
19857 cx.set_head_text(&diff_base);
19858 executor.run_until_parked();
19859 cx.update_editor(|editor, window, cx| {
19860 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19861 });
19862 executor.run_until_parked();
19863
19864 cx.assert_state_with_diff(
19865 r#"
19866 use some::mod1;
19867 use some::mod2;
19868
19869 const A: u32 = 42;
19870 const B: u32 = 42;
19871 - const C: u32 = 42;
19872 + const C: u32 = 43ˇ
19873 const D: u32 = 42;
19874
19875
19876 fn main() {
19877 println!("hello");
19878
19879 println!("world");
19880 }"#
19881 .unindent(),
19882 );
19883
19884 cx.update_editor(|editor, window, cx| {
19885 editor.handle_input("\nnew_line\n", window, cx);
19886 });
19887 executor.run_until_parked();
19888
19889 cx.assert_state_with_diff(
19890 r#"
19891 use some::mod1;
19892 use some::mod2;
19893
19894 const A: u32 = 42;
19895 const B: u32 = 42;
19896 - const C: u32 = 42;
19897 + const C: u32 = 43
19898 + new_line
19899 + ˇ
19900 const D: u32 = 42;
19901
19902
19903 fn main() {
19904 println!("hello");
19905
19906 println!("world");
19907 }"#
19908 .unindent(),
19909 );
19910}
19911
19912#[gpui::test]
19913async fn test_stage_and_unstage_added_file_hunk(
19914 executor: BackgroundExecutor,
19915 cx: &mut TestAppContext,
19916) {
19917 init_test(cx, |_| {});
19918
19919 let mut cx = EditorTestContext::new(cx).await;
19920 cx.update_editor(|editor, _, cx| {
19921 editor.set_expand_all_diff_hunks(cx);
19922 });
19923
19924 let working_copy = r#"
19925 ˇfn main() {
19926 println!("hello, world!");
19927 }
19928 "#
19929 .unindent();
19930
19931 cx.set_state(&working_copy);
19932 executor.run_until_parked();
19933
19934 cx.assert_state_with_diff(
19935 r#"
19936 + ˇfn main() {
19937 + println!("hello, world!");
19938 + }
19939 "#
19940 .unindent(),
19941 );
19942 cx.assert_index_text(None);
19943
19944 cx.update_editor(|editor, window, cx| {
19945 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19946 });
19947 executor.run_until_parked();
19948 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
19949 cx.assert_state_with_diff(
19950 r#"
19951 + ˇfn main() {
19952 + println!("hello, world!");
19953 + }
19954 "#
19955 .unindent(),
19956 );
19957
19958 cx.update_editor(|editor, window, cx| {
19959 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19960 });
19961 executor.run_until_parked();
19962 cx.assert_index_text(None);
19963}
19964
19965async fn setup_indent_guides_editor(
19966 text: &str,
19967 cx: &mut TestAppContext,
19968) -> (BufferId, EditorTestContext) {
19969 init_test(cx, |_| {});
19970
19971 let mut cx = EditorTestContext::new(cx).await;
19972
19973 let buffer_id = cx.update_editor(|editor, window, cx| {
19974 editor.set_text(text, window, cx);
19975 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
19976
19977 buffer_ids[0]
19978 });
19979
19980 (buffer_id, cx)
19981}
19982
19983fn assert_indent_guides(
19984 range: Range<u32>,
19985 expected: Vec<IndentGuide>,
19986 active_indices: Option<Vec<usize>>,
19987 cx: &mut EditorTestContext,
19988) {
19989 let indent_guides = cx.update_editor(|editor, window, cx| {
19990 let snapshot = editor.snapshot(window, cx).display_snapshot;
19991 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
19992 editor,
19993 MultiBufferRow(range.start)..MultiBufferRow(range.end),
19994 true,
19995 &snapshot,
19996 cx,
19997 );
19998
19999 indent_guides.sort_by(|a, b| {
20000 a.depth.cmp(&b.depth).then(
20001 a.start_row
20002 .cmp(&b.start_row)
20003 .then(a.end_row.cmp(&b.end_row)),
20004 )
20005 });
20006 indent_guides
20007 });
20008
20009 if let Some(expected) = active_indices {
20010 let active_indices = cx.update_editor(|editor, window, cx| {
20011 let snapshot = editor.snapshot(window, cx).display_snapshot;
20012 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
20013 });
20014
20015 assert_eq!(
20016 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
20017 expected,
20018 "Active indent guide indices do not match"
20019 );
20020 }
20021
20022 assert_eq!(indent_guides, expected, "Indent guides do not match");
20023}
20024
20025fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
20026 IndentGuide {
20027 buffer_id,
20028 start_row: MultiBufferRow(start_row),
20029 end_row: MultiBufferRow(end_row),
20030 depth,
20031 tab_size: 4,
20032 settings: IndentGuideSettings {
20033 enabled: true,
20034 line_width: 1,
20035 active_line_width: 1,
20036 coloring: IndentGuideColoring::default(),
20037 background_coloring: IndentGuideBackgroundColoring::default(),
20038 },
20039 }
20040}
20041
20042#[gpui::test]
20043async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
20044 let (buffer_id, mut cx) = setup_indent_guides_editor(
20045 &"
20046 fn main() {
20047 let a = 1;
20048 }"
20049 .unindent(),
20050 cx,
20051 )
20052 .await;
20053
20054 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20055}
20056
20057#[gpui::test]
20058async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
20059 let (buffer_id, mut cx) = setup_indent_guides_editor(
20060 &"
20061 fn main() {
20062 let a = 1;
20063 let b = 2;
20064 }"
20065 .unindent(),
20066 cx,
20067 )
20068 .await;
20069
20070 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
20071}
20072
20073#[gpui::test]
20074async fn test_indent_guide_nested(cx: &mut TestAppContext) {
20075 let (buffer_id, mut cx) = setup_indent_guides_editor(
20076 &"
20077 fn main() {
20078 let a = 1;
20079 if a == 3 {
20080 let b = 2;
20081 } else {
20082 let c = 3;
20083 }
20084 }"
20085 .unindent(),
20086 cx,
20087 )
20088 .await;
20089
20090 assert_indent_guides(
20091 0..8,
20092 vec![
20093 indent_guide(buffer_id, 1, 6, 0),
20094 indent_guide(buffer_id, 3, 3, 1),
20095 indent_guide(buffer_id, 5, 5, 1),
20096 ],
20097 None,
20098 &mut cx,
20099 );
20100}
20101
20102#[gpui::test]
20103async fn test_indent_guide_tab(cx: &mut TestAppContext) {
20104 let (buffer_id, mut cx) = setup_indent_guides_editor(
20105 &"
20106 fn main() {
20107 let a = 1;
20108 let b = 2;
20109 let c = 3;
20110 }"
20111 .unindent(),
20112 cx,
20113 )
20114 .await;
20115
20116 assert_indent_guides(
20117 0..5,
20118 vec![
20119 indent_guide(buffer_id, 1, 3, 0),
20120 indent_guide(buffer_id, 2, 2, 1),
20121 ],
20122 None,
20123 &mut cx,
20124 );
20125}
20126
20127#[gpui::test]
20128async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
20129 let (buffer_id, mut cx) = setup_indent_guides_editor(
20130 &"
20131 fn main() {
20132 let a = 1;
20133
20134 let c = 3;
20135 }"
20136 .unindent(),
20137 cx,
20138 )
20139 .await;
20140
20141 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
20142}
20143
20144#[gpui::test]
20145async fn test_indent_guide_complex(cx: &mut TestAppContext) {
20146 let (buffer_id, mut cx) = setup_indent_guides_editor(
20147 &"
20148 fn main() {
20149 let a = 1;
20150
20151 let c = 3;
20152
20153 if a == 3 {
20154 let b = 2;
20155 } else {
20156 let c = 3;
20157 }
20158 }"
20159 .unindent(),
20160 cx,
20161 )
20162 .await;
20163
20164 assert_indent_guides(
20165 0..11,
20166 vec![
20167 indent_guide(buffer_id, 1, 9, 0),
20168 indent_guide(buffer_id, 6, 6, 1),
20169 indent_guide(buffer_id, 8, 8, 1),
20170 ],
20171 None,
20172 &mut cx,
20173 );
20174}
20175
20176#[gpui::test]
20177async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
20178 let (buffer_id, mut cx) = setup_indent_guides_editor(
20179 &"
20180 fn main() {
20181 let a = 1;
20182
20183 let c = 3;
20184
20185 if a == 3 {
20186 let b = 2;
20187 } else {
20188 let c = 3;
20189 }
20190 }"
20191 .unindent(),
20192 cx,
20193 )
20194 .await;
20195
20196 assert_indent_guides(
20197 1..11,
20198 vec![
20199 indent_guide(buffer_id, 1, 9, 0),
20200 indent_guide(buffer_id, 6, 6, 1),
20201 indent_guide(buffer_id, 8, 8, 1),
20202 ],
20203 None,
20204 &mut cx,
20205 );
20206}
20207
20208#[gpui::test]
20209async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
20210 let (buffer_id, mut cx) = setup_indent_guides_editor(
20211 &"
20212 fn main() {
20213 let a = 1;
20214
20215 let c = 3;
20216
20217 if a == 3 {
20218 let b = 2;
20219 } else {
20220 let c = 3;
20221 }
20222 }"
20223 .unindent(),
20224 cx,
20225 )
20226 .await;
20227
20228 assert_indent_guides(
20229 1..10,
20230 vec![
20231 indent_guide(buffer_id, 1, 9, 0),
20232 indent_guide(buffer_id, 6, 6, 1),
20233 indent_guide(buffer_id, 8, 8, 1),
20234 ],
20235 None,
20236 &mut cx,
20237 );
20238}
20239
20240#[gpui::test]
20241async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
20242 let (buffer_id, mut cx) = setup_indent_guides_editor(
20243 &"
20244 fn main() {
20245 if a {
20246 b(
20247 c,
20248 d,
20249 )
20250 } else {
20251 e(
20252 f
20253 )
20254 }
20255 }"
20256 .unindent(),
20257 cx,
20258 )
20259 .await;
20260
20261 assert_indent_guides(
20262 0..11,
20263 vec![
20264 indent_guide(buffer_id, 1, 10, 0),
20265 indent_guide(buffer_id, 2, 5, 1),
20266 indent_guide(buffer_id, 7, 9, 1),
20267 indent_guide(buffer_id, 3, 4, 2),
20268 indent_guide(buffer_id, 8, 8, 2),
20269 ],
20270 None,
20271 &mut cx,
20272 );
20273
20274 cx.update_editor(|editor, window, cx| {
20275 editor.fold_at(MultiBufferRow(2), window, cx);
20276 assert_eq!(
20277 editor.display_text(cx),
20278 "
20279 fn main() {
20280 if a {
20281 b(⋯
20282 )
20283 } else {
20284 e(
20285 f
20286 )
20287 }
20288 }"
20289 .unindent()
20290 );
20291 });
20292
20293 assert_indent_guides(
20294 0..11,
20295 vec![
20296 indent_guide(buffer_id, 1, 10, 0),
20297 indent_guide(buffer_id, 2, 5, 1),
20298 indent_guide(buffer_id, 7, 9, 1),
20299 indent_guide(buffer_id, 8, 8, 2),
20300 ],
20301 None,
20302 &mut cx,
20303 );
20304}
20305
20306#[gpui::test]
20307async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
20308 let (buffer_id, mut cx) = setup_indent_guides_editor(
20309 &"
20310 block1
20311 block2
20312 block3
20313 block4
20314 block2
20315 block1
20316 block1"
20317 .unindent(),
20318 cx,
20319 )
20320 .await;
20321
20322 assert_indent_guides(
20323 1..10,
20324 vec![
20325 indent_guide(buffer_id, 1, 4, 0),
20326 indent_guide(buffer_id, 2, 3, 1),
20327 indent_guide(buffer_id, 3, 3, 2),
20328 ],
20329 None,
20330 &mut cx,
20331 );
20332}
20333
20334#[gpui::test]
20335async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
20336 let (buffer_id, mut cx) = setup_indent_guides_editor(
20337 &"
20338 block1
20339 block2
20340 block3
20341
20342 block1
20343 block1"
20344 .unindent(),
20345 cx,
20346 )
20347 .await;
20348
20349 assert_indent_guides(
20350 0..6,
20351 vec![
20352 indent_guide(buffer_id, 1, 2, 0),
20353 indent_guide(buffer_id, 2, 2, 1),
20354 ],
20355 None,
20356 &mut cx,
20357 );
20358}
20359
20360#[gpui::test]
20361async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
20362 let (buffer_id, mut cx) = setup_indent_guides_editor(
20363 &"
20364 function component() {
20365 \treturn (
20366 \t\t\t
20367 \t\t<div>
20368 \t\t\t<abc></abc>
20369 \t\t</div>
20370 \t)
20371 }"
20372 .unindent(),
20373 cx,
20374 )
20375 .await;
20376
20377 assert_indent_guides(
20378 0..8,
20379 vec![
20380 indent_guide(buffer_id, 1, 6, 0),
20381 indent_guide(buffer_id, 2, 5, 1),
20382 indent_guide(buffer_id, 4, 4, 2),
20383 ],
20384 None,
20385 &mut cx,
20386 );
20387}
20388
20389#[gpui::test]
20390async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
20391 let (buffer_id, mut cx) = setup_indent_guides_editor(
20392 &"
20393 function component() {
20394 \treturn (
20395 \t
20396 \t\t<div>
20397 \t\t\t<abc></abc>
20398 \t\t</div>
20399 \t)
20400 }"
20401 .unindent(),
20402 cx,
20403 )
20404 .await;
20405
20406 assert_indent_guides(
20407 0..8,
20408 vec![
20409 indent_guide(buffer_id, 1, 6, 0),
20410 indent_guide(buffer_id, 2, 5, 1),
20411 indent_guide(buffer_id, 4, 4, 2),
20412 ],
20413 None,
20414 &mut cx,
20415 );
20416}
20417
20418#[gpui::test]
20419async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
20420 let (buffer_id, mut cx) = setup_indent_guides_editor(
20421 &"
20422 block1
20423
20424
20425
20426 block2
20427 "
20428 .unindent(),
20429 cx,
20430 )
20431 .await;
20432
20433 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20434}
20435
20436#[gpui::test]
20437async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
20438 let (buffer_id, mut cx) = setup_indent_guides_editor(
20439 &"
20440 def a:
20441 \tb = 3
20442 \tif True:
20443 \t\tc = 4
20444 \t\td = 5
20445 \tprint(b)
20446 "
20447 .unindent(),
20448 cx,
20449 )
20450 .await;
20451
20452 assert_indent_guides(
20453 0..6,
20454 vec![
20455 indent_guide(buffer_id, 1, 5, 0),
20456 indent_guide(buffer_id, 3, 4, 1),
20457 ],
20458 None,
20459 &mut cx,
20460 );
20461}
20462
20463#[gpui::test]
20464async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
20465 let (buffer_id, mut cx) = setup_indent_guides_editor(
20466 &"
20467 fn main() {
20468 let a = 1;
20469 }"
20470 .unindent(),
20471 cx,
20472 )
20473 .await;
20474
20475 cx.update_editor(|editor, window, cx| {
20476 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20477 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20478 });
20479 });
20480
20481 assert_indent_guides(
20482 0..3,
20483 vec![indent_guide(buffer_id, 1, 1, 0)],
20484 Some(vec![0]),
20485 &mut cx,
20486 );
20487}
20488
20489#[gpui::test]
20490async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
20491 let (buffer_id, mut cx) = setup_indent_guides_editor(
20492 &"
20493 fn main() {
20494 if 1 == 2 {
20495 let a = 1;
20496 }
20497 }"
20498 .unindent(),
20499 cx,
20500 )
20501 .await;
20502
20503 cx.update_editor(|editor, window, cx| {
20504 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20505 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20506 });
20507 });
20508
20509 assert_indent_guides(
20510 0..4,
20511 vec![
20512 indent_guide(buffer_id, 1, 3, 0),
20513 indent_guide(buffer_id, 2, 2, 1),
20514 ],
20515 Some(vec![1]),
20516 &mut cx,
20517 );
20518
20519 cx.update_editor(|editor, window, cx| {
20520 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20521 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20522 });
20523 });
20524
20525 assert_indent_guides(
20526 0..4,
20527 vec![
20528 indent_guide(buffer_id, 1, 3, 0),
20529 indent_guide(buffer_id, 2, 2, 1),
20530 ],
20531 Some(vec![1]),
20532 &mut cx,
20533 );
20534
20535 cx.update_editor(|editor, window, cx| {
20536 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20537 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
20538 });
20539 });
20540
20541 assert_indent_guides(
20542 0..4,
20543 vec![
20544 indent_guide(buffer_id, 1, 3, 0),
20545 indent_guide(buffer_id, 2, 2, 1),
20546 ],
20547 Some(vec![0]),
20548 &mut cx,
20549 );
20550}
20551
20552#[gpui::test]
20553async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
20554 let (buffer_id, mut cx) = setup_indent_guides_editor(
20555 &"
20556 fn main() {
20557 let a = 1;
20558
20559 let b = 2;
20560 }"
20561 .unindent(),
20562 cx,
20563 )
20564 .await;
20565
20566 cx.update_editor(|editor, window, cx| {
20567 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20568 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20569 });
20570 });
20571
20572 assert_indent_guides(
20573 0..5,
20574 vec![indent_guide(buffer_id, 1, 3, 0)],
20575 Some(vec![0]),
20576 &mut cx,
20577 );
20578}
20579
20580#[gpui::test]
20581async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
20582 let (buffer_id, mut cx) = setup_indent_guides_editor(
20583 &"
20584 def m:
20585 a = 1
20586 pass"
20587 .unindent(),
20588 cx,
20589 )
20590 .await;
20591
20592 cx.update_editor(|editor, window, cx| {
20593 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20594 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20595 });
20596 });
20597
20598 assert_indent_guides(
20599 0..3,
20600 vec![indent_guide(buffer_id, 1, 2, 0)],
20601 Some(vec![0]),
20602 &mut cx,
20603 );
20604}
20605
20606#[gpui::test]
20607async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
20608 init_test(cx, |_| {});
20609 let mut cx = EditorTestContext::new(cx).await;
20610 let text = indoc! {
20611 "
20612 impl A {
20613 fn b() {
20614 0;
20615 3;
20616 5;
20617 6;
20618 7;
20619 }
20620 }
20621 "
20622 };
20623 let base_text = indoc! {
20624 "
20625 impl A {
20626 fn b() {
20627 0;
20628 1;
20629 2;
20630 3;
20631 4;
20632 }
20633 fn c() {
20634 5;
20635 6;
20636 7;
20637 }
20638 }
20639 "
20640 };
20641
20642 cx.update_editor(|editor, window, cx| {
20643 editor.set_text(text, window, cx);
20644
20645 editor.buffer().update(cx, |multibuffer, cx| {
20646 let buffer = multibuffer.as_singleton().unwrap();
20647 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
20648
20649 multibuffer.set_all_diff_hunks_expanded(cx);
20650 multibuffer.add_diff(diff, cx);
20651
20652 buffer.read(cx).remote_id()
20653 })
20654 });
20655 cx.run_until_parked();
20656
20657 cx.assert_state_with_diff(
20658 indoc! { "
20659 impl A {
20660 fn b() {
20661 0;
20662 - 1;
20663 - 2;
20664 3;
20665 - 4;
20666 - }
20667 - fn c() {
20668 5;
20669 6;
20670 7;
20671 }
20672 }
20673 ˇ"
20674 }
20675 .to_string(),
20676 );
20677
20678 let mut actual_guides = cx.update_editor(|editor, window, cx| {
20679 editor
20680 .snapshot(window, cx)
20681 .buffer_snapshot
20682 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
20683 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
20684 .collect::<Vec<_>>()
20685 });
20686 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
20687 assert_eq!(
20688 actual_guides,
20689 vec![
20690 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
20691 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
20692 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
20693 ]
20694 );
20695}
20696
20697#[gpui::test]
20698async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20699 init_test(cx, |_| {});
20700 let mut cx = EditorTestContext::new(cx).await;
20701
20702 let diff_base = r#"
20703 a
20704 b
20705 c
20706 "#
20707 .unindent();
20708
20709 cx.set_state(
20710 &r#"
20711 ˇA
20712 b
20713 C
20714 "#
20715 .unindent(),
20716 );
20717 cx.set_head_text(&diff_base);
20718 cx.update_editor(|editor, window, cx| {
20719 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20720 });
20721 executor.run_until_parked();
20722
20723 let both_hunks_expanded = r#"
20724 - a
20725 + ˇA
20726 b
20727 - c
20728 + C
20729 "#
20730 .unindent();
20731
20732 cx.assert_state_with_diff(both_hunks_expanded.clone());
20733
20734 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20735 let snapshot = editor.snapshot(window, cx);
20736 let hunks = editor
20737 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20738 .collect::<Vec<_>>();
20739 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20740 let buffer_id = hunks[0].buffer_id;
20741 hunks
20742 .into_iter()
20743 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20744 .collect::<Vec<_>>()
20745 });
20746 assert_eq!(hunk_ranges.len(), 2);
20747
20748 cx.update_editor(|editor, _, cx| {
20749 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20750 });
20751 executor.run_until_parked();
20752
20753 let second_hunk_expanded = r#"
20754 ˇA
20755 b
20756 - c
20757 + C
20758 "#
20759 .unindent();
20760
20761 cx.assert_state_with_diff(second_hunk_expanded);
20762
20763 cx.update_editor(|editor, _, cx| {
20764 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20765 });
20766 executor.run_until_parked();
20767
20768 cx.assert_state_with_diff(both_hunks_expanded.clone());
20769
20770 cx.update_editor(|editor, _, cx| {
20771 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20772 });
20773 executor.run_until_parked();
20774
20775 let first_hunk_expanded = r#"
20776 - a
20777 + ˇA
20778 b
20779 C
20780 "#
20781 .unindent();
20782
20783 cx.assert_state_with_diff(first_hunk_expanded);
20784
20785 cx.update_editor(|editor, _, cx| {
20786 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20787 });
20788 executor.run_until_parked();
20789
20790 cx.assert_state_with_diff(both_hunks_expanded);
20791
20792 cx.set_state(
20793 &r#"
20794 ˇA
20795 b
20796 "#
20797 .unindent(),
20798 );
20799 cx.run_until_parked();
20800
20801 // TODO this cursor position seems bad
20802 cx.assert_state_with_diff(
20803 r#"
20804 - ˇa
20805 + A
20806 b
20807 "#
20808 .unindent(),
20809 );
20810
20811 cx.update_editor(|editor, window, cx| {
20812 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20813 });
20814
20815 cx.assert_state_with_diff(
20816 r#"
20817 - ˇa
20818 + A
20819 b
20820 - c
20821 "#
20822 .unindent(),
20823 );
20824
20825 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20826 let snapshot = editor.snapshot(window, cx);
20827 let hunks = editor
20828 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20829 .collect::<Vec<_>>();
20830 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20831 let buffer_id = hunks[0].buffer_id;
20832 hunks
20833 .into_iter()
20834 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20835 .collect::<Vec<_>>()
20836 });
20837 assert_eq!(hunk_ranges.len(), 2);
20838
20839 cx.update_editor(|editor, _, cx| {
20840 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20841 });
20842 executor.run_until_parked();
20843
20844 cx.assert_state_with_diff(
20845 r#"
20846 - ˇa
20847 + A
20848 b
20849 "#
20850 .unindent(),
20851 );
20852}
20853
20854#[gpui::test]
20855async fn test_toggle_deletion_hunk_at_start_of_file(
20856 executor: BackgroundExecutor,
20857 cx: &mut TestAppContext,
20858) {
20859 init_test(cx, |_| {});
20860 let mut cx = EditorTestContext::new(cx).await;
20861
20862 let diff_base = r#"
20863 a
20864 b
20865 c
20866 "#
20867 .unindent();
20868
20869 cx.set_state(
20870 &r#"
20871 ˇb
20872 c
20873 "#
20874 .unindent(),
20875 );
20876 cx.set_head_text(&diff_base);
20877 cx.update_editor(|editor, window, cx| {
20878 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20879 });
20880 executor.run_until_parked();
20881
20882 let hunk_expanded = r#"
20883 - a
20884 ˇb
20885 c
20886 "#
20887 .unindent();
20888
20889 cx.assert_state_with_diff(hunk_expanded.clone());
20890
20891 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20892 let snapshot = editor.snapshot(window, cx);
20893 let hunks = editor
20894 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20895 .collect::<Vec<_>>();
20896 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20897 let buffer_id = hunks[0].buffer_id;
20898 hunks
20899 .into_iter()
20900 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20901 .collect::<Vec<_>>()
20902 });
20903 assert_eq!(hunk_ranges.len(), 1);
20904
20905 cx.update_editor(|editor, _, cx| {
20906 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20907 });
20908 executor.run_until_parked();
20909
20910 let hunk_collapsed = r#"
20911 ˇb
20912 c
20913 "#
20914 .unindent();
20915
20916 cx.assert_state_with_diff(hunk_collapsed);
20917
20918 cx.update_editor(|editor, _, cx| {
20919 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20920 });
20921 executor.run_until_parked();
20922
20923 cx.assert_state_with_diff(hunk_expanded);
20924}
20925
20926#[gpui::test]
20927async fn test_display_diff_hunks(cx: &mut TestAppContext) {
20928 init_test(cx, |_| {});
20929
20930 let fs = FakeFs::new(cx.executor());
20931 fs.insert_tree(
20932 path!("/test"),
20933 json!({
20934 ".git": {},
20935 "file-1": "ONE\n",
20936 "file-2": "TWO\n",
20937 "file-3": "THREE\n",
20938 }),
20939 )
20940 .await;
20941
20942 fs.set_head_for_repo(
20943 path!("/test/.git").as_ref(),
20944 &[
20945 ("file-1", "one\n".into()),
20946 ("file-2", "two\n".into()),
20947 ("file-3", "three\n".into()),
20948 ],
20949 "deadbeef",
20950 );
20951
20952 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
20953 let mut buffers = vec![];
20954 for i in 1..=3 {
20955 let buffer = project
20956 .update(cx, |project, cx| {
20957 let path = format!(path!("/test/file-{}"), i);
20958 project.open_local_buffer(path, cx)
20959 })
20960 .await
20961 .unwrap();
20962 buffers.push(buffer);
20963 }
20964
20965 let multibuffer = cx.new(|cx| {
20966 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
20967 multibuffer.set_all_diff_hunks_expanded(cx);
20968 for buffer in &buffers {
20969 let snapshot = buffer.read(cx).snapshot();
20970 multibuffer.set_excerpts_for_path(
20971 PathKey::namespaced(
20972 0,
20973 buffer.read(cx).file().unwrap().path().as_unix_str().into(),
20974 ),
20975 buffer.clone(),
20976 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
20977 2,
20978 cx,
20979 );
20980 }
20981 multibuffer
20982 });
20983
20984 let editor = cx.add_window(|window, cx| {
20985 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
20986 });
20987 cx.run_until_parked();
20988
20989 let snapshot = editor
20990 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
20991 .unwrap();
20992 let hunks = snapshot
20993 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
20994 .map(|hunk| match hunk {
20995 DisplayDiffHunk::Unfolded {
20996 display_row_range, ..
20997 } => display_row_range,
20998 DisplayDiffHunk::Folded { .. } => unreachable!(),
20999 })
21000 .collect::<Vec<_>>();
21001 assert_eq!(
21002 hunks,
21003 [
21004 DisplayRow(2)..DisplayRow(4),
21005 DisplayRow(7)..DisplayRow(9),
21006 DisplayRow(12)..DisplayRow(14),
21007 ]
21008 );
21009}
21010
21011#[gpui::test]
21012async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
21013 init_test(cx, |_| {});
21014
21015 let mut cx = EditorTestContext::new(cx).await;
21016 cx.set_head_text(indoc! { "
21017 one
21018 two
21019 three
21020 four
21021 five
21022 "
21023 });
21024 cx.set_index_text(indoc! { "
21025 one
21026 two
21027 three
21028 four
21029 five
21030 "
21031 });
21032 cx.set_state(indoc! {"
21033 one
21034 TWO
21035 ˇTHREE
21036 FOUR
21037 five
21038 "});
21039 cx.run_until_parked();
21040 cx.update_editor(|editor, window, cx| {
21041 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21042 });
21043 cx.run_until_parked();
21044 cx.assert_index_text(Some(indoc! {"
21045 one
21046 TWO
21047 THREE
21048 FOUR
21049 five
21050 "}));
21051 cx.set_state(indoc! { "
21052 one
21053 TWO
21054 ˇTHREE-HUNDRED
21055 FOUR
21056 five
21057 "});
21058 cx.run_until_parked();
21059 cx.update_editor(|editor, window, cx| {
21060 let snapshot = editor.snapshot(window, cx);
21061 let hunks = editor
21062 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
21063 .collect::<Vec<_>>();
21064 assert_eq!(hunks.len(), 1);
21065 assert_eq!(
21066 hunks[0].status(),
21067 DiffHunkStatus {
21068 kind: DiffHunkStatusKind::Modified,
21069 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
21070 }
21071 );
21072
21073 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21074 });
21075 cx.run_until_parked();
21076 cx.assert_index_text(Some(indoc! {"
21077 one
21078 TWO
21079 THREE-HUNDRED
21080 FOUR
21081 five
21082 "}));
21083}
21084
21085#[gpui::test]
21086fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
21087 init_test(cx, |_| {});
21088
21089 let editor = cx.add_window(|window, cx| {
21090 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
21091 build_editor(buffer, window, cx)
21092 });
21093
21094 let render_args = Arc::new(Mutex::new(None));
21095 let snapshot = editor
21096 .update(cx, |editor, window, cx| {
21097 let snapshot = editor.buffer().read(cx).snapshot(cx);
21098 let range =
21099 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
21100
21101 struct RenderArgs {
21102 row: MultiBufferRow,
21103 folded: bool,
21104 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
21105 }
21106
21107 let crease = Crease::inline(
21108 range,
21109 FoldPlaceholder::test(),
21110 {
21111 let toggle_callback = render_args.clone();
21112 move |row, folded, callback, _window, _cx| {
21113 *toggle_callback.lock() = Some(RenderArgs {
21114 row,
21115 folded,
21116 callback,
21117 });
21118 div()
21119 }
21120 },
21121 |_row, _folded, _window, _cx| div(),
21122 );
21123
21124 editor.insert_creases(Some(crease), cx);
21125 let snapshot = editor.snapshot(window, cx);
21126 let _div =
21127 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
21128 snapshot
21129 })
21130 .unwrap();
21131
21132 let render_args = render_args.lock().take().unwrap();
21133 assert_eq!(render_args.row, MultiBufferRow(1));
21134 assert!(!render_args.folded);
21135 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21136
21137 cx.update_window(*editor, |_, window, cx| {
21138 (render_args.callback)(true, window, cx)
21139 })
21140 .unwrap();
21141 let snapshot = editor
21142 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21143 .unwrap();
21144 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
21145
21146 cx.update_window(*editor, |_, window, cx| {
21147 (render_args.callback)(false, window, cx)
21148 })
21149 .unwrap();
21150 let snapshot = editor
21151 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21152 .unwrap();
21153 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21154}
21155
21156#[gpui::test]
21157async fn test_input_text(cx: &mut TestAppContext) {
21158 init_test(cx, |_| {});
21159 let mut cx = EditorTestContext::new(cx).await;
21160
21161 cx.set_state(
21162 &r#"ˇone
21163 two
21164
21165 three
21166 fourˇ
21167 five
21168
21169 siˇx"#
21170 .unindent(),
21171 );
21172
21173 cx.dispatch_action(HandleInput(String::new()));
21174 cx.assert_editor_state(
21175 &r#"ˇone
21176 two
21177
21178 three
21179 fourˇ
21180 five
21181
21182 siˇx"#
21183 .unindent(),
21184 );
21185
21186 cx.dispatch_action(HandleInput("AAAA".to_string()));
21187 cx.assert_editor_state(
21188 &r#"AAAAˇone
21189 two
21190
21191 three
21192 fourAAAAˇ
21193 five
21194
21195 siAAAAˇx"#
21196 .unindent(),
21197 );
21198}
21199
21200#[gpui::test]
21201async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
21202 init_test(cx, |_| {});
21203
21204 let mut cx = EditorTestContext::new(cx).await;
21205 cx.set_state(
21206 r#"let foo = 1;
21207let foo = 2;
21208let foo = 3;
21209let fooˇ = 4;
21210let foo = 5;
21211let foo = 6;
21212let foo = 7;
21213let foo = 8;
21214let foo = 9;
21215let foo = 10;
21216let foo = 11;
21217let foo = 12;
21218let foo = 13;
21219let foo = 14;
21220let foo = 15;"#,
21221 );
21222
21223 cx.update_editor(|e, window, cx| {
21224 assert_eq!(
21225 e.next_scroll_position,
21226 NextScrollCursorCenterTopBottom::Center,
21227 "Default next scroll direction is center",
21228 );
21229
21230 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21231 assert_eq!(
21232 e.next_scroll_position,
21233 NextScrollCursorCenterTopBottom::Top,
21234 "After center, next scroll direction should be top",
21235 );
21236
21237 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21238 assert_eq!(
21239 e.next_scroll_position,
21240 NextScrollCursorCenterTopBottom::Bottom,
21241 "After top, next scroll direction should be bottom",
21242 );
21243
21244 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21245 assert_eq!(
21246 e.next_scroll_position,
21247 NextScrollCursorCenterTopBottom::Center,
21248 "After bottom, scrolling should start over",
21249 );
21250
21251 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21252 assert_eq!(
21253 e.next_scroll_position,
21254 NextScrollCursorCenterTopBottom::Top,
21255 "Scrolling continues if retriggered fast enough"
21256 );
21257 });
21258
21259 cx.executor()
21260 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
21261 cx.executor().run_until_parked();
21262 cx.update_editor(|e, _, _| {
21263 assert_eq!(
21264 e.next_scroll_position,
21265 NextScrollCursorCenterTopBottom::Center,
21266 "If scrolling is not triggered fast enough, it should reset"
21267 );
21268 });
21269}
21270
21271#[gpui::test]
21272async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
21273 init_test(cx, |_| {});
21274 let mut cx = EditorLspTestContext::new_rust(
21275 lsp::ServerCapabilities {
21276 definition_provider: Some(lsp::OneOf::Left(true)),
21277 references_provider: Some(lsp::OneOf::Left(true)),
21278 ..lsp::ServerCapabilities::default()
21279 },
21280 cx,
21281 )
21282 .await;
21283
21284 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
21285 let go_to_definition = cx
21286 .lsp
21287 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21288 move |params, _| async move {
21289 if empty_go_to_definition {
21290 Ok(None)
21291 } else {
21292 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
21293 uri: params.text_document_position_params.text_document.uri,
21294 range: lsp::Range::new(
21295 lsp::Position::new(4, 3),
21296 lsp::Position::new(4, 6),
21297 ),
21298 })))
21299 }
21300 },
21301 );
21302 let references = cx
21303 .lsp
21304 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21305 Ok(Some(vec![lsp::Location {
21306 uri: params.text_document_position.text_document.uri,
21307 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
21308 }]))
21309 });
21310 (go_to_definition, references)
21311 };
21312
21313 cx.set_state(
21314 &r#"fn one() {
21315 let mut a = ˇtwo();
21316 }
21317
21318 fn two() {}"#
21319 .unindent(),
21320 );
21321 set_up_lsp_handlers(false, &mut cx);
21322 let navigated = cx
21323 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21324 .await
21325 .expect("Failed to navigate to definition");
21326 assert_eq!(
21327 navigated,
21328 Navigated::Yes,
21329 "Should have navigated to definition from the GetDefinition response"
21330 );
21331 cx.assert_editor_state(
21332 &r#"fn one() {
21333 let mut a = two();
21334 }
21335
21336 fn «twoˇ»() {}"#
21337 .unindent(),
21338 );
21339
21340 let editors = cx.update_workspace(|workspace, _, cx| {
21341 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21342 });
21343 cx.update_editor(|_, _, test_editor_cx| {
21344 assert_eq!(
21345 editors.len(),
21346 1,
21347 "Initially, only one, test, editor should be open in the workspace"
21348 );
21349 assert_eq!(
21350 test_editor_cx.entity(),
21351 editors.last().expect("Asserted len is 1").clone()
21352 );
21353 });
21354
21355 set_up_lsp_handlers(true, &mut cx);
21356 let navigated = cx
21357 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21358 .await
21359 .expect("Failed to navigate to lookup references");
21360 assert_eq!(
21361 navigated,
21362 Navigated::Yes,
21363 "Should have navigated to references as a fallback after empty GoToDefinition response"
21364 );
21365 // We should not change the selections in the existing file,
21366 // if opening another milti buffer with the references
21367 cx.assert_editor_state(
21368 &r#"fn one() {
21369 let mut a = two();
21370 }
21371
21372 fn «twoˇ»() {}"#
21373 .unindent(),
21374 );
21375 let editors = cx.update_workspace(|workspace, _, cx| {
21376 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21377 });
21378 cx.update_editor(|_, _, test_editor_cx| {
21379 assert_eq!(
21380 editors.len(),
21381 2,
21382 "After falling back to references search, we open a new editor with the results"
21383 );
21384 let references_fallback_text = editors
21385 .into_iter()
21386 .find(|new_editor| *new_editor != test_editor_cx.entity())
21387 .expect("Should have one non-test editor now")
21388 .read(test_editor_cx)
21389 .text(test_editor_cx);
21390 assert_eq!(
21391 references_fallback_text, "fn one() {\n let mut a = two();\n}",
21392 "Should use the range from the references response and not the GoToDefinition one"
21393 );
21394 });
21395}
21396
21397#[gpui::test]
21398async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
21399 init_test(cx, |_| {});
21400 cx.update(|cx| {
21401 let mut editor_settings = EditorSettings::get_global(cx).clone();
21402 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
21403 EditorSettings::override_global(editor_settings, cx);
21404 });
21405 let mut cx = EditorLspTestContext::new_rust(
21406 lsp::ServerCapabilities {
21407 definition_provider: Some(lsp::OneOf::Left(true)),
21408 references_provider: Some(lsp::OneOf::Left(true)),
21409 ..lsp::ServerCapabilities::default()
21410 },
21411 cx,
21412 )
21413 .await;
21414 let original_state = r#"fn one() {
21415 let mut a = ˇtwo();
21416 }
21417
21418 fn two() {}"#
21419 .unindent();
21420 cx.set_state(&original_state);
21421
21422 let mut go_to_definition = cx
21423 .lsp
21424 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21425 move |_, _| async move { Ok(None) },
21426 );
21427 let _references = cx
21428 .lsp
21429 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
21430 panic!("Should not call for references with no go to definition fallback")
21431 });
21432
21433 let navigated = cx
21434 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21435 .await
21436 .expect("Failed to navigate to lookup references");
21437 go_to_definition
21438 .next()
21439 .await
21440 .expect("Should have called the go_to_definition handler");
21441
21442 assert_eq!(
21443 navigated,
21444 Navigated::No,
21445 "Should have navigated to references as a fallback after empty GoToDefinition response"
21446 );
21447 cx.assert_editor_state(&original_state);
21448 let editors = cx.update_workspace(|workspace, _, cx| {
21449 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21450 });
21451 cx.update_editor(|_, _, _| {
21452 assert_eq!(
21453 editors.len(),
21454 1,
21455 "After unsuccessful fallback, no other editor should have been opened"
21456 );
21457 });
21458}
21459
21460#[gpui::test]
21461async fn test_find_all_references_editor_reuse(cx: &mut TestAppContext) {
21462 init_test(cx, |_| {});
21463 let mut cx = EditorLspTestContext::new_rust(
21464 lsp::ServerCapabilities {
21465 references_provider: Some(lsp::OneOf::Left(true)),
21466 ..lsp::ServerCapabilities::default()
21467 },
21468 cx,
21469 )
21470 .await;
21471
21472 cx.set_state(
21473 &r#"
21474 fn one() {
21475 let mut a = two();
21476 }
21477
21478 fn ˇtwo() {}"#
21479 .unindent(),
21480 );
21481 cx.lsp
21482 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21483 Ok(Some(vec![
21484 lsp::Location {
21485 uri: params.text_document_position.text_document.uri.clone(),
21486 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21487 },
21488 lsp::Location {
21489 uri: params.text_document_position.text_document.uri,
21490 range: lsp::Range::new(lsp::Position::new(4, 4), lsp::Position::new(4, 7)),
21491 },
21492 ]))
21493 });
21494 let navigated = cx
21495 .update_editor(|editor, window, cx| {
21496 editor.find_all_references(&FindAllReferences, window, cx)
21497 })
21498 .unwrap()
21499 .await
21500 .expect("Failed to navigate to references");
21501 assert_eq!(
21502 navigated,
21503 Navigated::Yes,
21504 "Should have navigated to references from the FindAllReferences response"
21505 );
21506 cx.assert_editor_state(
21507 &r#"fn one() {
21508 let mut a = two();
21509 }
21510
21511 fn ˇtwo() {}"#
21512 .unindent(),
21513 );
21514
21515 let editors = cx.update_workspace(|workspace, _, cx| {
21516 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21517 });
21518 cx.update_editor(|_, _, _| {
21519 assert_eq!(editors.len(), 2, "We should have opened a new multibuffer");
21520 });
21521
21522 cx.set_state(
21523 &r#"fn one() {
21524 let mut a = ˇtwo();
21525 }
21526
21527 fn two() {}"#
21528 .unindent(),
21529 );
21530 let navigated = cx
21531 .update_editor(|editor, window, cx| {
21532 editor.find_all_references(&FindAllReferences, window, cx)
21533 })
21534 .unwrap()
21535 .await
21536 .expect("Failed to navigate to references");
21537 assert_eq!(
21538 navigated,
21539 Navigated::Yes,
21540 "Should have navigated to references from the FindAllReferences response"
21541 );
21542 cx.assert_editor_state(
21543 &r#"fn one() {
21544 let mut a = ˇtwo();
21545 }
21546
21547 fn two() {}"#
21548 .unindent(),
21549 );
21550 let editors = cx.update_workspace(|workspace, _, cx| {
21551 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21552 });
21553 cx.update_editor(|_, _, _| {
21554 assert_eq!(
21555 editors.len(),
21556 2,
21557 "should have re-used the previous multibuffer"
21558 );
21559 });
21560
21561 cx.set_state(
21562 &r#"fn one() {
21563 let mut a = ˇtwo();
21564 }
21565 fn three() {}
21566 fn two() {}"#
21567 .unindent(),
21568 );
21569 cx.lsp
21570 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21571 Ok(Some(vec![
21572 lsp::Location {
21573 uri: params.text_document_position.text_document.uri.clone(),
21574 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21575 },
21576 lsp::Location {
21577 uri: params.text_document_position.text_document.uri,
21578 range: lsp::Range::new(lsp::Position::new(5, 4), lsp::Position::new(5, 7)),
21579 },
21580 ]))
21581 });
21582 let navigated = cx
21583 .update_editor(|editor, window, cx| {
21584 editor.find_all_references(&FindAllReferences, window, cx)
21585 })
21586 .unwrap()
21587 .await
21588 .expect("Failed to navigate to references");
21589 assert_eq!(
21590 navigated,
21591 Navigated::Yes,
21592 "Should have navigated to references from the FindAllReferences response"
21593 );
21594 cx.assert_editor_state(
21595 &r#"fn one() {
21596 let mut a = ˇtwo();
21597 }
21598 fn three() {}
21599 fn two() {}"#
21600 .unindent(),
21601 );
21602 let editors = cx.update_workspace(|workspace, _, cx| {
21603 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21604 });
21605 cx.update_editor(|_, _, _| {
21606 assert_eq!(
21607 editors.len(),
21608 3,
21609 "should have used a new multibuffer as offsets changed"
21610 );
21611 });
21612}
21613#[gpui::test]
21614async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
21615 init_test(cx, |_| {});
21616
21617 let language = Arc::new(Language::new(
21618 LanguageConfig::default(),
21619 Some(tree_sitter_rust::LANGUAGE.into()),
21620 ));
21621
21622 let text = r#"
21623 #[cfg(test)]
21624 mod tests() {
21625 #[test]
21626 fn runnable_1() {
21627 let a = 1;
21628 }
21629
21630 #[test]
21631 fn runnable_2() {
21632 let a = 1;
21633 let b = 2;
21634 }
21635 }
21636 "#
21637 .unindent();
21638
21639 let fs = FakeFs::new(cx.executor());
21640 fs.insert_file("/file.rs", Default::default()).await;
21641
21642 let project = Project::test(fs, ["/a".as_ref()], cx).await;
21643 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21644 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21645 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
21646 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
21647
21648 let editor = cx.new_window_entity(|window, cx| {
21649 Editor::new(
21650 EditorMode::full(),
21651 multi_buffer,
21652 Some(project.clone()),
21653 window,
21654 cx,
21655 )
21656 });
21657
21658 editor.update_in(cx, |editor, window, cx| {
21659 let snapshot = editor.buffer().read(cx).snapshot(cx);
21660 editor.tasks.insert(
21661 (buffer.read(cx).remote_id(), 3),
21662 RunnableTasks {
21663 templates: vec![],
21664 offset: snapshot.anchor_before(43),
21665 column: 0,
21666 extra_variables: HashMap::default(),
21667 context_range: BufferOffset(43)..BufferOffset(85),
21668 },
21669 );
21670 editor.tasks.insert(
21671 (buffer.read(cx).remote_id(), 8),
21672 RunnableTasks {
21673 templates: vec![],
21674 offset: snapshot.anchor_before(86),
21675 column: 0,
21676 extra_variables: HashMap::default(),
21677 context_range: BufferOffset(86)..BufferOffset(191),
21678 },
21679 );
21680
21681 // Test finding task when cursor is inside function body
21682 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21683 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
21684 });
21685 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
21686 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
21687
21688 // Test finding task when cursor is on function name
21689 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21690 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
21691 });
21692 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
21693 assert_eq!(row, 8, "Should find task when cursor is on function name");
21694 });
21695}
21696
21697#[gpui::test]
21698async fn test_folding_buffers(cx: &mut TestAppContext) {
21699 init_test(cx, |_| {});
21700
21701 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
21702 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
21703 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
21704
21705 let fs = FakeFs::new(cx.executor());
21706 fs.insert_tree(
21707 path!("/a"),
21708 json!({
21709 "first.rs": sample_text_1,
21710 "second.rs": sample_text_2,
21711 "third.rs": sample_text_3,
21712 }),
21713 )
21714 .await;
21715 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21716 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21717 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21718 let worktree = project.update(cx, |project, cx| {
21719 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
21720 assert_eq!(worktrees.len(), 1);
21721 worktrees.pop().unwrap()
21722 });
21723 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
21724
21725 let buffer_1 = project
21726 .update(cx, |project, cx| {
21727 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
21728 })
21729 .await
21730 .unwrap();
21731 let buffer_2 = project
21732 .update(cx, |project, cx| {
21733 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
21734 })
21735 .await
21736 .unwrap();
21737 let buffer_3 = project
21738 .update(cx, |project, cx| {
21739 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
21740 })
21741 .await
21742 .unwrap();
21743
21744 let multi_buffer = cx.new(|cx| {
21745 let mut multi_buffer = MultiBuffer::new(ReadWrite);
21746 multi_buffer.push_excerpts(
21747 buffer_1.clone(),
21748 [
21749 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21750 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21751 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21752 ],
21753 cx,
21754 );
21755 multi_buffer.push_excerpts(
21756 buffer_2.clone(),
21757 [
21758 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21759 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21760 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21761 ],
21762 cx,
21763 );
21764 multi_buffer.push_excerpts(
21765 buffer_3.clone(),
21766 [
21767 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21768 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21769 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21770 ],
21771 cx,
21772 );
21773 multi_buffer
21774 });
21775 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
21776 Editor::new(
21777 EditorMode::full(),
21778 multi_buffer.clone(),
21779 Some(project.clone()),
21780 window,
21781 cx,
21782 )
21783 });
21784
21785 assert_eq!(
21786 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21787 "\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",
21788 );
21789
21790 multi_buffer_editor.update(cx, |editor, cx| {
21791 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
21792 });
21793 assert_eq!(
21794 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21795 "\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",
21796 "After folding the first buffer, its text should not be displayed"
21797 );
21798
21799 multi_buffer_editor.update(cx, |editor, cx| {
21800 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
21801 });
21802 assert_eq!(
21803 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21804 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
21805 "After folding the second buffer, its text should not be displayed"
21806 );
21807
21808 multi_buffer_editor.update(cx, |editor, cx| {
21809 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
21810 });
21811 assert_eq!(
21812 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21813 "\n\n\n\n\n",
21814 "After folding the third buffer, its text should not be displayed"
21815 );
21816
21817 // Emulate selection inside the fold logic, that should work
21818 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21819 editor
21820 .snapshot(window, cx)
21821 .next_line_boundary(Point::new(0, 4));
21822 });
21823
21824 multi_buffer_editor.update(cx, |editor, cx| {
21825 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
21826 });
21827 assert_eq!(
21828 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21829 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
21830 "After unfolding the second buffer, its text should be displayed"
21831 );
21832
21833 // Typing inside of buffer 1 causes that buffer to be unfolded.
21834 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21835 assert_eq!(
21836 multi_buffer
21837 .read(cx)
21838 .snapshot(cx)
21839 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
21840 .collect::<String>(),
21841 "bbbb"
21842 );
21843 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
21844 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
21845 });
21846 editor.handle_input("B", window, cx);
21847 });
21848
21849 assert_eq!(
21850 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21851 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
21852 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
21853 );
21854
21855 multi_buffer_editor.update(cx, |editor, cx| {
21856 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
21857 });
21858 assert_eq!(
21859 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21860 "\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",
21861 "After unfolding the all buffers, all original text should be displayed"
21862 );
21863}
21864
21865#[gpui::test]
21866async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
21867 init_test(cx, |_| {});
21868
21869 let sample_text_1 = "1111\n2222\n3333".to_string();
21870 let sample_text_2 = "4444\n5555\n6666".to_string();
21871 let sample_text_3 = "7777\n8888\n9999".to_string();
21872
21873 let fs = FakeFs::new(cx.executor());
21874 fs.insert_tree(
21875 path!("/a"),
21876 json!({
21877 "first.rs": sample_text_1,
21878 "second.rs": sample_text_2,
21879 "third.rs": sample_text_3,
21880 }),
21881 )
21882 .await;
21883 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21884 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21885 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21886 let worktree = project.update(cx, |project, cx| {
21887 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
21888 assert_eq!(worktrees.len(), 1);
21889 worktrees.pop().unwrap()
21890 });
21891 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
21892
21893 let buffer_1 = project
21894 .update(cx, |project, cx| {
21895 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
21896 })
21897 .await
21898 .unwrap();
21899 let buffer_2 = project
21900 .update(cx, |project, cx| {
21901 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
21902 })
21903 .await
21904 .unwrap();
21905 let buffer_3 = project
21906 .update(cx, |project, cx| {
21907 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
21908 })
21909 .await
21910 .unwrap();
21911
21912 let multi_buffer = cx.new(|cx| {
21913 let mut multi_buffer = MultiBuffer::new(ReadWrite);
21914 multi_buffer.push_excerpts(
21915 buffer_1.clone(),
21916 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21917 cx,
21918 );
21919 multi_buffer.push_excerpts(
21920 buffer_2.clone(),
21921 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21922 cx,
21923 );
21924 multi_buffer.push_excerpts(
21925 buffer_3.clone(),
21926 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21927 cx,
21928 );
21929 multi_buffer
21930 });
21931
21932 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
21933 Editor::new(
21934 EditorMode::full(),
21935 multi_buffer,
21936 Some(project.clone()),
21937 window,
21938 cx,
21939 )
21940 });
21941
21942 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
21943 assert_eq!(
21944 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21945 full_text,
21946 );
21947
21948 multi_buffer_editor.update(cx, |editor, cx| {
21949 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
21950 });
21951 assert_eq!(
21952 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21953 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
21954 "After folding the first buffer, its text should not be displayed"
21955 );
21956
21957 multi_buffer_editor.update(cx, |editor, cx| {
21958 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
21959 });
21960
21961 assert_eq!(
21962 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21963 "\n\n\n\n\n\n7777\n8888\n9999",
21964 "After folding the second buffer, its text should not be displayed"
21965 );
21966
21967 multi_buffer_editor.update(cx, |editor, cx| {
21968 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
21969 });
21970 assert_eq!(
21971 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21972 "\n\n\n\n\n",
21973 "After folding the third buffer, its text should not be displayed"
21974 );
21975
21976 multi_buffer_editor.update(cx, |editor, cx| {
21977 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
21978 });
21979 assert_eq!(
21980 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21981 "\n\n\n\n4444\n5555\n6666\n\n",
21982 "After unfolding the second buffer, its text should be displayed"
21983 );
21984
21985 multi_buffer_editor.update(cx, |editor, cx| {
21986 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
21987 });
21988 assert_eq!(
21989 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21990 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
21991 "After unfolding the first buffer, its text should be displayed"
21992 );
21993
21994 multi_buffer_editor.update(cx, |editor, cx| {
21995 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
21996 });
21997 assert_eq!(
21998 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21999 full_text,
22000 "After unfolding all buffers, all original text should be displayed"
22001 );
22002}
22003
22004#[gpui::test]
22005async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
22006 init_test(cx, |_| {});
22007
22008 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
22009
22010 let fs = FakeFs::new(cx.executor());
22011 fs.insert_tree(
22012 path!("/a"),
22013 json!({
22014 "main.rs": sample_text,
22015 }),
22016 )
22017 .await;
22018 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22019 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22020 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22021 let worktree = project.update(cx, |project, cx| {
22022 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22023 assert_eq!(worktrees.len(), 1);
22024 worktrees.pop().unwrap()
22025 });
22026 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22027
22028 let buffer_1 = project
22029 .update(cx, |project, cx| {
22030 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22031 })
22032 .await
22033 .unwrap();
22034
22035 let multi_buffer = cx.new(|cx| {
22036 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22037 multi_buffer.push_excerpts(
22038 buffer_1.clone(),
22039 [ExcerptRange::new(
22040 Point::new(0, 0)
22041 ..Point::new(
22042 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
22043 0,
22044 ),
22045 )],
22046 cx,
22047 );
22048 multi_buffer
22049 });
22050 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22051 Editor::new(
22052 EditorMode::full(),
22053 multi_buffer,
22054 Some(project.clone()),
22055 window,
22056 cx,
22057 )
22058 });
22059
22060 let selection_range = Point::new(1, 0)..Point::new(2, 0);
22061 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22062 enum TestHighlight {}
22063 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
22064 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
22065 editor.highlight_text::<TestHighlight>(
22066 vec![highlight_range.clone()],
22067 HighlightStyle::color(Hsla::green()),
22068 cx,
22069 );
22070 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22071 s.select_ranges(Some(highlight_range))
22072 });
22073 });
22074
22075 let full_text = format!("\n\n{sample_text}");
22076 assert_eq!(
22077 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22078 full_text,
22079 );
22080}
22081
22082#[gpui::test]
22083async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
22084 init_test(cx, |_| {});
22085 cx.update(|cx| {
22086 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
22087 "keymaps/default-linux.json",
22088 cx,
22089 )
22090 .unwrap();
22091 cx.bind_keys(default_key_bindings);
22092 });
22093
22094 let (editor, cx) = cx.add_window_view(|window, cx| {
22095 let multi_buffer = MultiBuffer::build_multi(
22096 [
22097 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
22098 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
22099 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
22100 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
22101 ],
22102 cx,
22103 );
22104 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
22105
22106 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
22107 // fold all but the second buffer, so that we test navigating between two
22108 // adjacent folded buffers, as well as folded buffers at the start and
22109 // end the multibuffer
22110 editor.fold_buffer(buffer_ids[0], cx);
22111 editor.fold_buffer(buffer_ids[2], cx);
22112 editor.fold_buffer(buffer_ids[3], cx);
22113
22114 editor
22115 });
22116 cx.simulate_resize(size(px(1000.), px(1000.)));
22117
22118 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
22119 cx.assert_excerpts_with_selections(indoc! {"
22120 [EXCERPT]
22121 ˇ[FOLDED]
22122 [EXCERPT]
22123 a1
22124 b1
22125 [EXCERPT]
22126 [FOLDED]
22127 [EXCERPT]
22128 [FOLDED]
22129 "
22130 });
22131 cx.simulate_keystroke("down");
22132 cx.assert_excerpts_with_selections(indoc! {"
22133 [EXCERPT]
22134 [FOLDED]
22135 [EXCERPT]
22136 ˇa1
22137 b1
22138 [EXCERPT]
22139 [FOLDED]
22140 [EXCERPT]
22141 [FOLDED]
22142 "
22143 });
22144 cx.simulate_keystroke("down");
22145 cx.assert_excerpts_with_selections(indoc! {"
22146 [EXCERPT]
22147 [FOLDED]
22148 [EXCERPT]
22149 a1
22150 ˇb1
22151 [EXCERPT]
22152 [FOLDED]
22153 [EXCERPT]
22154 [FOLDED]
22155 "
22156 });
22157 cx.simulate_keystroke("down");
22158 cx.assert_excerpts_with_selections(indoc! {"
22159 [EXCERPT]
22160 [FOLDED]
22161 [EXCERPT]
22162 a1
22163 b1
22164 ˇ[EXCERPT]
22165 [FOLDED]
22166 [EXCERPT]
22167 [FOLDED]
22168 "
22169 });
22170 cx.simulate_keystroke("down");
22171 cx.assert_excerpts_with_selections(indoc! {"
22172 [EXCERPT]
22173 [FOLDED]
22174 [EXCERPT]
22175 a1
22176 b1
22177 [EXCERPT]
22178 ˇ[FOLDED]
22179 [EXCERPT]
22180 [FOLDED]
22181 "
22182 });
22183 for _ in 0..5 {
22184 cx.simulate_keystroke("down");
22185 cx.assert_excerpts_with_selections(indoc! {"
22186 [EXCERPT]
22187 [FOLDED]
22188 [EXCERPT]
22189 a1
22190 b1
22191 [EXCERPT]
22192 [FOLDED]
22193 [EXCERPT]
22194 ˇ[FOLDED]
22195 "
22196 });
22197 }
22198
22199 cx.simulate_keystroke("up");
22200 cx.assert_excerpts_with_selections(indoc! {"
22201 [EXCERPT]
22202 [FOLDED]
22203 [EXCERPT]
22204 a1
22205 b1
22206 [EXCERPT]
22207 ˇ[FOLDED]
22208 [EXCERPT]
22209 [FOLDED]
22210 "
22211 });
22212 cx.simulate_keystroke("up");
22213 cx.assert_excerpts_with_selections(indoc! {"
22214 [EXCERPT]
22215 [FOLDED]
22216 [EXCERPT]
22217 a1
22218 b1
22219 ˇ[EXCERPT]
22220 [FOLDED]
22221 [EXCERPT]
22222 [FOLDED]
22223 "
22224 });
22225 cx.simulate_keystroke("up");
22226 cx.assert_excerpts_with_selections(indoc! {"
22227 [EXCERPT]
22228 [FOLDED]
22229 [EXCERPT]
22230 a1
22231 ˇb1
22232 [EXCERPT]
22233 [FOLDED]
22234 [EXCERPT]
22235 [FOLDED]
22236 "
22237 });
22238 cx.simulate_keystroke("up");
22239 cx.assert_excerpts_with_selections(indoc! {"
22240 [EXCERPT]
22241 [FOLDED]
22242 [EXCERPT]
22243 ˇa1
22244 b1
22245 [EXCERPT]
22246 [FOLDED]
22247 [EXCERPT]
22248 [FOLDED]
22249 "
22250 });
22251 for _ in 0..5 {
22252 cx.simulate_keystroke("up");
22253 cx.assert_excerpts_with_selections(indoc! {"
22254 [EXCERPT]
22255 ˇ[FOLDED]
22256 [EXCERPT]
22257 a1
22258 b1
22259 [EXCERPT]
22260 [FOLDED]
22261 [EXCERPT]
22262 [FOLDED]
22263 "
22264 });
22265 }
22266}
22267
22268#[gpui::test]
22269async fn test_edit_prediction_text(cx: &mut TestAppContext) {
22270 init_test(cx, |_| {});
22271
22272 // Simple insertion
22273 assert_highlighted_edits(
22274 "Hello, world!",
22275 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
22276 true,
22277 cx,
22278 |highlighted_edits, cx| {
22279 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
22280 assert_eq!(highlighted_edits.highlights.len(), 1);
22281 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
22282 assert_eq!(
22283 highlighted_edits.highlights[0].1.background_color,
22284 Some(cx.theme().status().created_background)
22285 );
22286 },
22287 )
22288 .await;
22289
22290 // Replacement
22291 assert_highlighted_edits(
22292 "This is a test.",
22293 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
22294 false,
22295 cx,
22296 |highlighted_edits, cx| {
22297 assert_eq!(highlighted_edits.text, "That is a test.");
22298 assert_eq!(highlighted_edits.highlights.len(), 1);
22299 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
22300 assert_eq!(
22301 highlighted_edits.highlights[0].1.background_color,
22302 Some(cx.theme().status().created_background)
22303 );
22304 },
22305 )
22306 .await;
22307
22308 // Multiple edits
22309 assert_highlighted_edits(
22310 "Hello, world!",
22311 vec![
22312 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
22313 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
22314 ],
22315 false,
22316 cx,
22317 |highlighted_edits, cx| {
22318 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
22319 assert_eq!(highlighted_edits.highlights.len(), 2);
22320 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
22321 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
22322 assert_eq!(
22323 highlighted_edits.highlights[0].1.background_color,
22324 Some(cx.theme().status().created_background)
22325 );
22326 assert_eq!(
22327 highlighted_edits.highlights[1].1.background_color,
22328 Some(cx.theme().status().created_background)
22329 );
22330 },
22331 )
22332 .await;
22333
22334 // Multiple lines with edits
22335 assert_highlighted_edits(
22336 "First line\nSecond line\nThird line\nFourth line",
22337 vec![
22338 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
22339 (
22340 Point::new(2, 0)..Point::new(2, 10),
22341 "New third line".to_string(),
22342 ),
22343 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
22344 ],
22345 false,
22346 cx,
22347 |highlighted_edits, cx| {
22348 assert_eq!(
22349 highlighted_edits.text,
22350 "Second modified\nNew third line\nFourth updated line"
22351 );
22352 assert_eq!(highlighted_edits.highlights.len(), 3);
22353 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
22354 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
22355 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
22356 for highlight in &highlighted_edits.highlights {
22357 assert_eq!(
22358 highlight.1.background_color,
22359 Some(cx.theme().status().created_background)
22360 );
22361 }
22362 },
22363 )
22364 .await;
22365}
22366
22367#[gpui::test]
22368async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
22369 init_test(cx, |_| {});
22370
22371 // Deletion
22372 assert_highlighted_edits(
22373 "Hello, world!",
22374 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
22375 true,
22376 cx,
22377 |highlighted_edits, cx| {
22378 assert_eq!(highlighted_edits.text, "Hello, world!");
22379 assert_eq!(highlighted_edits.highlights.len(), 1);
22380 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
22381 assert_eq!(
22382 highlighted_edits.highlights[0].1.background_color,
22383 Some(cx.theme().status().deleted_background)
22384 );
22385 },
22386 )
22387 .await;
22388
22389 // Insertion
22390 assert_highlighted_edits(
22391 "Hello, world!",
22392 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
22393 true,
22394 cx,
22395 |highlighted_edits, cx| {
22396 assert_eq!(highlighted_edits.highlights.len(), 1);
22397 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
22398 assert_eq!(
22399 highlighted_edits.highlights[0].1.background_color,
22400 Some(cx.theme().status().created_background)
22401 );
22402 },
22403 )
22404 .await;
22405}
22406
22407async fn assert_highlighted_edits(
22408 text: &str,
22409 edits: Vec<(Range<Point>, String)>,
22410 include_deletions: bool,
22411 cx: &mut TestAppContext,
22412 assertion_fn: impl Fn(HighlightedText, &App),
22413) {
22414 let window = cx.add_window(|window, cx| {
22415 let buffer = MultiBuffer::build_simple(text, cx);
22416 Editor::new(EditorMode::full(), buffer, None, window, cx)
22417 });
22418 let cx = &mut VisualTestContext::from_window(*window, cx);
22419
22420 let (buffer, snapshot) = window
22421 .update(cx, |editor, _window, cx| {
22422 (
22423 editor.buffer().clone(),
22424 editor.buffer().read(cx).snapshot(cx),
22425 )
22426 })
22427 .unwrap();
22428
22429 let edits = edits
22430 .into_iter()
22431 .map(|(range, edit)| {
22432 (
22433 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
22434 edit,
22435 )
22436 })
22437 .collect::<Vec<_>>();
22438
22439 let text_anchor_edits = edits
22440 .clone()
22441 .into_iter()
22442 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
22443 .collect::<Vec<_>>();
22444
22445 let edit_preview = window
22446 .update(cx, |_, _window, cx| {
22447 buffer
22448 .read(cx)
22449 .as_singleton()
22450 .unwrap()
22451 .read(cx)
22452 .preview_edits(text_anchor_edits.into(), cx)
22453 })
22454 .unwrap()
22455 .await;
22456
22457 cx.update(|_window, cx| {
22458 let highlighted_edits = edit_prediction_edit_text(
22459 snapshot.as_singleton().unwrap().2,
22460 &edits,
22461 &edit_preview,
22462 include_deletions,
22463 cx,
22464 );
22465 assertion_fn(highlighted_edits, cx)
22466 });
22467}
22468
22469#[track_caller]
22470fn assert_breakpoint(
22471 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
22472 path: &Arc<Path>,
22473 expected: Vec<(u32, Breakpoint)>,
22474) {
22475 if expected.is_empty() {
22476 assert!(!breakpoints.contains_key(path), "{}", path.display());
22477 } else {
22478 let mut breakpoint = breakpoints
22479 .get(path)
22480 .unwrap()
22481 .iter()
22482 .map(|breakpoint| {
22483 (
22484 breakpoint.row,
22485 Breakpoint {
22486 message: breakpoint.message.clone(),
22487 state: breakpoint.state,
22488 condition: breakpoint.condition.clone(),
22489 hit_condition: breakpoint.hit_condition.clone(),
22490 },
22491 )
22492 })
22493 .collect::<Vec<_>>();
22494
22495 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
22496
22497 assert_eq!(expected, breakpoint);
22498 }
22499}
22500
22501fn add_log_breakpoint_at_cursor(
22502 editor: &mut Editor,
22503 log_message: &str,
22504 window: &mut Window,
22505 cx: &mut Context<Editor>,
22506) {
22507 let (anchor, bp) = editor
22508 .breakpoints_at_cursors(window, cx)
22509 .first()
22510 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
22511 .unwrap_or_else(|| {
22512 let cursor_position: Point = editor.selections.newest(cx).head();
22513
22514 let breakpoint_position = editor
22515 .snapshot(window, cx)
22516 .display_snapshot
22517 .buffer_snapshot
22518 .anchor_before(Point::new(cursor_position.row, 0));
22519
22520 (breakpoint_position, Breakpoint::new_log(log_message))
22521 });
22522
22523 editor.edit_breakpoint_at_anchor(
22524 anchor,
22525 bp,
22526 BreakpointEditAction::EditLogMessage(log_message.into()),
22527 cx,
22528 );
22529}
22530
22531#[gpui::test]
22532async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
22533 init_test(cx, |_| {});
22534
22535 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22536 let fs = FakeFs::new(cx.executor());
22537 fs.insert_tree(
22538 path!("/a"),
22539 json!({
22540 "main.rs": sample_text,
22541 }),
22542 )
22543 .await;
22544 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22545 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22546 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22547
22548 let fs = FakeFs::new(cx.executor());
22549 fs.insert_tree(
22550 path!("/a"),
22551 json!({
22552 "main.rs": sample_text,
22553 }),
22554 )
22555 .await;
22556 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22557 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22558 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22559 let worktree_id = workspace
22560 .update(cx, |workspace, _window, cx| {
22561 workspace.project().update(cx, |project, cx| {
22562 project.worktrees(cx).next().unwrap().read(cx).id()
22563 })
22564 })
22565 .unwrap();
22566
22567 let buffer = project
22568 .update(cx, |project, cx| {
22569 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22570 })
22571 .await
22572 .unwrap();
22573
22574 let (editor, cx) = cx.add_window_view(|window, cx| {
22575 Editor::new(
22576 EditorMode::full(),
22577 MultiBuffer::build_from_buffer(buffer, cx),
22578 Some(project.clone()),
22579 window,
22580 cx,
22581 )
22582 });
22583
22584 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22585 let abs_path = project.read_with(cx, |project, cx| {
22586 project
22587 .absolute_path(&project_path, cx)
22588 .map(Arc::from)
22589 .unwrap()
22590 });
22591
22592 // assert we can add breakpoint on the first line
22593 editor.update_in(cx, |editor, window, cx| {
22594 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22595 editor.move_to_end(&MoveToEnd, window, cx);
22596 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22597 });
22598
22599 let breakpoints = editor.update(cx, |editor, cx| {
22600 editor
22601 .breakpoint_store()
22602 .as_ref()
22603 .unwrap()
22604 .read(cx)
22605 .all_source_breakpoints(cx)
22606 });
22607
22608 assert_eq!(1, breakpoints.len());
22609 assert_breakpoint(
22610 &breakpoints,
22611 &abs_path,
22612 vec![
22613 (0, Breakpoint::new_standard()),
22614 (3, Breakpoint::new_standard()),
22615 ],
22616 );
22617
22618 editor.update_in(cx, |editor, window, cx| {
22619 editor.move_to_beginning(&MoveToBeginning, window, cx);
22620 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22621 });
22622
22623 let breakpoints = editor.update(cx, |editor, cx| {
22624 editor
22625 .breakpoint_store()
22626 .as_ref()
22627 .unwrap()
22628 .read(cx)
22629 .all_source_breakpoints(cx)
22630 });
22631
22632 assert_eq!(1, breakpoints.len());
22633 assert_breakpoint(
22634 &breakpoints,
22635 &abs_path,
22636 vec![(3, Breakpoint::new_standard())],
22637 );
22638
22639 editor.update_in(cx, |editor, window, cx| {
22640 editor.move_to_end(&MoveToEnd, window, cx);
22641 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22642 });
22643
22644 let breakpoints = editor.update(cx, |editor, cx| {
22645 editor
22646 .breakpoint_store()
22647 .as_ref()
22648 .unwrap()
22649 .read(cx)
22650 .all_source_breakpoints(cx)
22651 });
22652
22653 assert_eq!(0, breakpoints.len());
22654 assert_breakpoint(&breakpoints, &abs_path, vec![]);
22655}
22656
22657#[gpui::test]
22658async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
22659 init_test(cx, |_| {});
22660
22661 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22662
22663 let fs = FakeFs::new(cx.executor());
22664 fs.insert_tree(
22665 path!("/a"),
22666 json!({
22667 "main.rs": sample_text,
22668 }),
22669 )
22670 .await;
22671 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22672 let (workspace, cx) =
22673 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22674
22675 let worktree_id = workspace.update(cx, |workspace, cx| {
22676 workspace.project().update(cx, |project, cx| {
22677 project.worktrees(cx).next().unwrap().read(cx).id()
22678 })
22679 });
22680
22681 let buffer = project
22682 .update(cx, |project, cx| {
22683 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22684 })
22685 .await
22686 .unwrap();
22687
22688 let (editor, cx) = cx.add_window_view(|window, cx| {
22689 Editor::new(
22690 EditorMode::full(),
22691 MultiBuffer::build_from_buffer(buffer, cx),
22692 Some(project.clone()),
22693 window,
22694 cx,
22695 )
22696 });
22697
22698 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22699 let abs_path = project.read_with(cx, |project, cx| {
22700 project
22701 .absolute_path(&project_path, cx)
22702 .map(Arc::from)
22703 .unwrap()
22704 });
22705
22706 editor.update_in(cx, |editor, window, cx| {
22707 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
22708 });
22709
22710 let breakpoints = editor.update(cx, |editor, cx| {
22711 editor
22712 .breakpoint_store()
22713 .as_ref()
22714 .unwrap()
22715 .read(cx)
22716 .all_source_breakpoints(cx)
22717 });
22718
22719 assert_breakpoint(
22720 &breakpoints,
22721 &abs_path,
22722 vec![(0, Breakpoint::new_log("hello world"))],
22723 );
22724
22725 // Removing a log message from a log breakpoint should remove it
22726 editor.update_in(cx, |editor, window, cx| {
22727 add_log_breakpoint_at_cursor(editor, "", window, cx);
22728 });
22729
22730 let breakpoints = editor.update(cx, |editor, cx| {
22731 editor
22732 .breakpoint_store()
22733 .as_ref()
22734 .unwrap()
22735 .read(cx)
22736 .all_source_breakpoints(cx)
22737 });
22738
22739 assert_breakpoint(&breakpoints, &abs_path, vec![]);
22740
22741 editor.update_in(cx, |editor, window, cx| {
22742 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22743 editor.move_to_end(&MoveToEnd, window, cx);
22744 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22745 // Not adding a log message to a standard breakpoint shouldn't remove it
22746 add_log_breakpoint_at_cursor(editor, "", window, cx);
22747 });
22748
22749 let breakpoints = editor.update(cx, |editor, cx| {
22750 editor
22751 .breakpoint_store()
22752 .as_ref()
22753 .unwrap()
22754 .read(cx)
22755 .all_source_breakpoints(cx)
22756 });
22757
22758 assert_breakpoint(
22759 &breakpoints,
22760 &abs_path,
22761 vec![
22762 (0, Breakpoint::new_standard()),
22763 (3, Breakpoint::new_standard()),
22764 ],
22765 );
22766
22767 editor.update_in(cx, |editor, window, cx| {
22768 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
22769 });
22770
22771 let breakpoints = editor.update(cx, |editor, cx| {
22772 editor
22773 .breakpoint_store()
22774 .as_ref()
22775 .unwrap()
22776 .read(cx)
22777 .all_source_breakpoints(cx)
22778 });
22779
22780 assert_breakpoint(
22781 &breakpoints,
22782 &abs_path,
22783 vec![
22784 (0, Breakpoint::new_standard()),
22785 (3, Breakpoint::new_log("hello world")),
22786 ],
22787 );
22788
22789 editor.update_in(cx, |editor, window, cx| {
22790 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
22791 });
22792
22793 let breakpoints = editor.update(cx, |editor, cx| {
22794 editor
22795 .breakpoint_store()
22796 .as_ref()
22797 .unwrap()
22798 .read(cx)
22799 .all_source_breakpoints(cx)
22800 });
22801
22802 assert_breakpoint(
22803 &breakpoints,
22804 &abs_path,
22805 vec![
22806 (0, Breakpoint::new_standard()),
22807 (3, Breakpoint::new_log("hello Earth!!")),
22808 ],
22809 );
22810}
22811
22812/// This also tests that Editor::breakpoint_at_cursor_head is working properly
22813/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
22814/// or when breakpoints were placed out of order. This tests for a regression too
22815#[gpui::test]
22816async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
22817 init_test(cx, |_| {});
22818
22819 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22820 let fs = FakeFs::new(cx.executor());
22821 fs.insert_tree(
22822 path!("/a"),
22823 json!({
22824 "main.rs": sample_text,
22825 }),
22826 )
22827 .await;
22828 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22829 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22830 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22831
22832 let fs = FakeFs::new(cx.executor());
22833 fs.insert_tree(
22834 path!("/a"),
22835 json!({
22836 "main.rs": sample_text,
22837 }),
22838 )
22839 .await;
22840 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22841 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22842 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22843 let worktree_id = workspace
22844 .update(cx, |workspace, _window, cx| {
22845 workspace.project().update(cx, |project, cx| {
22846 project.worktrees(cx).next().unwrap().read(cx).id()
22847 })
22848 })
22849 .unwrap();
22850
22851 let buffer = project
22852 .update(cx, |project, cx| {
22853 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22854 })
22855 .await
22856 .unwrap();
22857
22858 let (editor, cx) = cx.add_window_view(|window, cx| {
22859 Editor::new(
22860 EditorMode::full(),
22861 MultiBuffer::build_from_buffer(buffer, cx),
22862 Some(project.clone()),
22863 window,
22864 cx,
22865 )
22866 });
22867
22868 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22869 let abs_path = project.read_with(cx, |project, cx| {
22870 project
22871 .absolute_path(&project_path, cx)
22872 .map(Arc::from)
22873 .unwrap()
22874 });
22875
22876 // assert we can add breakpoint on the first line
22877 editor.update_in(cx, |editor, window, cx| {
22878 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22879 editor.move_to_end(&MoveToEnd, window, cx);
22880 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22881 editor.move_up(&MoveUp, window, cx);
22882 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22883 });
22884
22885 let breakpoints = editor.update(cx, |editor, cx| {
22886 editor
22887 .breakpoint_store()
22888 .as_ref()
22889 .unwrap()
22890 .read(cx)
22891 .all_source_breakpoints(cx)
22892 });
22893
22894 assert_eq!(1, breakpoints.len());
22895 assert_breakpoint(
22896 &breakpoints,
22897 &abs_path,
22898 vec![
22899 (0, Breakpoint::new_standard()),
22900 (2, Breakpoint::new_standard()),
22901 (3, Breakpoint::new_standard()),
22902 ],
22903 );
22904
22905 editor.update_in(cx, |editor, window, cx| {
22906 editor.move_to_beginning(&MoveToBeginning, window, cx);
22907 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22908 editor.move_to_end(&MoveToEnd, window, cx);
22909 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22910 // Disabling a breakpoint that doesn't exist should do nothing
22911 editor.move_up(&MoveUp, window, cx);
22912 editor.move_up(&MoveUp, window, cx);
22913 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22914 });
22915
22916 let breakpoints = editor.update(cx, |editor, cx| {
22917 editor
22918 .breakpoint_store()
22919 .as_ref()
22920 .unwrap()
22921 .read(cx)
22922 .all_source_breakpoints(cx)
22923 });
22924
22925 let disable_breakpoint = {
22926 let mut bp = Breakpoint::new_standard();
22927 bp.state = BreakpointState::Disabled;
22928 bp
22929 };
22930
22931 assert_eq!(1, breakpoints.len());
22932 assert_breakpoint(
22933 &breakpoints,
22934 &abs_path,
22935 vec![
22936 (0, disable_breakpoint.clone()),
22937 (2, Breakpoint::new_standard()),
22938 (3, disable_breakpoint.clone()),
22939 ],
22940 );
22941
22942 editor.update_in(cx, |editor, window, cx| {
22943 editor.move_to_beginning(&MoveToBeginning, window, cx);
22944 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
22945 editor.move_to_end(&MoveToEnd, window, cx);
22946 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
22947 editor.move_up(&MoveUp, window, cx);
22948 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22949 });
22950
22951 let breakpoints = editor.update(cx, |editor, cx| {
22952 editor
22953 .breakpoint_store()
22954 .as_ref()
22955 .unwrap()
22956 .read(cx)
22957 .all_source_breakpoints(cx)
22958 });
22959
22960 assert_eq!(1, breakpoints.len());
22961 assert_breakpoint(
22962 &breakpoints,
22963 &abs_path,
22964 vec![
22965 (0, Breakpoint::new_standard()),
22966 (2, disable_breakpoint),
22967 (3, Breakpoint::new_standard()),
22968 ],
22969 );
22970}
22971
22972#[gpui::test]
22973async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
22974 init_test(cx, |_| {});
22975 let capabilities = lsp::ServerCapabilities {
22976 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
22977 prepare_provider: Some(true),
22978 work_done_progress_options: Default::default(),
22979 })),
22980 ..Default::default()
22981 };
22982 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
22983
22984 cx.set_state(indoc! {"
22985 struct Fˇoo {}
22986 "});
22987
22988 cx.update_editor(|editor, _, cx| {
22989 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
22990 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
22991 editor.highlight_background::<DocumentHighlightRead>(
22992 &[highlight_range],
22993 |theme| theme.colors().editor_document_highlight_read_background,
22994 cx,
22995 );
22996 });
22997
22998 let mut prepare_rename_handler = cx
22999 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
23000 move |_, _, _| async move {
23001 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
23002 start: lsp::Position {
23003 line: 0,
23004 character: 7,
23005 },
23006 end: lsp::Position {
23007 line: 0,
23008 character: 10,
23009 },
23010 })))
23011 },
23012 );
23013 let prepare_rename_task = cx
23014 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23015 .expect("Prepare rename was not started");
23016 prepare_rename_handler.next().await.unwrap();
23017 prepare_rename_task.await.expect("Prepare rename failed");
23018
23019 let mut rename_handler =
23020 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23021 let edit = lsp::TextEdit {
23022 range: lsp::Range {
23023 start: lsp::Position {
23024 line: 0,
23025 character: 7,
23026 },
23027 end: lsp::Position {
23028 line: 0,
23029 character: 10,
23030 },
23031 },
23032 new_text: "FooRenamed".to_string(),
23033 };
23034 Ok(Some(lsp::WorkspaceEdit::new(
23035 // Specify the same edit twice
23036 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
23037 )))
23038 });
23039 let rename_task = cx
23040 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23041 .expect("Confirm rename was not started");
23042 rename_handler.next().await.unwrap();
23043 rename_task.await.expect("Confirm rename failed");
23044 cx.run_until_parked();
23045
23046 // Despite two edits, only one is actually applied as those are identical
23047 cx.assert_editor_state(indoc! {"
23048 struct FooRenamedˇ {}
23049 "});
23050}
23051
23052#[gpui::test]
23053async fn test_rename_without_prepare(cx: &mut TestAppContext) {
23054 init_test(cx, |_| {});
23055 // These capabilities indicate that the server does not support prepare rename.
23056 let capabilities = lsp::ServerCapabilities {
23057 rename_provider: Some(lsp::OneOf::Left(true)),
23058 ..Default::default()
23059 };
23060 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23061
23062 cx.set_state(indoc! {"
23063 struct Fˇoo {}
23064 "});
23065
23066 cx.update_editor(|editor, _window, cx| {
23067 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23068 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23069 editor.highlight_background::<DocumentHighlightRead>(
23070 &[highlight_range],
23071 |theme| theme.colors().editor_document_highlight_read_background,
23072 cx,
23073 );
23074 });
23075
23076 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23077 .expect("Prepare rename was not started")
23078 .await
23079 .expect("Prepare rename failed");
23080
23081 let mut rename_handler =
23082 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23083 let edit = lsp::TextEdit {
23084 range: lsp::Range {
23085 start: lsp::Position {
23086 line: 0,
23087 character: 7,
23088 },
23089 end: lsp::Position {
23090 line: 0,
23091 character: 10,
23092 },
23093 },
23094 new_text: "FooRenamed".to_string(),
23095 };
23096 Ok(Some(lsp::WorkspaceEdit::new(
23097 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
23098 )))
23099 });
23100 let rename_task = cx
23101 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23102 .expect("Confirm rename was not started");
23103 rename_handler.next().await.unwrap();
23104 rename_task.await.expect("Confirm rename failed");
23105 cx.run_until_parked();
23106
23107 // Correct range is renamed, as `surrounding_word` is used to find it.
23108 cx.assert_editor_state(indoc! {"
23109 struct FooRenamedˇ {}
23110 "});
23111}
23112
23113#[gpui::test]
23114async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
23115 init_test(cx, |_| {});
23116 let mut cx = EditorTestContext::new(cx).await;
23117
23118 let language = Arc::new(
23119 Language::new(
23120 LanguageConfig::default(),
23121 Some(tree_sitter_html::LANGUAGE.into()),
23122 )
23123 .with_brackets_query(
23124 r#"
23125 ("<" @open "/>" @close)
23126 ("</" @open ">" @close)
23127 ("<" @open ">" @close)
23128 ("\"" @open "\"" @close)
23129 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
23130 "#,
23131 )
23132 .unwrap(),
23133 );
23134 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23135
23136 cx.set_state(indoc! {"
23137 <span>ˇ</span>
23138 "});
23139 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23140 cx.assert_editor_state(indoc! {"
23141 <span>
23142 ˇ
23143 </span>
23144 "});
23145
23146 cx.set_state(indoc! {"
23147 <span><span></span>ˇ</span>
23148 "});
23149 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23150 cx.assert_editor_state(indoc! {"
23151 <span><span></span>
23152 ˇ</span>
23153 "});
23154
23155 cx.set_state(indoc! {"
23156 <span>ˇ
23157 </span>
23158 "});
23159 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23160 cx.assert_editor_state(indoc! {"
23161 <span>
23162 ˇ
23163 </span>
23164 "});
23165}
23166
23167#[gpui::test(iterations = 10)]
23168async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
23169 init_test(cx, |_| {});
23170
23171 let fs = FakeFs::new(cx.executor());
23172 fs.insert_tree(
23173 path!("/dir"),
23174 json!({
23175 "a.ts": "a",
23176 }),
23177 )
23178 .await;
23179
23180 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
23181 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23182 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23183
23184 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23185 language_registry.add(Arc::new(Language::new(
23186 LanguageConfig {
23187 name: "TypeScript".into(),
23188 matcher: LanguageMatcher {
23189 path_suffixes: vec!["ts".to_string()],
23190 ..Default::default()
23191 },
23192 ..Default::default()
23193 },
23194 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
23195 )));
23196 let mut fake_language_servers = language_registry.register_fake_lsp(
23197 "TypeScript",
23198 FakeLspAdapter {
23199 capabilities: lsp::ServerCapabilities {
23200 code_lens_provider: Some(lsp::CodeLensOptions {
23201 resolve_provider: Some(true),
23202 }),
23203 execute_command_provider: Some(lsp::ExecuteCommandOptions {
23204 commands: vec!["_the/command".to_string()],
23205 ..lsp::ExecuteCommandOptions::default()
23206 }),
23207 ..lsp::ServerCapabilities::default()
23208 },
23209 ..FakeLspAdapter::default()
23210 },
23211 );
23212
23213 let editor = workspace
23214 .update(cx, |workspace, window, cx| {
23215 workspace.open_abs_path(
23216 PathBuf::from(path!("/dir/a.ts")),
23217 OpenOptions::default(),
23218 window,
23219 cx,
23220 )
23221 })
23222 .unwrap()
23223 .await
23224 .unwrap()
23225 .downcast::<Editor>()
23226 .unwrap();
23227 cx.executor().run_until_parked();
23228
23229 let fake_server = fake_language_servers.next().await.unwrap();
23230
23231 let buffer = editor.update(cx, |editor, cx| {
23232 editor
23233 .buffer()
23234 .read(cx)
23235 .as_singleton()
23236 .expect("have opened a single file by path")
23237 });
23238
23239 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
23240 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
23241 drop(buffer_snapshot);
23242 let actions = cx
23243 .update_window(*workspace, |_, window, cx| {
23244 project.code_actions(&buffer, anchor..anchor, window, cx)
23245 })
23246 .unwrap();
23247
23248 fake_server
23249 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23250 Ok(Some(vec![
23251 lsp::CodeLens {
23252 range: lsp::Range::default(),
23253 command: Some(lsp::Command {
23254 title: "Code lens command".to_owned(),
23255 command: "_the/command".to_owned(),
23256 arguments: None,
23257 }),
23258 data: None,
23259 },
23260 lsp::CodeLens {
23261 range: lsp::Range::default(),
23262 command: Some(lsp::Command {
23263 title: "Command not in capabilities".to_owned(),
23264 command: "not in capabilities".to_owned(),
23265 arguments: None,
23266 }),
23267 data: None,
23268 },
23269 lsp::CodeLens {
23270 range: lsp::Range {
23271 start: lsp::Position {
23272 line: 1,
23273 character: 1,
23274 },
23275 end: lsp::Position {
23276 line: 1,
23277 character: 1,
23278 },
23279 },
23280 command: Some(lsp::Command {
23281 title: "Command not in range".to_owned(),
23282 command: "_the/command".to_owned(),
23283 arguments: None,
23284 }),
23285 data: None,
23286 },
23287 ]))
23288 })
23289 .next()
23290 .await;
23291
23292 let actions = actions.await.unwrap();
23293 assert_eq!(
23294 actions.len(),
23295 1,
23296 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
23297 );
23298 let action = actions[0].clone();
23299 let apply = project.update(cx, |project, cx| {
23300 project.apply_code_action(buffer.clone(), action, true, cx)
23301 });
23302
23303 // Resolving the code action does not populate its edits. In absence of
23304 // edits, we must execute the given command.
23305 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
23306 |mut lens, _| async move {
23307 let lens_command = lens.command.as_mut().expect("should have a command");
23308 assert_eq!(lens_command.title, "Code lens command");
23309 lens_command.arguments = Some(vec![json!("the-argument")]);
23310 Ok(lens)
23311 },
23312 );
23313
23314 // While executing the command, the language server sends the editor
23315 // a `workspaceEdit` request.
23316 fake_server
23317 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
23318 let fake = fake_server.clone();
23319 move |params, _| {
23320 assert_eq!(params.command, "_the/command");
23321 let fake = fake.clone();
23322 async move {
23323 fake.server
23324 .request::<lsp::request::ApplyWorkspaceEdit>(
23325 lsp::ApplyWorkspaceEditParams {
23326 label: None,
23327 edit: lsp::WorkspaceEdit {
23328 changes: Some(
23329 [(
23330 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
23331 vec![lsp::TextEdit {
23332 range: lsp::Range::new(
23333 lsp::Position::new(0, 0),
23334 lsp::Position::new(0, 0),
23335 ),
23336 new_text: "X".into(),
23337 }],
23338 )]
23339 .into_iter()
23340 .collect(),
23341 ),
23342 ..lsp::WorkspaceEdit::default()
23343 },
23344 },
23345 )
23346 .await
23347 .into_response()
23348 .unwrap();
23349 Ok(Some(json!(null)))
23350 }
23351 }
23352 })
23353 .next()
23354 .await;
23355
23356 // Applying the code lens command returns a project transaction containing the edits
23357 // sent by the language server in its `workspaceEdit` request.
23358 let transaction = apply.await.unwrap();
23359 assert!(transaction.0.contains_key(&buffer));
23360 buffer.update(cx, |buffer, cx| {
23361 assert_eq!(buffer.text(), "Xa");
23362 buffer.undo(cx);
23363 assert_eq!(buffer.text(), "a");
23364 });
23365
23366 let actions_after_edits = cx
23367 .update_window(*workspace, |_, window, cx| {
23368 project.code_actions(&buffer, anchor..anchor, window, cx)
23369 })
23370 .unwrap()
23371 .await
23372 .unwrap();
23373 assert_eq!(
23374 actions, actions_after_edits,
23375 "For the same selection, same code lens actions should be returned"
23376 );
23377
23378 let _responses =
23379 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23380 panic!("No more code lens requests are expected");
23381 });
23382 editor.update_in(cx, |editor, window, cx| {
23383 editor.select_all(&SelectAll, window, cx);
23384 });
23385 cx.executor().run_until_parked();
23386 let new_actions = cx
23387 .update_window(*workspace, |_, window, cx| {
23388 project.code_actions(&buffer, anchor..anchor, window, cx)
23389 })
23390 .unwrap()
23391 .await
23392 .unwrap();
23393 assert_eq!(
23394 actions, new_actions,
23395 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
23396 );
23397}
23398
23399#[gpui::test]
23400async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
23401 init_test(cx, |_| {});
23402
23403 let fs = FakeFs::new(cx.executor());
23404 let main_text = r#"fn main() {
23405println!("1");
23406println!("2");
23407println!("3");
23408println!("4");
23409println!("5");
23410}"#;
23411 let lib_text = "mod foo {}";
23412 fs.insert_tree(
23413 path!("/a"),
23414 json!({
23415 "lib.rs": lib_text,
23416 "main.rs": main_text,
23417 }),
23418 )
23419 .await;
23420
23421 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23422 let (workspace, cx) =
23423 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23424 let worktree_id = workspace.update(cx, |workspace, cx| {
23425 workspace.project().update(cx, |project, cx| {
23426 project.worktrees(cx).next().unwrap().read(cx).id()
23427 })
23428 });
23429
23430 let expected_ranges = vec![
23431 Point::new(0, 0)..Point::new(0, 0),
23432 Point::new(1, 0)..Point::new(1, 1),
23433 Point::new(2, 0)..Point::new(2, 2),
23434 Point::new(3, 0)..Point::new(3, 3),
23435 ];
23436
23437 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23438 let editor_1 = workspace
23439 .update_in(cx, |workspace, window, cx| {
23440 workspace.open_path(
23441 (worktree_id, rel_path("main.rs")),
23442 Some(pane_1.downgrade()),
23443 true,
23444 window,
23445 cx,
23446 )
23447 })
23448 .unwrap()
23449 .await
23450 .downcast::<Editor>()
23451 .unwrap();
23452 pane_1.update(cx, |pane, cx| {
23453 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23454 open_editor.update(cx, |editor, cx| {
23455 assert_eq!(
23456 editor.display_text(cx),
23457 main_text,
23458 "Original main.rs text on initial open",
23459 );
23460 assert_eq!(
23461 editor
23462 .selections
23463 .all::<Point>(cx)
23464 .into_iter()
23465 .map(|s| s.range())
23466 .collect::<Vec<_>>(),
23467 vec![Point::zero()..Point::zero()],
23468 "Default selections on initial open",
23469 );
23470 })
23471 });
23472 editor_1.update_in(cx, |editor, window, cx| {
23473 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23474 s.select_ranges(expected_ranges.clone());
23475 });
23476 });
23477
23478 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
23479 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
23480 });
23481 let editor_2 = workspace
23482 .update_in(cx, |workspace, window, cx| {
23483 workspace.open_path(
23484 (worktree_id, rel_path("main.rs")),
23485 Some(pane_2.downgrade()),
23486 true,
23487 window,
23488 cx,
23489 )
23490 })
23491 .unwrap()
23492 .await
23493 .downcast::<Editor>()
23494 .unwrap();
23495 pane_2.update(cx, |pane, cx| {
23496 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23497 open_editor.update(cx, |editor, cx| {
23498 assert_eq!(
23499 editor.display_text(cx),
23500 main_text,
23501 "Original main.rs text on initial open in another panel",
23502 );
23503 assert_eq!(
23504 editor
23505 .selections
23506 .all::<Point>(cx)
23507 .into_iter()
23508 .map(|s| s.range())
23509 .collect::<Vec<_>>(),
23510 vec![Point::zero()..Point::zero()],
23511 "Default selections on initial open in another panel",
23512 );
23513 })
23514 });
23515
23516 editor_2.update_in(cx, |editor, window, cx| {
23517 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
23518 });
23519
23520 let _other_editor_1 = workspace
23521 .update_in(cx, |workspace, window, cx| {
23522 workspace.open_path(
23523 (worktree_id, rel_path("lib.rs")),
23524 Some(pane_1.downgrade()),
23525 true,
23526 window,
23527 cx,
23528 )
23529 })
23530 .unwrap()
23531 .await
23532 .downcast::<Editor>()
23533 .unwrap();
23534 pane_1
23535 .update_in(cx, |pane, window, cx| {
23536 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23537 })
23538 .await
23539 .unwrap();
23540 drop(editor_1);
23541 pane_1.update(cx, |pane, cx| {
23542 pane.active_item()
23543 .unwrap()
23544 .downcast::<Editor>()
23545 .unwrap()
23546 .update(cx, |editor, cx| {
23547 assert_eq!(
23548 editor.display_text(cx),
23549 lib_text,
23550 "Other file should be open and active",
23551 );
23552 });
23553 assert_eq!(pane.items().count(), 1, "No other editors should be open");
23554 });
23555
23556 let _other_editor_2 = workspace
23557 .update_in(cx, |workspace, window, cx| {
23558 workspace.open_path(
23559 (worktree_id, rel_path("lib.rs")),
23560 Some(pane_2.downgrade()),
23561 true,
23562 window,
23563 cx,
23564 )
23565 })
23566 .unwrap()
23567 .await
23568 .downcast::<Editor>()
23569 .unwrap();
23570 pane_2
23571 .update_in(cx, |pane, window, cx| {
23572 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23573 })
23574 .await
23575 .unwrap();
23576 drop(editor_2);
23577 pane_2.update(cx, |pane, cx| {
23578 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23579 open_editor.update(cx, |editor, cx| {
23580 assert_eq!(
23581 editor.display_text(cx),
23582 lib_text,
23583 "Other file should be open and active in another panel too",
23584 );
23585 });
23586 assert_eq!(
23587 pane.items().count(),
23588 1,
23589 "No other editors should be open in another pane",
23590 );
23591 });
23592
23593 let _editor_1_reopened = workspace
23594 .update_in(cx, |workspace, window, cx| {
23595 workspace.open_path(
23596 (worktree_id, rel_path("main.rs")),
23597 Some(pane_1.downgrade()),
23598 true,
23599 window,
23600 cx,
23601 )
23602 })
23603 .unwrap()
23604 .await
23605 .downcast::<Editor>()
23606 .unwrap();
23607 let _editor_2_reopened = workspace
23608 .update_in(cx, |workspace, window, cx| {
23609 workspace.open_path(
23610 (worktree_id, rel_path("main.rs")),
23611 Some(pane_2.downgrade()),
23612 true,
23613 window,
23614 cx,
23615 )
23616 })
23617 .unwrap()
23618 .await
23619 .downcast::<Editor>()
23620 .unwrap();
23621 pane_1.update(cx, |pane, cx| {
23622 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23623 open_editor.update(cx, |editor, cx| {
23624 assert_eq!(
23625 editor.display_text(cx),
23626 main_text,
23627 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
23628 );
23629 assert_eq!(
23630 editor
23631 .selections
23632 .all::<Point>(cx)
23633 .into_iter()
23634 .map(|s| s.range())
23635 .collect::<Vec<_>>(),
23636 expected_ranges,
23637 "Previous editor in the 1st panel had selections and should get them restored on reopen",
23638 );
23639 })
23640 });
23641 pane_2.update(cx, |pane, cx| {
23642 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23643 open_editor.update(cx, |editor, cx| {
23644 assert_eq!(
23645 editor.display_text(cx),
23646 r#"fn main() {
23647⋯rintln!("1");
23648⋯intln!("2");
23649⋯ntln!("3");
23650println!("4");
23651println!("5");
23652}"#,
23653 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
23654 );
23655 assert_eq!(
23656 editor
23657 .selections
23658 .all::<Point>(cx)
23659 .into_iter()
23660 .map(|s| s.range())
23661 .collect::<Vec<_>>(),
23662 vec![Point::zero()..Point::zero()],
23663 "Previous editor in the 2nd pane had no selections changed hence should restore none",
23664 );
23665 })
23666 });
23667}
23668
23669#[gpui::test]
23670async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
23671 init_test(cx, |_| {});
23672
23673 let fs = FakeFs::new(cx.executor());
23674 let main_text = r#"fn main() {
23675println!("1");
23676println!("2");
23677println!("3");
23678println!("4");
23679println!("5");
23680}"#;
23681 let lib_text = "mod foo {}";
23682 fs.insert_tree(
23683 path!("/a"),
23684 json!({
23685 "lib.rs": lib_text,
23686 "main.rs": main_text,
23687 }),
23688 )
23689 .await;
23690
23691 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23692 let (workspace, cx) =
23693 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23694 let worktree_id = workspace.update(cx, |workspace, cx| {
23695 workspace.project().update(cx, |project, cx| {
23696 project.worktrees(cx).next().unwrap().read(cx).id()
23697 })
23698 });
23699
23700 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23701 let editor = workspace
23702 .update_in(cx, |workspace, window, cx| {
23703 workspace.open_path(
23704 (worktree_id, rel_path("main.rs")),
23705 Some(pane.downgrade()),
23706 true,
23707 window,
23708 cx,
23709 )
23710 })
23711 .unwrap()
23712 .await
23713 .downcast::<Editor>()
23714 .unwrap();
23715 pane.update(cx, |pane, cx| {
23716 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23717 open_editor.update(cx, |editor, cx| {
23718 assert_eq!(
23719 editor.display_text(cx),
23720 main_text,
23721 "Original main.rs text on initial open",
23722 );
23723 })
23724 });
23725 editor.update_in(cx, |editor, window, cx| {
23726 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
23727 });
23728
23729 cx.update_global(|store: &mut SettingsStore, cx| {
23730 store.update_user_settings(cx, |s| {
23731 s.workspace.restore_on_file_reopen = Some(false);
23732 });
23733 });
23734 editor.update_in(cx, |editor, window, cx| {
23735 editor.fold_ranges(
23736 vec![
23737 Point::new(1, 0)..Point::new(1, 1),
23738 Point::new(2, 0)..Point::new(2, 2),
23739 Point::new(3, 0)..Point::new(3, 3),
23740 ],
23741 false,
23742 window,
23743 cx,
23744 );
23745 });
23746 pane.update_in(cx, |pane, window, cx| {
23747 pane.close_all_items(&CloseAllItems::default(), window, cx)
23748 })
23749 .await
23750 .unwrap();
23751 pane.update(cx, |pane, _| {
23752 assert!(pane.active_item().is_none());
23753 });
23754 cx.update_global(|store: &mut SettingsStore, cx| {
23755 store.update_user_settings(cx, |s| {
23756 s.workspace.restore_on_file_reopen = Some(true);
23757 });
23758 });
23759
23760 let _editor_reopened = workspace
23761 .update_in(cx, |workspace, window, cx| {
23762 workspace.open_path(
23763 (worktree_id, rel_path("main.rs")),
23764 Some(pane.downgrade()),
23765 true,
23766 window,
23767 cx,
23768 )
23769 })
23770 .unwrap()
23771 .await
23772 .downcast::<Editor>()
23773 .unwrap();
23774 pane.update(cx, |pane, cx| {
23775 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23776 open_editor.update(cx, |editor, cx| {
23777 assert_eq!(
23778 editor.display_text(cx),
23779 main_text,
23780 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
23781 );
23782 })
23783 });
23784}
23785
23786#[gpui::test]
23787async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
23788 struct EmptyModalView {
23789 focus_handle: gpui::FocusHandle,
23790 }
23791 impl EventEmitter<DismissEvent> for EmptyModalView {}
23792 impl Render for EmptyModalView {
23793 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
23794 div()
23795 }
23796 }
23797 impl Focusable for EmptyModalView {
23798 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
23799 self.focus_handle.clone()
23800 }
23801 }
23802 impl workspace::ModalView for EmptyModalView {}
23803 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
23804 EmptyModalView {
23805 focus_handle: cx.focus_handle(),
23806 }
23807 }
23808
23809 init_test(cx, |_| {});
23810
23811 let fs = FakeFs::new(cx.executor());
23812 let project = Project::test(fs, [], cx).await;
23813 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23814 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
23815 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23816 let editor = cx.new_window_entity(|window, cx| {
23817 Editor::new(
23818 EditorMode::full(),
23819 buffer,
23820 Some(project.clone()),
23821 window,
23822 cx,
23823 )
23824 });
23825 workspace
23826 .update(cx, |workspace, window, cx| {
23827 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
23828 })
23829 .unwrap();
23830 editor.update_in(cx, |editor, window, cx| {
23831 editor.open_context_menu(&OpenContextMenu, window, cx);
23832 assert!(editor.mouse_context_menu.is_some());
23833 });
23834 workspace
23835 .update(cx, |workspace, window, cx| {
23836 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
23837 })
23838 .unwrap();
23839 cx.read(|cx| {
23840 assert!(editor.read(cx).mouse_context_menu.is_none());
23841 });
23842}
23843
23844fn set_linked_edit_ranges(
23845 opening: (Point, Point),
23846 closing: (Point, Point),
23847 editor: &mut Editor,
23848 cx: &mut Context<Editor>,
23849) {
23850 let Some((buffer, _)) = editor
23851 .buffer
23852 .read(cx)
23853 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
23854 else {
23855 panic!("Failed to get buffer for selection position");
23856 };
23857 let buffer = buffer.read(cx);
23858 let buffer_id = buffer.remote_id();
23859 let opening_range = buffer.anchor_before(opening.0)..buffer.anchor_after(opening.1);
23860 let closing_range = buffer.anchor_before(closing.0)..buffer.anchor_after(closing.1);
23861 let mut linked_ranges = HashMap::default();
23862 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
23863 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
23864}
23865
23866#[gpui::test]
23867async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
23868 init_test(cx, |_| {});
23869
23870 let fs = FakeFs::new(cx.executor());
23871 fs.insert_file(path!("/file.html"), Default::default())
23872 .await;
23873
23874 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
23875
23876 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23877 let html_language = Arc::new(Language::new(
23878 LanguageConfig {
23879 name: "HTML".into(),
23880 matcher: LanguageMatcher {
23881 path_suffixes: vec!["html".to_string()],
23882 ..LanguageMatcher::default()
23883 },
23884 brackets: BracketPairConfig {
23885 pairs: vec![BracketPair {
23886 start: "<".into(),
23887 end: ">".into(),
23888 close: true,
23889 ..Default::default()
23890 }],
23891 ..Default::default()
23892 },
23893 ..Default::default()
23894 },
23895 Some(tree_sitter_html::LANGUAGE.into()),
23896 ));
23897 language_registry.add(html_language);
23898 let mut fake_servers = language_registry.register_fake_lsp(
23899 "HTML",
23900 FakeLspAdapter {
23901 capabilities: lsp::ServerCapabilities {
23902 completion_provider: Some(lsp::CompletionOptions {
23903 resolve_provider: Some(true),
23904 ..Default::default()
23905 }),
23906 ..Default::default()
23907 },
23908 ..Default::default()
23909 },
23910 );
23911
23912 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23913 let cx = &mut VisualTestContext::from_window(*workspace, cx);
23914
23915 let worktree_id = workspace
23916 .update(cx, |workspace, _window, cx| {
23917 workspace.project().update(cx, |project, cx| {
23918 project.worktrees(cx).next().unwrap().read(cx).id()
23919 })
23920 })
23921 .unwrap();
23922 project
23923 .update(cx, |project, cx| {
23924 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
23925 })
23926 .await
23927 .unwrap();
23928 let editor = workspace
23929 .update(cx, |workspace, window, cx| {
23930 workspace.open_path((worktree_id, rel_path("file.html")), None, true, window, cx)
23931 })
23932 .unwrap()
23933 .await
23934 .unwrap()
23935 .downcast::<Editor>()
23936 .unwrap();
23937
23938 let fake_server = fake_servers.next().await.unwrap();
23939 editor.update_in(cx, |editor, window, cx| {
23940 editor.set_text("<ad></ad>", window, cx);
23941 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
23942 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
23943 });
23944 set_linked_edit_ranges(
23945 (Point::new(0, 1), Point::new(0, 3)),
23946 (Point::new(0, 6), Point::new(0, 8)),
23947 editor,
23948 cx,
23949 );
23950 });
23951 let mut completion_handle =
23952 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
23953 Ok(Some(lsp::CompletionResponse::Array(vec![
23954 lsp::CompletionItem {
23955 label: "head".to_string(),
23956 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
23957 lsp::InsertReplaceEdit {
23958 new_text: "head".to_string(),
23959 insert: lsp::Range::new(
23960 lsp::Position::new(0, 1),
23961 lsp::Position::new(0, 3),
23962 ),
23963 replace: lsp::Range::new(
23964 lsp::Position::new(0, 1),
23965 lsp::Position::new(0, 3),
23966 ),
23967 },
23968 )),
23969 ..Default::default()
23970 },
23971 ])))
23972 });
23973 editor.update_in(cx, |editor, window, cx| {
23974 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
23975 });
23976 cx.run_until_parked();
23977 completion_handle.next().await.unwrap();
23978 editor.update(cx, |editor, _| {
23979 assert!(
23980 editor.context_menu_visible(),
23981 "Completion menu should be visible"
23982 );
23983 });
23984 editor.update_in(cx, |editor, window, cx| {
23985 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
23986 });
23987 cx.executor().run_until_parked();
23988 editor.update(cx, |editor, cx| {
23989 assert_eq!(editor.text(cx), "<head></head>");
23990 });
23991}
23992
23993#[gpui::test]
23994async fn test_linked_edits_on_typing_punctuation(cx: &mut TestAppContext) {
23995 init_test(cx, |_| {});
23996
23997 let mut cx = EditorTestContext::new(cx).await;
23998 let language = Arc::new(Language::new(
23999 LanguageConfig {
24000 name: "TSX".into(),
24001 matcher: LanguageMatcher {
24002 path_suffixes: vec!["tsx".to_string()],
24003 ..LanguageMatcher::default()
24004 },
24005 brackets: BracketPairConfig {
24006 pairs: vec![BracketPair {
24007 start: "<".into(),
24008 end: ">".into(),
24009 close: true,
24010 ..Default::default()
24011 }],
24012 ..Default::default()
24013 },
24014 linked_edit_characters: HashSet::from_iter(['.']),
24015 ..Default::default()
24016 },
24017 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
24018 ));
24019 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24020
24021 // Test typing > does not extend linked pair
24022 cx.set_state("<divˇ<div></div>");
24023 cx.update_editor(|editor, _, cx| {
24024 set_linked_edit_ranges(
24025 (Point::new(0, 1), Point::new(0, 4)),
24026 (Point::new(0, 11), Point::new(0, 14)),
24027 editor,
24028 cx,
24029 );
24030 });
24031 cx.update_editor(|editor, window, cx| {
24032 editor.handle_input(">", window, cx);
24033 });
24034 cx.assert_editor_state("<div>ˇ<div></div>");
24035
24036 // Test typing . do extend linked pair
24037 cx.set_state("<Animatedˇ></Animated>");
24038 cx.update_editor(|editor, _, cx| {
24039 set_linked_edit_ranges(
24040 (Point::new(0, 1), Point::new(0, 9)),
24041 (Point::new(0, 12), Point::new(0, 20)),
24042 editor,
24043 cx,
24044 );
24045 });
24046 cx.update_editor(|editor, window, cx| {
24047 editor.handle_input(".", window, cx);
24048 });
24049 cx.assert_editor_state("<Animated.ˇ></Animated.>");
24050 cx.update_editor(|editor, _, cx| {
24051 set_linked_edit_ranges(
24052 (Point::new(0, 1), Point::new(0, 10)),
24053 (Point::new(0, 13), Point::new(0, 21)),
24054 editor,
24055 cx,
24056 );
24057 });
24058 cx.update_editor(|editor, window, cx| {
24059 editor.handle_input("V", window, cx);
24060 });
24061 cx.assert_editor_state("<Animated.Vˇ></Animated.V>");
24062}
24063
24064#[gpui::test]
24065async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
24066 init_test(cx, |_| {});
24067
24068 let fs = FakeFs::new(cx.executor());
24069 fs.insert_tree(
24070 path!("/root"),
24071 json!({
24072 "a": {
24073 "main.rs": "fn main() {}",
24074 },
24075 "foo": {
24076 "bar": {
24077 "external_file.rs": "pub mod external {}",
24078 }
24079 }
24080 }),
24081 )
24082 .await;
24083
24084 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
24085 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24086 language_registry.add(rust_lang());
24087 let _fake_servers = language_registry.register_fake_lsp(
24088 "Rust",
24089 FakeLspAdapter {
24090 ..FakeLspAdapter::default()
24091 },
24092 );
24093 let (workspace, cx) =
24094 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24095 let worktree_id = workspace.update(cx, |workspace, cx| {
24096 workspace.project().update(cx, |project, cx| {
24097 project.worktrees(cx).next().unwrap().read(cx).id()
24098 })
24099 });
24100
24101 let assert_language_servers_count =
24102 |expected: usize, context: &str, cx: &mut VisualTestContext| {
24103 project.update(cx, |project, cx| {
24104 let current = project
24105 .lsp_store()
24106 .read(cx)
24107 .as_local()
24108 .unwrap()
24109 .language_servers
24110 .len();
24111 assert_eq!(expected, current, "{context}");
24112 });
24113 };
24114
24115 assert_language_servers_count(
24116 0,
24117 "No servers should be running before any file is open",
24118 cx,
24119 );
24120 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24121 let main_editor = workspace
24122 .update_in(cx, |workspace, window, cx| {
24123 workspace.open_path(
24124 (worktree_id, rel_path("main.rs")),
24125 Some(pane.downgrade()),
24126 true,
24127 window,
24128 cx,
24129 )
24130 })
24131 .unwrap()
24132 .await
24133 .downcast::<Editor>()
24134 .unwrap();
24135 pane.update(cx, |pane, cx| {
24136 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24137 open_editor.update(cx, |editor, cx| {
24138 assert_eq!(
24139 editor.display_text(cx),
24140 "fn main() {}",
24141 "Original main.rs text on initial open",
24142 );
24143 });
24144 assert_eq!(open_editor, main_editor);
24145 });
24146 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
24147
24148 let external_editor = workspace
24149 .update_in(cx, |workspace, window, cx| {
24150 workspace.open_abs_path(
24151 PathBuf::from("/root/foo/bar/external_file.rs"),
24152 OpenOptions::default(),
24153 window,
24154 cx,
24155 )
24156 })
24157 .await
24158 .expect("opening external file")
24159 .downcast::<Editor>()
24160 .expect("downcasted external file's open element to editor");
24161 pane.update(cx, |pane, cx| {
24162 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24163 open_editor.update(cx, |editor, cx| {
24164 assert_eq!(
24165 editor.display_text(cx),
24166 "pub mod external {}",
24167 "External file is open now",
24168 );
24169 });
24170 assert_eq!(open_editor, external_editor);
24171 });
24172 assert_language_servers_count(
24173 1,
24174 "Second, external, *.rs file should join the existing server",
24175 cx,
24176 );
24177
24178 pane.update_in(cx, |pane, window, cx| {
24179 pane.close_active_item(&CloseActiveItem::default(), window, cx)
24180 })
24181 .await
24182 .unwrap();
24183 pane.update_in(cx, |pane, window, cx| {
24184 pane.navigate_backward(&Default::default(), window, cx);
24185 });
24186 cx.run_until_parked();
24187 pane.update(cx, |pane, cx| {
24188 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24189 open_editor.update(cx, |editor, cx| {
24190 assert_eq!(
24191 editor.display_text(cx),
24192 "pub mod external {}",
24193 "External file is open now",
24194 );
24195 });
24196 });
24197 assert_language_servers_count(
24198 1,
24199 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
24200 cx,
24201 );
24202
24203 cx.update(|_, cx| {
24204 workspace::reload(cx);
24205 });
24206 assert_language_servers_count(
24207 1,
24208 "After reloading the worktree with local and external files opened, only one project should be started",
24209 cx,
24210 );
24211}
24212
24213#[gpui::test]
24214async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
24215 init_test(cx, |_| {});
24216
24217 let mut cx = EditorTestContext::new(cx).await;
24218 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24219 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24220
24221 // test cursor move to start of each line on tab
24222 // for `if`, `elif`, `else`, `while`, `with` and `for`
24223 cx.set_state(indoc! {"
24224 def main():
24225 ˇ for item in items:
24226 ˇ while item.active:
24227 ˇ if item.value > 10:
24228 ˇ continue
24229 ˇ elif item.value < 0:
24230 ˇ break
24231 ˇ else:
24232 ˇ with item.context() as ctx:
24233 ˇ yield count
24234 ˇ else:
24235 ˇ log('while else')
24236 ˇ else:
24237 ˇ log('for else')
24238 "});
24239 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24240 cx.assert_editor_state(indoc! {"
24241 def main():
24242 ˇfor item in items:
24243 ˇwhile item.active:
24244 ˇif item.value > 10:
24245 ˇcontinue
24246 ˇelif item.value < 0:
24247 ˇbreak
24248 ˇelse:
24249 ˇwith item.context() as ctx:
24250 ˇyield count
24251 ˇelse:
24252 ˇlog('while else')
24253 ˇelse:
24254 ˇlog('for else')
24255 "});
24256 // test relative indent is preserved when tab
24257 // for `if`, `elif`, `else`, `while`, `with` and `for`
24258 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24259 cx.assert_editor_state(indoc! {"
24260 def main():
24261 ˇfor item in items:
24262 ˇwhile item.active:
24263 ˇif item.value > 10:
24264 ˇcontinue
24265 ˇelif item.value < 0:
24266 ˇbreak
24267 ˇelse:
24268 ˇwith item.context() as ctx:
24269 ˇyield count
24270 ˇelse:
24271 ˇlog('while else')
24272 ˇelse:
24273 ˇlog('for else')
24274 "});
24275
24276 // test cursor move to start of each line on tab
24277 // for `try`, `except`, `else`, `finally`, `match` and `def`
24278 cx.set_state(indoc! {"
24279 def main():
24280 ˇ try:
24281 ˇ fetch()
24282 ˇ except ValueError:
24283 ˇ handle_error()
24284 ˇ else:
24285 ˇ match value:
24286 ˇ case _:
24287 ˇ finally:
24288 ˇ def status():
24289 ˇ return 0
24290 "});
24291 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24292 cx.assert_editor_state(indoc! {"
24293 def main():
24294 ˇtry:
24295 ˇfetch()
24296 ˇexcept ValueError:
24297 ˇhandle_error()
24298 ˇelse:
24299 ˇmatch value:
24300 ˇcase _:
24301 ˇfinally:
24302 ˇdef status():
24303 ˇreturn 0
24304 "});
24305 // test relative indent is preserved when tab
24306 // for `try`, `except`, `else`, `finally`, `match` and `def`
24307 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24308 cx.assert_editor_state(indoc! {"
24309 def main():
24310 ˇtry:
24311 ˇfetch()
24312 ˇexcept ValueError:
24313 ˇhandle_error()
24314 ˇelse:
24315 ˇmatch value:
24316 ˇcase _:
24317 ˇfinally:
24318 ˇdef status():
24319 ˇreturn 0
24320 "});
24321}
24322
24323#[gpui::test]
24324async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
24325 init_test(cx, |_| {});
24326
24327 let mut cx = EditorTestContext::new(cx).await;
24328 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24329 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24330
24331 // test `else` auto outdents when typed inside `if` block
24332 cx.set_state(indoc! {"
24333 def main():
24334 if i == 2:
24335 return
24336 ˇ
24337 "});
24338 cx.update_editor(|editor, window, cx| {
24339 editor.handle_input("else:", window, cx);
24340 });
24341 cx.assert_editor_state(indoc! {"
24342 def main():
24343 if i == 2:
24344 return
24345 else:ˇ
24346 "});
24347
24348 // test `except` auto outdents when typed inside `try` block
24349 cx.set_state(indoc! {"
24350 def main():
24351 try:
24352 i = 2
24353 ˇ
24354 "});
24355 cx.update_editor(|editor, window, cx| {
24356 editor.handle_input("except:", window, cx);
24357 });
24358 cx.assert_editor_state(indoc! {"
24359 def main():
24360 try:
24361 i = 2
24362 except:ˇ
24363 "});
24364
24365 // test `else` auto outdents when typed inside `except` block
24366 cx.set_state(indoc! {"
24367 def main():
24368 try:
24369 i = 2
24370 except:
24371 j = 2
24372 ˇ
24373 "});
24374 cx.update_editor(|editor, window, cx| {
24375 editor.handle_input("else:", window, cx);
24376 });
24377 cx.assert_editor_state(indoc! {"
24378 def main():
24379 try:
24380 i = 2
24381 except:
24382 j = 2
24383 else:ˇ
24384 "});
24385
24386 // test `finally` auto outdents when typed inside `else` block
24387 cx.set_state(indoc! {"
24388 def main():
24389 try:
24390 i = 2
24391 except:
24392 j = 2
24393 else:
24394 k = 2
24395 ˇ
24396 "});
24397 cx.update_editor(|editor, window, cx| {
24398 editor.handle_input("finally:", window, cx);
24399 });
24400 cx.assert_editor_state(indoc! {"
24401 def main():
24402 try:
24403 i = 2
24404 except:
24405 j = 2
24406 else:
24407 k = 2
24408 finally:ˇ
24409 "});
24410
24411 // test `else` does not outdents when typed inside `except` block right after for block
24412 cx.set_state(indoc! {"
24413 def main():
24414 try:
24415 i = 2
24416 except:
24417 for i in range(n):
24418 pass
24419 ˇ
24420 "});
24421 cx.update_editor(|editor, window, cx| {
24422 editor.handle_input("else:", window, cx);
24423 });
24424 cx.assert_editor_state(indoc! {"
24425 def main():
24426 try:
24427 i = 2
24428 except:
24429 for i in range(n):
24430 pass
24431 else:ˇ
24432 "});
24433
24434 // test `finally` auto outdents when typed inside `else` block right after for block
24435 cx.set_state(indoc! {"
24436 def main():
24437 try:
24438 i = 2
24439 except:
24440 j = 2
24441 else:
24442 for i in range(n):
24443 pass
24444 ˇ
24445 "});
24446 cx.update_editor(|editor, window, cx| {
24447 editor.handle_input("finally:", window, cx);
24448 });
24449 cx.assert_editor_state(indoc! {"
24450 def main():
24451 try:
24452 i = 2
24453 except:
24454 j = 2
24455 else:
24456 for i in range(n):
24457 pass
24458 finally:ˇ
24459 "});
24460
24461 // test `except` outdents to inner "try" block
24462 cx.set_state(indoc! {"
24463 def main():
24464 try:
24465 i = 2
24466 if i == 2:
24467 try:
24468 i = 3
24469 ˇ
24470 "});
24471 cx.update_editor(|editor, window, cx| {
24472 editor.handle_input("except:", window, cx);
24473 });
24474 cx.assert_editor_state(indoc! {"
24475 def main():
24476 try:
24477 i = 2
24478 if i == 2:
24479 try:
24480 i = 3
24481 except:ˇ
24482 "});
24483
24484 // test `except` outdents to outer "try" block
24485 cx.set_state(indoc! {"
24486 def main():
24487 try:
24488 i = 2
24489 if i == 2:
24490 try:
24491 i = 3
24492 ˇ
24493 "});
24494 cx.update_editor(|editor, window, cx| {
24495 editor.handle_input("except:", window, cx);
24496 });
24497 cx.assert_editor_state(indoc! {"
24498 def main():
24499 try:
24500 i = 2
24501 if i == 2:
24502 try:
24503 i = 3
24504 except:ˇ
24505 "});
24506
24507 // test `else` stays at correct indent when typed after `for` block
24508 cx.set_state(indoc! {"
24509 def main():
24510 for i in range(10):
24511 if i == 3:
24512 break
24513 ˇ
24514 "});
24515 cx.update_editor(|editor, window, cx| {
24516 editor.handle_input("else:", window, cx);
24517 });
24518 cx.assert_editor_state(indoc! {"
24519 def main():
24520 for i in range(10):
24521 if i == 3:
24522 break
24523 else:ˇ
24524 "});
24525
24526 // test does not outdent on typing after line with square brackets
24527 cx.set_state(indoc! {"
24528 def f() -> list[str]:
24529 ˇ
24530 "});
24531 cx.update_editor(|editor, window, cx| {
24532 editor.handle_input("a", window, cx);
24533 });
24534 cx.assert_editor_state(indoc! {"
24535 def f() -> list[str]:
24536 aˇ
24537 "});
24538
24539 // test does not outdent on typing : after case keyword
24540 cx.set_state(indoc! {"
24541 match 1:
24542 caseˇ
24543 "});
24544 cx.update_editor(|editor, window, cx| {
24545 editor.handle_input(":", window, cx);
24546 });
24547 cx.assert_editor_state(indoc! {"
24548 match 1:
24549 case:ˇ
24550 "});
24551}
24552
24553#[gpui::test]
24554async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
24555 init_test(cx, |_| {});
24556 update_test_language_settings(cx, |settings| {
24557 settings.defaults.extend_comment_on_newline = Some(false);
24558 });
24559 let mut cx = EditorTestContext::new(cx).await;
24560 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24561 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24562
24563 // test correct indent after newline on comment
24564 cx.set_state(indoc! {"
24565 # COMMENT:ˇ
24566 "});
24567 cx.update_editor(|editor, window, cx| {
24568 editor.newline(&Newline, window, cx);
24569 });
24570 cx.assert_editor_state(indoc! {"
24571 # COMMENT:
24572 ˇ
24573 "});
24574
24575 // test correct indent after newline in brackets
24576 cx.set_state(indoc! {"
24577 {ˇ}
24578 "});
24579 cx.update_editor(|editor, window, cx| {
24580 editor.newline(&Newline, window, cx);
24581 });
24582 cx.run_until_parked();
24583 cx.assert_editor_state(indoc! {"
24584 {
24585 ˇ
24586 }
24587 "});
24588
24589 cx.set_state(indoc! {"
24590 (ˇ)
24591 "});
24592 cx.update_editor(|editor, window, cx| {
24593 editor.newline(&Newline, window, cx);
24594 });
24595 cx.run_until_parked();
24596 cx.assert_editor_state(indoc! {"
24597 (
24598 ˇ
24599 )
24600 "});
24601
24602 // do not indent after empty lists or dictionaries
24603 cx.set_state(indoc! {"
24604 a = []ˇ
24605 "});
24606 cx.update_editor(|editor, window, cx| {
24607 editor.newline(&Newline, window, cx);
24608 });
24609 cx.run_until_parked();
24610 cx.assert_editor_state(indoc! {"
24611 a = []
24612 ˇ
24613 "});
24614}
24615
24616#[gpui::test]
24617async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
24618 init_test(cx, |_| {});
24619
24620 let mut cx = EditorTestContext::new(cx).await;
24621 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24622 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24623
24624 // test cursor move to start of each line on tab
24625 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
24626 cx.set_state(indoc! {"
24627 function main() {
24628 ˇ for item in $items; do
24629 ˇ while [ -n \"$item\" ]; do
24630 ˇ if [ \"$value\" -gt 10 ]; then
24631 ˇ continue
24632 ˇ elif [ \"$value\" -lt 0 ]; then
24633 ˇ break
24634 ˇ else
24635 ˇ echo \"$item\"
24636 ˇ fi
24637 ˇ done
24638 ˇ done
24639 ˇ}
24640 "});
24641 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24642 cx.assert_editor_state(indoc! {"
24643 function main() {
24644 ˇfor item in $items; do
24645 ˇwhile [ -n \"$item\" ]; do
24646 ˇif [ \"$value\" -gt 10 ]; then
24647 ˇcontinue
24648 ˇelif [ \"$value\" -lt 0 ]; then
24649 ˇbreak
24650 ˇelse
24651 ˇecho \"$item\"
24652 ˇfi
24653 ˇdone
24654 ˇdone
24655 ˇ}
24656 "});
24657 // test relative indent is preserved when tab
24658 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24659 cx.assert_editor_state(indoc! {"
24660 function main() {
24661 ˇfor item in $items; do
24662 ˇwhile [ -n \"$item\" ]; do
24663 ˇif [ \"$value\" -gt 10 ]; then
24664 ˇcontinue
24665 ˇelif [ \"$value\" -lt 0 ]; then
24666 ˇbreak
24667 ˇelse
24668 ˇecho \"$item\"
24669 ˇfi
24670 ˇdone
24671 ˇdone
24672 ˇ}
24673 "});
24674
24675 // test cursor move to start of each line on tab
24676 // for `case` statement with patterns
24677 cx.set_state(indoc! {"
24678 function handle() {
24679 ˇ case \"$1\" in
24680 ˇ start)
24681 ˇ echo \"a\"
24682 ˇ ;;
24683 ˇ stop)
24684 ˇ echo \"b\"
24685 ˇ ;;
24686 ˇ *)
24687 ˇ echo \"c\"
24688 ˇ ;;
24689 ˇ esac
24690 ˇ}
24691 "});
24692 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24693 cx.assert_editor_state(indoc! {"
24694 function handle() {
24695 ˇcase \"$1\" in
24696 ˇstart)
24697 ˇecho \"a\"
24698 ˇ;;
24699 ˇstop)
24700 ˇecho \"b\"
24701 ˇ;;
24702 ˇ*)
24703 ˇecho \"c\"
24704 ˇ;;
24705 ˇesac
24706 ˇ}
24707 "});
24708}
24709
24710#[gpui::test]
24711async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
24712 init_test(cx, |_| {});
24713
24714 let mut cx = EditorTestContext::new(cx).await;
24715 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24716 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24717
24718 // test indents on comment insert
24719 cx.set_state(indoc! {"
24720 function main() {
24721 ˇ for item in $items; do
24722 ˇ while [ -n \"$item\" ]; do
24723 ˇ if [ \"$value\" -gt 10 ]; then
24724 ˇ continue
24725 ˇ elif [ \"$value\" -lt 0 ]; then
24726 ˇ break
24727 ˇ else
24728 ˇ echo \"$item\"
24729 ˇ fi
24730 ˇ done
24731 ˇ done
24732 ˇ}
24733 "});
24734 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
24735 cx.assert_editor_state(indoc! {"
24736 function main() {
24737 #ˇ for item in $items; do
24738 #ˇ while [ -n \"$item\" ]; do
24739 #ˇ if [ \"$value\" -gt 10 ]; then
24740 #ˇ continue
24741 #ˇ elif [ \"$value\" -lt 0 ]; then
24742 #ˇ break
24743 #ˇ else
24744 #ˇ echo \"$item\"
24745 #ˇ fi
24746 #ˇ done
24747 #ˇ done
24748 #ˇ}
24749 "});
24750}
24751
24752#[gpui::test]
24753async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
24754 init_test(cx, |_| {});
24755
24756 let mut cx = EditorTestContext::new(cx).await;
24757 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24758 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24759
24760 // test `else` auto outdents when typed inside `if` block
24761 cx.set_state(indoc! {"
24762 if [ \"$1\" = \"test\" ]; then
24763 echo \"foo bar\"
24764 ˇ
24765 "});
24766 cx.update_editor(|editor, window, cx| {
24767 editor.handle_input("else", window, cx);
24768 });
24769 cx.assert_editor_state(indoc! {"
24770 if [ \"$1\" = \"test\" ]; then
24771 echo \"foo bar\"
24772 elseˇ
24773 "});
24774
24775 // test `elif` auto outdents when typed inside `if` block
24776 cx.set_state(indoc! {"
24777 if [ \"$1\" = \"test\" ]; then
24778 echo \"foo bar\"
24779 ˇ
24780 "});
24781 cx.update_editor(|editor, window, cx| {
24782 editor.handle_input("elif", window, cx);
24783 });
24784 cx.assert_editor_state(indoc! {"
24785 if [ \"$1\" = \"test\" ]; then
24786 echo \"foo bar\"
24787 elifˇ
24788 "});
24789
24790 // test `fi` auto outdents when typed inside `else` block
24791 cx.set_state(indoc! {"
24792 if [ \"$1\" = \"test\" ]; then
24793 echo \"foo bar\"
24794 else
24795 echo \"bar baz\"
24796 ˇ
24797 "});
24798 cx.update_editor(|editor, window, cx| {
24799 editor.handle_input("fi", window, cx);
24800 });
24801 cx.assert_editor_state(indoc! {"
24802 if [ \"$1\" = \"test\" ]; then
24803 echo \"foo bar\"
24804 else
24805 echo \"bar baz\"
24806 fiˇ
24807 "});
24808
24809 // test `done` auto outdents when typed inside `while` block
24810 cx.set_state(indoc! {"
24811 while read line; do
24812 echo \"$line\"
24813 ˇ
24814 "});
24815 cx.update_editor(|editor, window, cx| {
24816 editor.handle_input("done", window, cx);
24817 });
24818 cx.assert_editor_state(indoc! {"
24819 while read line; do
24820 echo \"$line\"
24821 doneˇ
24822 "});
24823
24824 // test `done` auto outdents when typed inside `for` block
24825 cx.set_state(indoc! {"
24826 for file in *.txt; do
24827 cat \"$file\"
24828 ˇ
24829 "});
24830 cx.update_editor(|editor, window, cx| {
24831 editor.handle_input("done", window, cx);
24832 });
24833 cx.assert_editor_state(indoc! {"
24834 for file in *.txt; do
24835 cat \"$file\"
24836 doneˇ
24837 "});
24838
24839 // test `esac` auto outdents when typed inside `case` block
24840 cx.set_state(indoc! {"
24841 case \"$1\" in
24842 start)
24843 echo \"foo bar\"
24844 ;;
24845 stop)
24846 echo \"bar baz\"
24847 ;;
24848 ˇ
24849 "});
24850 cx.update_editor(|editor, window, cx| {
24851 editor.handle_input("esac", window, cx);
24852 });
24853 cx.assert_editor_state(indoc! {"
24854 case \"$1\" in
24855 start)
24856 echo \"foo bar\"
24857 ;;
24858 stop)
24859 echo \"bar baz\"
24860 ;;
24861 esacˇ
24862 "});
24863
24864 // test `*)` auto outdents when typed inside `case` block
24865 cx.set_state(indoc! {"
24866 case \"$1\" in
24867 start)
24868 echo \"foo bar\"
24869 ;;
24870 ˇ
24871 "});
24872 cx.update_editor(|editor, window, cx| {
24873 editor.handle_input("*)", window, cx);
24874 });
24875 cx.assert_editor_state(indoc! {"
24876 case \"$1\" in
24877 start)
24878 echo \"foo bar\"
24879 ;;
24880 *)ˇ
24881 "});
24882
24883 // test `fi` outdents to correct level with nested if blocks
24884 cx.set_state(indoc! {"
24885 if [ \"$1\" = \"test\" ]; then
24886 echo \"outer if\"
24887 if [ \"$2\" = \"debug\" ]; then
24888 echo \"inner if\"
24889 ˇ
24890 "});
24891 cx.update_editor(|editor, window, cx| {
24892 editor.handle_input("fi", window, cx);
24893 });
24894 cx.assert_editor_state(indoc! {"
24895 if [ \"$1\" = \"test\" ]; then
24896 echo \"outer if\"
24897 if [ \"$2\" = \"debug\" ]; then
24898 echo \"inner if\"
24899 fiˇ
24900 "});
24901}
24902
24903#[gpui::test]
24904async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
24905 init_test(cx, |_| {});
24906 update_test_language_settings(cx, |settings| {
24907 settings.defaults.extend_comment_on_newline = Some(false);
24908 });
24909 let mut cx = EditorTestContext::new(cx).await;
24910 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24911 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24912
24913 // test correct indent after newline on comment
24914 cx.set_state(indoc! {"
24915 # COMMENT:ˇ
24916 "});
24917 cx.update_editor(|editor, window, cx| {
24918 editor.newline(&Newline, window, cx);
24919 });
24920 cx.assert_editor_state(indoc! {"
24921 # COMMENT:
24922 ˇ
24923 "});
24924
24925 // test correct indent after newline after `then`
24926 cx.set_state(indoc! {"
24927
24928 if [ \"$1\" = \"test\" ]; thenˇ
24929 "});
24930 cx.update_editor(|editor, window, cx| {
24931 editor.newline(&Newline, window, cx);
24932 });
24933 cx.run_until_parked();
24934 cx.assert_editor_state(indoc! {"
24935
24936 if [ \"$1\" = \"test\" ]; then
24937 ˇ
24938 "});
24939
24940 // test correct indent after newline after `else`
24941 cx.set_state(indoc! {"
24942 if [ \"$1\" = \"test\" ]; then
24943 elseˇ
24944 "});
24945 cx.update_editor(|editor, window, cx| {
24946 editor.newline(&Newline, window, cx);
24947 });
24948 cx.run_until_parked();
24949 cx.assert_editor_state(indoc! {"
24950 if [ \"$1\" = \"test\" ]; then
24951 else
24952 ˇ
24953 "});
24954
24955 // test correct indent after newline after `elif`
24956 cx.set_state(indoc! {"
24957 if [ \"$1\" = \"test\" ]; then
24958 elifˇ
24959 "});
24960 cx.update_editor(|editor, window, cx| {
24961 editor.newline(&Newline, window, cx);
24962 });
24963 cx.run_until_parked();
24964 cx.assert_editor_state(indoc! {"
24965 if [ \"$1\" = \"test\" ]; then
24966 elif
24967 ˇ
24968 "});
24969
24970 // test correct indent after newline after `do`
24971 cx.set_state(indoc! {"
24972 for file in *.txt; doˇ
24973 "});
24974 cx.update_editor(|editor, window, cx| {
24975 editor.newline(&Newline, window, cx);
24976 });
24977 cx.run_until_parked();
24978 cx.assert_editor_state(indoc! {"
24979 for file in *.txt; do
24980 ˇ
24981 "});
24982
24983 // test correct indent after newline after case pattern
24984 cx.set_state(indoc! {"
24985 case \"$1\" in
24986 start)ˇ
24987 "});
24988 cx.update_editor(|editor, window, cx| {
24989 editor.newline(&Newline, window, cx);
24990 });
24991 cx.run_until_parked();
24992 cx.assert_editor_state(indoc! {"
24993 case \"$1\" in
24994 start)
24995 ˇ
24996 "});
24997
24998 // test correct indent after newline after case pattern
24999 cx.set_state(indoc! {"
25000 case \"$1\" in
25001 start)
25002 ;;
25003 *)ˇ
25004 "});
25005 cx.update_editor(|editor, window, cx| {
25006 editor.newline(&Newline, window, cx);
25007 });
25008 cx.run_until_parked();
25009 cx.assert_editor_state(indoc! {"
25010 case \"$1\" in
25011 start)
25012 ;;
25013 *)
25014 ˇ
25015 "});
25016
25017 // test correct indent after newline after function opening brace
25018 cx.set_state(indoc! {"
25019 function test() {ˇ}
25020 "});
25021 cx.update_editor(|editor, window, cx| {
25022 editor.newline(&Newline, window, cx);
25023 });
25024 cx.run_until_parked();
25025 cx.assert_editor_state(indoc! {"
25026 function test() {
25027 ˇ
25028 }
25029 "});
25030
25031 // test no extra indent after semicolon on same line
25032 cx.set_state(indoc! {"
25033 echo \"test\";ˇ
25034 "});
25035 cx.update_editor(|editor, window, cx| {
25036 editor.newline(&Newline, window, cx);
25037 });
25038 cx.run_until_parked();
25039 cx.assert_editor_state(indoc! {"
25040 echo \"test\";
25041 ˇ
25042 "});
25043}
25044
25045fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
25046 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
25047 point..point
25048}
25049
25050#[track_caller]
25051fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
25052 let (text, ranges) = marked_text_ranges(marked_text, true);
25053 assert_eq!(editor.text(cx), text);
25054 assert_eq!(
25055 editor.selections.ranges(cx),
25056 ranges,
25057 "Assert selections are {}",
25058 marked_text
25059 );
25060}
25061
25062pub fn handle_signature_help_request(
25063 cx: &mut EditorLspTestContext,
25064 mocked_response: lsp::SignatureHelp,
25065) -> impl Future<Output = ()> + use<> {
25066 let mut request =
25067 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
25068 let mocked_response = mocked_response.clone();
25069 async move { Ok(Some(mocked_response)) }
25070 });
25071
25072 async move {
25073 request.next().await;
25074 }
25075}
25076
25077#[track_caller]
25078pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
25079 cx.update_editor(|editor, _, _| {
25080 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
25081 let entries = menu.entries.borrow();
25082 let entries = entries
25083 .iter()
25084 .map(|entry| entry.string.as_str())
25085 .collect::<Vec<_>>();
25086 assert_eq!(entries, expected);
25087 } else {
25088 panic!("Expected completions menu");
25089 }
25090 });
25091}
25092
25093/// Handle completion request passing a marked string specifying where the completion
25094/// should be triggered from using '|' character, what range should be replaced, and what completions
25095/// should be returned using '<' and '>' to delimit the range.
25096///
25097/// Also see `handle_completion_request_with_insert_and_replace`.
25098#[track_caller]
25099pub fn handle_completion_request(
25100 marked_string: &str,
25101 completions: Vec<&'static str>,
25102 is_incomplete: bool,
25103 counter: Arc<AtomicUsize>,
25104 cx: &mut EditorLspTestContext,
25105) -> impl Future<Output = ()> {
25106 let complete_from_marker: TextRangeMarker = '|'.into();
25107 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25108 let (_, mut marked_ranges) = marked_text_ranges_by(
25109 marked_string,
25110 vec![complete_from_marker.clone(), replace_range_marker.clone()],
25111 );
25112
25113 let complete_from_position =
25114 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25115 let replace_range =
25116 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25117
25118 let mut request =
25119 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25120 let completions = completions.clone();
25121 counter.fetch_add(1, atomic::Ordering::Release);
25122 async move {
25123 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25124 assert_eq!(
25125 params.text_document_position.position,
25126 complete_from_position
25127 );
25128 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
25129 is_incomplete,
25130 item_defaults: None,
25131 items: completions
25132 .iter()
25133 .map(|completion_text| lsp::CompletionItem {
25134 label: completion_text.to_string(),
25135 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
25136 range: replace_range,
25137 new_text: completion_text.to_string(),
25138 })),
25139 ..Default::default()
25140 })
25141 .collect(),
25142 })))
25143 }
25144 });
25145
25146 async move {
25147 request.next().await;
25148 }
25149}
25150
25151/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
25152/// given instead, which also contains an `insert` range.
25153///
25154/// This function uses markers to define ranges:
25155/// - `|` marks the cursor position
25156/// - `<>` marks the replace range
25157/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
25158pub fn handle_completion_request_with_insert_and_replace(
25159 cx: &mut EditorLspTestContext,
25160 marked_string: &str,
25161 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
25162 counter: Arc<AtomicUsize>,
25163) -> impl Future<Output = ()> {
25164 let complete_from_marker: TextRangeMarker = '|'.into();
25165 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25166 let insert_range_marker: TextRangeMarker = ('{', '}').into();
25167
25168 let (_, mut marked_ranges) = marked_text_ranges_by(
25169 marked_string,
25170 vec![
25171 complete_from_marker.clone(),
25172 replace_range_marker.clone(),
25173 insert_range_marker.clone(),
25174 ],
25175 );
25176
25177 let complete_from_position =
25178 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25179 let replace_range =
25180 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25181
25182 let insert_range = match marked_ranges.remove(&insert_range_marker) {
25183 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
25184 _ => lsp::Range {
25185 start: replace_range.start,
25186 end: complete_from_position,
25187 },
25188 };
25189
25190 let mut request =
25191 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25192 let completions = completions.clone();
25193 counter.fetch_add(1, atomic::Ordering::Release);
25194 async move {
25195 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25196 assert_eq!(
25197 params.text_document_position.position, complete_from_position,
25198 "marker `|` position doesn't match",
25199 );
25200 Ok(Some(lsp::CompletionResponse::Array(
25201 completions
25202 .iter()
25203 .map(|(label, new_text)| lsp::CompletionItem {
25204 label: label.to_string(),
25205 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
25206 lsp::InsertReplaceEdit {
25207 insert: insert_range,
25208 replace: replace_range,
25209 new_text: new_text.to_string(),
25210 },
25211 )),
25212 ..Default::default()
25213 })
25214 .collect(),
25215 )))
25216 }
25217 });
25218
25219 async move {
25220 request.next().await;
25221 }
25222}
25223
25224fn handle_resolve_completion_request(
25225 cx: &mut EditorLspTestContext,
25226 edits: Option<Vec<(&'static str, &'static str)>>,
25227) -> impl Future<Output = ()> {
25228 let edits = edits.map(|edits| {
25229 edits
25230 .iter()
25231 .map(|(marked_string, new_text)| {
25232 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
25233 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
25234 lsp::TextEdit::new(replace_range, new_text.to_string())
25235 })
25236 .collect::<Vec<_>>()
25237 });
25238
25239 let mut request =
25240 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
25241 let edits = edits.clone();
25242 async move {
25243 Ok(lsp::CompletionItem {
25244 additional_text_edits: edits,
25245 ..Default::default()
25246 })
25247 }
25248 });
25249
25250 async move {
25251 request.next().await;
25252 }
25253}
25254
25255pub(crate) fn update_test_language_settings(
25256 cx: &mut TestAppContext,
25257 f: impl Fn(&mut AllLanguageSettingsContent),
25258) {
25259 cx.update(|cx| {
25260 SettingsStore::update_global(cx, |store, cx| {
25261 store.update_user_settings(cx, |settings| f(&mut settings.project.all_languages));
25262 });
25263 });
25264}
25265
25266pub(crate) fn update_test_project_settings(
25267 cx: &mut TestAppContext,
25268 f: impl Fn(&mut ProjectSettingsContent),
25269) {
25270 cx.update(|cx| {
25271 SettingsStore::update_global(cx, |store, cx| {
25272 store.update_user_settings(cx, |settings| f(&mut settings.project));
25273 });
25274 });
25275}
25276
25277pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
25278 cx.update(|cx| {
25279 assets::Assets.load_test_fonts(cx);
25280 let store = SettingsStore::test(cx);
25281 cx.set_global(store);
25282 theme::init(theme::LoadThemes::JustBase, cx);
25283 release_channel::init(SemanticVersion::default(), cx);
25284 client::init_settings(cx);
25285 language::init(cx);
25286 Project::init_settings(cx);
25287 workspace::init_settings(cx);
25288 crate::init(cx);
25289 });
25290 zlog::init_test();
25291 update_test_language_settings(cx, f);
25292}
25293
25294#[track_caller]
25295fn assert_hunk_revert(
25296 not_reverted_text_with_selections: &str,
25297 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
25298 expected_reverted_text_with_selections: &str,
25299 base_text: &str,
25300 cx: &mut EditorLspTestContext,
25301) {
25302 cx.set_state(not_reverted_text_with_selections);
25303 cx.set_head_text(base_text);
25304 cx.executor().run_until_parked();
25305
25306 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
25307 let snapshot = editor.snapshot(window, cx);
25308 let reverted_hunk_statuses = snapshot
25309 .buffer_snapshot
25310 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
25311 .map(|hunk| hunk.status().kind)
25312 .collect::<Vec<_>>();
25313
25314 editor.git_restore(&Default::default(), window, cx);
25315 reverted_hunk_statuses
25316 });
25317 cx.executor().run_until_parked();
25318 cx.assert_editor_state(expected_reverted_text_with_selections);
25319 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
25320}
25321
25322#[gpui::test(iterations = 10)]
25323async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
25324 init_test(cx, |_| {});
25325
25326 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
25327 let counter = diagnostic_requests.clone();
25328
25329 let fs = FakeFs::new(cx.executor());
25330 fs.insert_tree(
25331 path!("/a"),
25332 json!({
25333 "first.rs": "fn main() { let a = 5; }",
25334 "second.rs": "// Test file",
25335 }),
25336 )
25337 .await;
25338
25339 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25340 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25341 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25342
25343 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25344 language_registry.add(rust_lang());
25345 let mut fake_servers = language_registry.register_fake_lsp(
25346 "Rust",
25347 FakeLspAdapter {
25348 capabilities: lsp::ServerCapabilities {
25349 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
25350 lsp::DiagnosticOptions {
25351 identifier: None,
25352 inter_file_dependencies: true,
25353 workspace_diagnostics: true,
25354 work_done_progress_options: Default::default(),
25355 },
25356 )),
25357 ..Default::default()
25358 },
25359 ..Default::default()
25360 },
25361 );
25362
25363 let editor = workspace
25364 .update(cx, |workspace, window, cx| {
25365 workspace.open_abs_path(
25366 PathBuf::from(path!("/a/first.rs")),
25367 OpenOptions::default(),
25368 window,
25369 cx,
25370 )
25371 })
25372 .unwrap()
25373 .await
25374 .unwrap()
25375 .downcast::<Editor>()
25376 .unwrap();
25377 let fake_server = fake_servers.next().await.unwrap();
25378 let server_id = fake_server.server.server_id();
25379 let mut first_request = fake_server
25380 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
25381 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
25382 let result_id = Some(new_result_id.to_string());
25383 assert_eq!(
25384 params.text_document.uri,
25385 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25386 );
25387 async move {
25388 Ok(lsp::DocumentDiagnosticReportResult::Report(
25389 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
25390 related_documents: None,
25391 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
25392 items: Vec::new(),
25393 result_id,
25394 },
25395 }),
25396 ))
25397 }
25398 });
25399
25400 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
25401 project.update(cx, |project, cx| {
25402 let buffer_id = editor
25403 .read(cx)
25404 .buffer()
25405 .read(cx)
25406 .as_singleton()
25407 .expect("created a singleton buffer")
25408 .read(cx)
25409 .remote_id();
25410 let buffer_result_id = project
25411 .lsp_store()
25412 .read(cx)
25413 .result_id(server_id, buffer_id, cx);
25414 assert_eq!(expected, buffer_result_id);
25415 });
25416 };
25417
25418 ensure_result_id(None, cx);
25419 cx.executor().advance_clock(Duration::from_millis(60));
25420 cx.executor().run_until_parked();
25421 assert_eq!(
25422 diagnostic_requests.load(atomic::Ordering::Acquire),
25423 1,
25424 "Opening file should trigger diagnostic request"
25425 );
25426 first_request
25427 .next()
25428 .await
25429 .expect("should have sent the first diagnostics pull request");
25430 ensure_result_id(Some("1".to_string()), cx);
25431
25432 // Editing should trigger diagnostics
25433 editor.update_in(cx, |editor, window, cx| {
25434 editor.handle_input("2", window, cx)
25435 });
25436 cx.executor().advance_clock(Duration::from_millis(60));
25437 cx.executor().run_until_parked();
25438 assert_eq!(
25439 diagnostic_requests.load(atomic::Ordering::Acquire),
25440 2,
25441 "Editing should trigger diagnostic request"
25442 );
25443 ensure_result_id(Some("2".to_string()), cx);
25444
25445 // Moving cursor should not trigger diagnostic request
25446 editor.update_in(cx, |editor, window, cx| {
25447 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25448 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
25449 });
25450 });
25451 cx.executor().advance_clock(Duration::from_millis(60));
25452 cx.executor().run_until_parked();
25453 assert_eq!(
25454 diagnostic_requests.load(atomic::Ordering::Acquire),
25455 2,
25456 "Cursor movement should not trigger diagnostic request"
25457 );
25458 ensure_result_id(Some("2".to_string()), cx);
25459 // Multiple rapid edits should be debounced
25460 for _ in 0..5 {
25461 editor.update_in(cx, |editor, window, cx| {
25462 editor.handle_input("x", window, cx)
25463 });
25464 }
25465 cx.executor().advance_clock(Duration::from_millis(60));
25466 cx.executor().run_until_parked();
25467
25468 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
25469 assert!(
25470 final_requests <= 4,
25471 "Multiple rapid edits should be debounced (got {final_requests} requests)",
25472 );
25473 ensure_result_id(Some(final_requests.to_string()), cx);
25474}
25475
25476#[gpui::test]
25477async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
25478 // Regression test for issue #11671
25479 // Previously, adding a cursor after moving multiple cursors would reset
25480 // the cursor count instead of adding to the existing cursors.
25481 init_test(cx, |_| {});
25482 let mut cx = EditorTestContext::new(cx).await;
25483
25484 // Create a simple buffer with cursor at start
25485 cx.set_state(indoc! {"
25486 ˇaaaa
25487 bbbb
25488 cccc
25489 dddd
25490 eeee
25491 ffff
25492 gggg
25493 hhhh"});
25494
25495 // Add 2 cursors below (so we have 3 total)
25496 cx.update_editor(|editor, window, cx| {
25497 editor.add_selection_below(&Default::default(), window, cx);
25498 editor.add_selection_below(&Default::default(), window, cx);
25499 });
25500
25501 // Verify we have 3 cursors
25502 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
25503 assert_eq!(
25504 initial_count, 3,
25505 "Should have 3 cursors after adding 2 below"
25506 );
25507
25508 // Move down one line
25509 cx.update_editor(|editor, window, cx| {
25510 editor.move_down(&MoveDown, window, cx);
25511 });
25512
25513 // Add another cursor below
25514 cx.update_editor(|editor, window, cx| {
25515 editor.add_selection_below(&Default::default(), window, cx);
25516 });
25517
25518 // Should now have 4 cursors (3 original + 1 new)
25519 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
25520 assert_eq!(
25521 final_count, 4,
25522 "Should have 4 cursors after moving and adding another"
25523 );
25524}
25525
25526#[gpui::test(iterations = 10)]
25527async fn test_document_colors(cx: &mut TestAppContext) {
25528 let expected_color = Rgba {
25529 r: 0.33,
25530 g: 0.33,
25531 b: 0.33,
25532 a: 0.33,
25533 };
25534
25535 init_test(cx, |_| {});
25536
25537 let fs = FakeFs::new(cx.executor());
25538 fs.insert_tree(
25539 path!("/a"),
25540 json!({
25541 "first.rs": "fn main() { let a = 5; }",
25542 }),
25543 )
25544 .await;
25545
25546 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25547 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25548 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25549
25550 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25551 language_registry.add(rust_lang());
25552 let mut fake_servers = language_registry.register_fake_lsp(
25553 "Rust",
25554 FakeLspAdapter {
25555 capabilities: lsp::ServerCapabilities {
25556 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
25557 ..lsp::ServerCapabilities::default()
25558 },
25559 name: "rust-analyzer",
25560 ..FakeLspAdapter::default()
25561 },
25562 );
25563 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
25564 "Rust",
25565 FakeLspAdapter {
25566 capabilities: lsp::ServerCapabilities {
25567 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
25568 ..lsp::ServerCapabilities::default()
25569 },
25570 name: "not-rust-analyzer",
25571 ..FakeLspAdapter::default()
25572 },
25573 );
25574
25575 let editor = workspace
25576 .update(cx, |workspace, window, cx| {
25577 workspace.open_abs_path(
25578 PathBuf::from(path!("/a/first.rs")),
25579 OpenOptions::default(),
25580 window,
25581 cx,
25582 )
25583 })
25584 .unwrap()
25585 .await
25586 .unwrap()
25587 .downcast::<Editor>()
25588 .unwrap();
25589 let fake_language_server = fake_servers.next().await.unwrap();
25590 let fake_language_server_without_capabilities =
25591 fake_servers_without_capabilities.next().await.unwrap();
25592 let requests_made = Arc::new(AtomicUsize::new(0));
25593 let closure_requests_made = Arc::clone(&requests_made);
25594 let mut color_request_handle = fake_language_server
25595 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
25596 let requests_made = Arc::clone(&closure_requests_made);
25597 async move {
25598 assert_eq!(
25599 params.text_document.uri,
25600 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25601 );
25602 requests_made.fetch_add(1, atomic::Ordering::Release);
25603 Ok(vec![
25604 lsp::ColorInformation {
25605 range: lsp::Range {
25606 start: lsp::Position {
25607 line: 0,
25608 character: 0,
25609 },
25610 end: lsp::Position {
25611 line: 0,
25612 character: 1,
25613 },
25614 },
25615 color: lsp::Color {
25616 red: 0.33,
25617 green: 0.33,
25618 blue: 0.33,
25619 alpha: 0.33,
25620 },
25621 },
25622 lsp::ColorInformation {
25623 range: lsp::Range {
25624 start: lsp::Position {
25625 line: 0,
25626 character: 0,
25627 },
25628 end: lsp::Position {
25629 line: 0,
25630 character: 1,
25631 },
25632 },
25633 color: lsp::Color {
25634 red: 0.33,
25635 green: 0.33,
25636 blue: 0.33,
25637 alpha: 0.33,
25638 },
25639 },
25640 ])
25641 }
25642 });
25643
25644 let _handle = fake_language_server_without_capabilities
25645 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
25646 panic!("Should not be called");
25647 });
25648 cx.executor().advance_clock(Duration::from_millis(100));
25649 color_request_handle.next().await.unwrap();
25650 cx.run_until_parked();
25651 assert_eq!(
25652 1,
25653 requests_made.load(atomic::Ordering::Acquire),
25654 "Should query for colors once per editor open"
25655 );
25656 editor.update_in(cx, |editor, _, cx| {
25657 assert_eq!(
25658 vec![expected_color],
25659 extract_color_inlays(editor, cx),
25660 "Should have an initial inlay"
25661 );
25662 });
25663
25664 // opening another file in a split should not influence the LSP query counter
25665 workspace
25666 .update(cx, |workspace, window, cx| {
25667 assert_eq!(
25668 workspace.panes().len(),
25669 1,
25670 "Should have one pane with one editor"
25671 );
25672 workspace.move_item_to_pane_in_direction(
25673 &MoveItemToPaneInDirection {
25674 direction: SplitDirection::Right,
25675 focus: false,
25676 clone: true,
25677 },
25678 window,
25679 cx,
25680 );
25681 })
25682 .unwrap();
25683 cx.run_until_parked();
25684 workspace
25685 .update(cx, |workspace, _, cx| {
25686 let panes = workspace.panes();
25687 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
25688 for pane in panes {
25689 let editor = pane
25690 .read(cx)
25691 .active_item()
25692 .and_then(|item| item.downcast::<Editor>())
25693 .expect("Should have opened an editor in each split");
25694 let editor_file = editor
25695 .read(cx)
25696 .buffer()
25697 .read(cx)
25698 .as_singleton()
25699 .expect("test deals with singleton buffers")
25700 .read(cx)
25701 .file()
25702 .expect("test buffese should have a file")
25703 .path();
25704 assert_eq!(
25705 editor_file.as_ref(),
25706 rel_path("first.rs"),
25707 "Both editors should be opened for the same file"
25708 )
25709 }
25710 })
25711 .unwrap();
25712
25713 cx.executor().advance_clock(Duration::from_millis(500));
25714 let save = editor.update_in(cx, |editor, window, cx| {
25715 editor.move_to_end(&MoveToEnd, window, cx);
25716 editor.handle_input("dirty", window, cx);
25717 editor.save(
25718 SaveOptions {
25719 format: true,
25720 autosave: true,
25721 },
25722 project.clone(),
25723 window,
25724 cx,
25725 )
25726 });
25727 save.await.unwrap();
25728
25729 color_request_handle.next().await.unwrap();
25730 cx.run_until_parked();
25731 assert_eq!(
25732 3,
25733 requests_made.load(atomic::Ordering::Acquire),
25734 "Should query for colors once per save and once per formatting after save"
25735 );
25736
25737 drop(editor);
25738 let close = workspace
25739 .update(cx, |workspace, window, cx| {
25740 workspace.active_pane().update(cx, |pane, cx| {
25741 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25742 })
25743 })
25744 .unwrap();
25745 close.await.unwrap();
25746 let close = workspace
25747 .update(cx, |workspace, window, cx| {
25748 workspace.active_pane().update(cx, |pane, cx| {
25749 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25750 })
25751 })
25752 .unwrap();
25753 close.await.unwrap();
25754 assert_eq!(
25755 3,
25756 requests_made.load(atomic::Ordering::Acquire),
25757 "After saving and closing all editors, no extra requests should be made"
25758 );
25759 workspace
25760 .update(cx, |workspace, _, cx| {
25761 assert!(
25762 workspace.active_item(cx).is_none(),
25763 "Should close all editors"
25764 )
25765 })
25766 .unwrap();
25767
25768 workspace
25769 .update(cx, |workspace, window, cx| {
25770 workspace.active_pane().update(cx, |pane, cx| {
25771 pane.navigate_backward(&workspace::GoBack, window, cx);
25772 })
25773 })
25774 .unwrap();
25775 cx.executor().advance_clock(Duration::from_millis(100));
25776 cx.run_until_parked();
25777 let editor = workspace
25778 .update(cx, |workspace, _, cx| {
25779 workspace
25780 .active_item(cx)
25781 .expect("Should have reopened the editor again after navigating back")
25782 .downcast::<Editor>()
25783 .expect("Should be an editor")
25784 })
25785 .unwrap();
25786 color_request_handle.next().await.unwrap();
25787 assert_eq!(
25788 3,
25789 requests_made.load(atomic::Ordering::Acquire),
25790 "Cache should be reused on buffer close and reopen"
25791 );
25792 editor.update(cx, |editor, cx| {
25793 assert_eq!(
25794 vec![expected_color],
25795 extract_color_inlays(editor, cx),
25796 "Should have an initial inlay"
25797 );
25798 });
25799
25800 drop(color_request_handle);
25801 let closure_requests_made = Arc::clone(&requests_made);
25802 let mut empty_color_request_handle = fake_language_server
25803 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
25804 let requests_made = Arc::clone(&closure_requests_made);
25805 async move {
25806 assert_eq!(
25807 params.text_document.uri,
25808 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25809 );
25810 requests_made.fetch_add(1, atomic::Ordering::Release);
25811 Ok(Vec::new())
25812 }
25813 });
25814 let save = editor.update_in(cx, |editor, window, cx| {
25815 editor.move_to_end(&MoveToEnd, window, cx);
25816 editor.handle_input("dirty_again", window, cx);
25817 editor.save(
25818 SaveOptions {
25819 format: false,
25820 autosave: true,
25821 },
25822 project.clone(),
25823 window,
25824 cx,
25825 )
25826 });
25827 save.await.unwrap();
25828
25829 empty_color_request_handle.next().await.unwrap();
25830 cx.run_until_parked();
25831 assert_eq!(
25832 4,
25833 requests_made.load(atomic::Ordering::Acquire),
25834 "Should query for colors once per save only, as formatting was not requested"
25835 );
25836 editor.update(cx, |editor, cx| {
25837 assert_eq!(
25838 Vec::<Rgba>::new(),
25839 extract_color_inlays(editor, cx),
25840 "Should clear all colors when the server returns an empty response"
25841 );
25842 });
25843}
25844
25845#[gpui::test]
25846async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
25847 init_test(cx, |_| {});
25848 let (editor, cx) = cx.add_window_view(Editor::single_line);
25849 editor.update_in(cx, |editor, window, cx| {
25850 editor.set_text("oops\n\nwow\n", window, cx)
25851 });
25852 cx.run_until_parked();
25853 editor.update(cx, |editor, cx| {
25854 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
25855 });
25856 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
25857 cx.run_until_parked();
25858 editor.update(cx, |editor, cx| {
25859 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
25860 });
25861}
25862
25863#[gpui::test]
25864async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
25865 init_test(cx, |_| {});
25866
25867 cx.update(|cx| {
25868 register_project_item::<Editor>(cx);
25869 });
25870
25871 let fs = FakeFs::new(cx.executor());
25872 fs.insert_tree("/root1", json!({})).await;
25873 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
25874 .await;
25875
25876 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
25877 let (workspace, cx) =
25878 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
25879
25880 let worktree_id = project.update(cx, |project, cx| {
25881 project.worktrees(cx).next().unwrap().read(cx).id()
25882 });
25883
25884 let handle = workspace
25885 .update_in(cx, |workspace, window, cx| {
25886 let project_path = (worktree_id, rel_path("one.pdf"));
25887 workspace.open_path(project_path, None, true, window, cx)
25888 })
25889 .await
25890 .unwrap();
25891
25892 assert_eq!(
25893 handle.to_any().entity_type(),
25894 TypeId::of::<InvalidBufferView>()
25895 );
25896}
25897
25898#[gpui::test]
25899async fn test_select_next_prev_syntax_node(cx: &mut TestAppContext) {
25900 init_test(cx, |_| {});
25901
25902 let language = Arc::new(Language::new(
25903 LanguageConfig::default(),
25904 Some(tree_sitter_rust::LANGUAGE.into()),
25905 ));
25906
25907 // Test hierarchical sibling navigation
25908 let text = r#"
25909 fn outer() {
25910 if condition {
25911 let a = 1;
25912 }
25913 let b = 2;
25914 }
25915
25916 fn another() {
25917 let c = 3;
25918 }
25919 "#;
25920
25921 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
25922 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
25923 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
25924
25925 // Wait for parsing to complete
25926 editor
25927 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
25928 .await;
25929
25930 editor.update_in(cx, |editor, window, cx| {
25931 // Start by selecting "let a = 1;" inside the if block
25932 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25933 s.select_display_ranges([
25934 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 26)
25935 ]);
25936 });
25937
25938 let initial_selection = editor.selections.display_ranges(cx);
25939 assert_eq!(initial_selection.len(), 1, "Should have one selection");
25940
25941 // Test select next sibling - should move up levels to find the next sibling
25942 // Since "let a = 1;" has no siblings in the if block, it should move up
25943 // to find "let b = 2;" which is a sibling of the if block
25944 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
25945 let next_selection = editor.selections.display_ranges(cx);
25946
25947 // Should have a selection and it should be different from the initial
25948 assert_eq!(
25949 next_selection.len(),
25950 1,
25951 "Should have one selection after next"
25952 );
25953 assert_ne!(
25954 next_selection[0], initial_selection[0],
25955 "Next sibling selection should be different"
25956 );
25957
25958 // Test hierarchical navigation by going to the end of the current function
25959 // and trying to navigate to the next function
25960 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25961 s.select_display_ranges([
25962 DisplayPoint::new(DisplayRow(5), 12)..DisplayPoint::new(DisplayRow(5), 22)
25963 ]);
25964 });
25965
25966 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
25967 let function_next_selection = editor.selections.display_ranges(cx);
25968
25969 // Should move to the next function
25970 assert_eq!(
25971 function_next_selection.len(),
25972 1,
25973 "Should have one selection after function next"
25974 );
25975
25976 // Test select previous sibling navigation
25977 editor.select_prev_syntax_node(&SelectPreviousSyntaxNode, window, cx);
25978 let prev_selection = editor.selections.display_ranges(cx);
25979
25980 // Should have a selection and it should be different
25981 assert_eq!(
25982 prev_selection.len(),
25983 1,
25984 "Should have one selection after prev"
25985 );
25986 assert_ne!(
25987 prev_selection[0], function_next_selection[0],
25988 "Previous sibling selection should be different from next"
25989 );
25990 });
25991}
25992
25993#[gpui::test]
25994async fn test_next_prev_document_highlight(cx: &mut TestAppContext) {
25995 init_test(cx, |_| {});
25996
25997 let mut cx = EditorTestContext::new(cx).await;
25998 cx.set_state(
25999 "let ˇvariable = 42;
26000let another = variable + 1;
26001let result = variable * 2;",
26002 );
26003
26004 // Set up document highlights manually (simulating LSP response)
26005 cx.update_editor(|editor, _window, cx| {
26006 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
26007
26008 // Create highlights for "variable" occurrences
26009 let highlight_ranges = [
26010 Point::new(0, 4)..Point::new(0, 12), // First "variable"
26011 Point::new(1, 14)..Point::new(1, 22), // Second "variable"
26012 Point::new(2, 13)..Point::new(2, 21), // Third "variable"
26013 ];
26014
26015 let anchor_ranges: Vec<_> = highlight_ranges
26016 .iter()
26017 .map(|range| range.clone().to_anchors(&buffer_snapshot))
26018 .collect();
26019
26020 editor.highlight_background::<DocumentHighlightRead>(
26021 &anchor_ranges,
26022 |theme| theme.colors().editor_document_highlight_read_background,
26023 cx,
26024 );
26025 });
26026
26027 // Go to next highlight - should move to second "variable"
26028 cx.update_editor(|editor, window, cx| {
26029 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26030 });
26031 cx.assert_editor_state(
26032 "let variable = 42;
26033let another = ˇvariable + 1;
26034let result = variable * 2;",
26035 );
26036
26037 // Go to next highlight - should move to third "variable"
26038 cx.update_editor(|editor, window, cx| {
26039 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26040 });
26041 cx.assert_editor_state(
26042 "let variable = 42;
26043let another = variable + 1;
26044let result = ˇvariable * 2;",
26045 );
26046
26047 // Go to next highlight - should stay at third "variable" (no wrap-around)
26048 cx.update_editor(|editor, window, cx| {
26049 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26050 });
26051 cx.assert_editor_state(
26052 "let variable = 42;
26053let another = variable + 1;
26054let result = ˇvariable * 2;",
26055 );
26056
26057 // Now test going backwards from third position
26058 cx.update_editor(|editor, window, cx| {
26059 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26060 });
26061 cx.assert_editor_state(
26062 "let variable = 42;
26063let another = ˇvariable + 1;
26064let result = variable * 2;",
26065 );
26066
26067 // Go to previous highlight - should move to first "variable"
26068 cx.update_editor(|editor, window, cx| {
26069 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26070 });
26071 cx.assert_editor_state(
26072 "let ˇvariable = 42;
26073let another = variable + 1;
26074let result = variable * 2;",
26075 );
26076
26077 // Go to previous highlight - should stay on first "variable"
26078 cx.update_editor(|editor, window, cx| {
26079 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26080 });
26081 cx.assert_editor_state(
26082 "let ˇvariable = 42;
26083let another = variable + 1;
26084let result = variable * 2;",
26085 );
26086}
26087
26088#[gpui::test]
26089async fn test_paste_url_from_other_app_creates_markdown_link_over_selected_text(
26090 cx: &mut gpui::TestAppContext,
26091) {
26092 init_test(cx, |_| {});
26093
26094 let url = "https://zed.dev";
26095
26096 let markdown_language = Arc::new(Language::new(
26097 LanguageConfig {
26098 name: "Markdown".into(),
26099 ..LanguageConfig::default()
26100 },
26101 None,
26102 ));
26103
26104 let mut cx = EditorTestContext::new(cx).await;
26105 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26106 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)");
26107
26108 cx.update_editor(|editor, window, cx| {
26109 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26110 editor.paste(&Paste, window, cx);
26111 });
26112
26113 cx.assert_editor_state(&format!(
26114 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)"
26115 ));
26116}
26117
26118#[gpui::test]
26119async fn test_paste_url_from_zed_copy_creates_markdown_link_over_selected_text(
26120 cx: &mut gpui::TestAppContext,
26121) {
26122 init_test(cx, |_| {});
26123
26124 let url = "https://zed.dev";
26125
26126 let markdown_language = Arc::new(Language::new(
26127 LanguageConfig {
26128 name: "Markdown".into(),
26129 ..LanguageConfig::default()
26130 },
26131 None,
26132 ));
26133
26134 let mut cx = EditorTestContext::new(cx).await;
26135 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26136 cx.set_state(&format!(
26137 "Hello, editor.\nZed is great (see this link: )\n«{url}ˇ»"
26138 ));
26139
26140 cx.update_editor(|editor, window, cx| {
26141 editor.copy(&Copy, window, cx);
26142 });
26143
26144 cx.set_state(&format!(
26145 "Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)\n{url}"
26146 ));
26147
26148 cx.update_editor(|editor, window, cx| {
26149 editor.paste(&Paste, window, cx);
26150 });
26151
26152 cx.assert_editor_state(&format!(
26153 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)\n{url}"
26154 ));
26155}
26156
26157#[gpui::test]
26158async fn test_paste_url_from_other_app_replaces_existing_url_without_creating_markdown_link(
26159 cx: &mut gpui::TestAppContext,
26160) {
26161 init_test(cx, |_| {});
26162
26163 let url = "https://zed.dev";
26164
26165 let markdown_language = Arc::new(Language::new(
26166 LanguageConfig {
26167 name: "Markdown".into(),
26168 ..LanguageConfig::default()
26169 },
26170 None,
26171 ));
26172
26173 let mut cx = EditorTestContext::new(cx).await;
26174 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26175 cx.set_state("Please visit zed's homepage: «https://www.apple.comˇ»");
26176
26177 cx.update_editor(|editor, window, cx| {
26178 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26179 editor.paste(&Paste, window, cx);
26180 });
26181
26182 cx.assert_editor_state(&format!("Please visit zed's homepage: {url}ˇ"));
26183}
26184
26185#[gpui::test]
26186async fn test_paste_plain_text_from_other_app_replaces_selection_without_creating_markdown_link(
26187 cx: &mut gpui::TestAppContext,
26188) {
26189 init_test(cx, |_| {});
26190
26191 let text = "Awesome";
26192
26193 let markdown_language = Arc::new(Language::new(
26194 LanguageConfig {
26195 name: "Markdown".into(),
26196 ..LanguageConfig::default()
26197 },
26198 None,
26199 ));
26200
26201 let mut cx = EditorTestContext::new(cx).await;
26202 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26203 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat»");
26204
26205 cx.update_editor(|editor, window, cx| {
26206 cx.write_to_clipboard(ClipboardItem::new_string(text.to_string()));
26207 editor.paste(&Paste, window, cx);
26208 });
26209
26210 cx.assert_editor_state(&format!("Hello, {text}ˇ.\nZed is {text}ˇ"));
26211}
26212
26213#[gpui::test]
26214async fn test_paste_url_from_other_app_without_creating_markdown_link_in_non_markdown_language(
26215 cx: &mut gpui::TestAppContext,
26216) {
26217 init_test(cx, |_| {});
26218
26219 let url = "https://zed.dev";
26220
26221 let markdown_language = Arc::new(Language::new(
26222 LanguageConfig {
26223 name: "Rust".into(),
26224 ..LanguageConfig::default()
26225 },
26226 None,
26227 ));
26228
26229 let mut cx = EditorTestContext::new(cx).await;
26230 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26231 cx.set_state("// Hello, «editorˇ».\n// Zed is «ˇgreat» (see this link: ˇ)");
26232
26233 cx.update_editor(|editor, window, cx| {
26234 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26235 editor.paste(&Paste, window, cx);
26236 });
26237
26238 cx.assert_editor_state(&format!(
26239 "// Hello, {url}ˇ.\n// Zed is {url}ˇ (see this link: {url}ˇ)"
26240 ));
26241}
26242
26243#[gpui::test]
26244async fn test_paste_url_from_other_app_creates_markdown_link_selectively_in_multi_buffer(
26245 cx: &mut TestAppContext,
26246) {
26247 init_test(cx, |_| {});
26248
26249 let url = "https://zed.dev";
26250
26251 let markdown_language = Arc::new(Language::new(
26252 LanguageConfig {
26253 name: "Markdown".into(),
26254 ..LanguageConfig::default()
26255 },
26256 None,
26257 ));
26258
26259 let (editor, cx) = cx.add_window_view(|window, cx| {
26260 let multi_buffer = MultiBuffer::build_multi(
26261 [
26262 ("this will embed -> link", vec![Point::row_range(0..1)]),
26263 ("this will replace -> link", vec![Point::row_range(0..1)]),
26264 ],
26265 cx,
26266 );
26267 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
26268 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26269 s.select_ranges(vec![
26270 Point::new(0, 19)..Point::new(0, 23),
26271 Point::new(1, 21)..Point::new(1, 25),
26272 ])
26273 });
26274 let first_buffer_id = multi_buffer
26275 .read(cx)
26276 .excerpt_buffer_ids()
26277 .into_iter()
26278 .next()
26279 .unwrap();
26280 let first_buffer = multi_buffer.read(cx).buffer(first_buffer_id).unwrap();
26281 first_buffer.update(cx, |buffer, cx| {
26282 buffer.set_language(Some(markdown_language.clone()), cx);
26283 });
26284
26285 editor
26286 });
26287 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
26288
26289 cx.update_editor(|editor, window, cx| {
26290 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26291 editor.paste(&Paste, window, cx);
26292 });
26293
26294 cx.assert_editor_state(&format!(
26295 "this will embed -> [link]({url})ˇ\nthis will replace -> {url}ˇ"
26296 ));
26297}
26298
26299#[gpui::test]
26300async fn test_race_in_multibuffer_save(cx: &mut TestAppContext) {
26301 init_test(cx, |_| {});
26302
26303 let fs = FakeFs::new(cx.executor());
26304 fs.insert_tree(
26305 path!("/project"),
26306 json!({
26307 "first.rs": "# First Document\nSome content here.",
26308 "second.rs": "Plain text content for second file.",
26309 }),
26310 )
26311 .await;
26312
26313 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
26314 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
26315 let cx = &mut VisualTestContext::from_window(*workspace, cx);
26316
26317 let language = rust_lang();
26318 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
26319 language_registry.add(language.clone());
26320 let mut fake_servers = language_registry.register_fake_lsp(
26321 "Rust",
26322 FakeLspAdapter {
26323 ..FakeLspAdapter::default()
26324 },
26325 );
26326
26327 let buffer1 = project
26328 .update(cx, |project, cx| {
26329 project.open_local_buffer(PathBuf::from(path!("/project/first.rs")), cx)
26330 })
26331 .await
26332 .unwrap();
26333 let buffer2 = project
26334 .update(cx, |project, cx| {
26335 project.open_local_buffer(PathBuf::from(path!("/project/second.rs")), cx)
26336 })
26337 .await
26338 .unwrap();
26339
26340 let multi_buffer = cx.new(|cx| {
26341 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
26342 multi_buffer.set_excerpts_for_path(
26343 PathKey::for_buffer(&buffer1, cx),
26344 buffer1.clone(),
26345 [Point::zero()..buffer1.read(cx).max_point()],
26346 3,
26347 cx,
26348 );
26349 multi_buffer.set_excerpts_for_path(
26350 PathKey::for_buffer(&buffer2, cx),
26351 buffer2.clone(),
26352 [Point::zero()..buffer1.read(cx).max_point()],
26353 3,
26354 cx,
26355 );
26356 multi_buffer
26357 });
26358
26359 let (editor, cx) = cx.add_window_view(|window, cx| {
26360 Editor::new(
26361 EditorMode::full(),
26362 multi_buffer,
26363 Some(project.clone()),
26364 window,
26365 cx,
26366 )
26367 });
26368
26369 let fake_language_server = fake_servers.next().await.unwrap();
26370
26371 buffer1.update(cx, |buffer, cx| buffer.edit([(0..0, "hello!")], None, cx));
26372
26373 let save = editor.update_in(cx, |editor, window, cx| {
26374 assert!(editor.is_dirty(cx));
26375
26376 editor.save(
26377 SaveOptions {
26378 format: true,
26379 autosave: true,
26380 },
26381 project,
26382 window,
26383 cx,
26384 )
26385 });
26386 let (start_edit_tx, start_edit_rx) = oneshot::channel();
26387 let (done_edit_tx, done_edit_rx) = oneshot::channel();
26388 let mut done_edit_rx = Some(done_edit_rx);
26389 let mut start_edit_tx = Some(start_edit_tx);
26390
26391 fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| {
26392 start_edit_tx.take().unwrap().send(()).unwrap();
26393 let done_edit_rx = done_edit_rx.take().unwrap();
26394 async move {
26395 done_edit_rx.await.unwrap();
26396 Ok(None)
26397 }
26398 });
26399
26400 start_edit_rx.await.unwrap();
26401 buffer2
26402 .update(cx, |buffer, cx| buffer.edit([(0..0, "world!")], None, cx))
26403 .unwrap();
26404
26405 done_edit_tx.send(()).unwrap();
26406
26407 save.await.unwrap();
26408 cx.update(|_, cx| assert!(editor.is_dirty(cx)));
26409}
26410
26411#[track_caller]
26412fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
26413 editor
26414 .all_inlays(cx)
26415 .into_iter()
26416 .filter_map(|inlay| inlay.get_color())
26417 .map(Rgba::from)
26418 .collect()
26419}