1use super::*;
2use crate::{
3 JoinLines,
4 code_context_menus::CodeContextMenu,
5 edit_prediction_tests::FakeEditPredictionProvider,
6 linked_editing_ranges::LinkedEditingRanges,
7 scroll::scroll_amount::ScrollAmount,
8 test::{
9 assert_text_with_selections, build_editor,
10 editor_lsp_test_context::{EditorLspTestContext, git_commit_lang},
11 editor_test_context::EditorTestContext,
12 select_ranges,
13 },
14};
15use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
16use collections::HashMap;
17use futures::{StreamExt, channel::oneshot};
18use gpui::{
19 BackgroundExecutor, DismissEvent, Rgba, SemanticVersion, TestAppContext, UpdateGlobal,
20 VisualTestContext, WindowBounds, WindowOptions, div,
21};
22use indoc::indoc;
23use language::{
24 BracketPairConfig,
25 Capability::ReadWrite,
26 DiagnosticSourceKind, FakeLspAdapter, IndentGuideSettings, LanguageConfig,
27 LanguageConfigOverride, LanguageMatcher, LanguageName, Override, Point,
28 language_settings::{
29 CompletionSettingsContent, FormatterList, LanguageSettingsContent, LspInsertMode,
30 },
31 tree_sitter_python,
32};
33use language_settings::Formatter;
34use lsp::CompletionParams;
35use multi_buffer::{IndentGuide, PathKey};
36use parking_lot::Mutex;
37use pretty_assertions::{assert_eq, assert_ne};
38use project::{
39 FakeFs,
40 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
41 project_settings::LspSettings,
42};
43use serde_json::{self, json};
44use settings::{
45 AllLanguageSettingsContent, IndentGuideBackgroundColoring, IndentGuideColoring,
46 ProjectSettingsContent,
47};
48use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
49use std::{
50 iter,
51 sync::atomic::{self, AtomicUsize},
52};
53use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
54use text::ToPoint as _;
55use unindent::Unindent;
56use util::{
57 assert_set_eq, path,
58 rel_path::rel_path,
59 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
60 uri,
61};
62use workspace::{
63 CloseActiveItem, CloseAllItems, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry,
64 OpenOptions, ViewId,
65 invalid_buffer_view::InvalidBufferView,
66 item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
67 register_project_item,
68};
69
70#[gpui::test]
71fn test_edit_events(cx: &mut TestAppContext) {
72 init_test(cx, |_| {});
73
74 let buffer = cx.new(|cx| {
75 let mut buffer = language::Buffer::local("123456", cx);
76 buffer.set_group_interval(Duration::from_secs(1));
77 buffer
78 });
79
80 let events = Rc::new(RefCell::new(Vec::new()));
81 let editor1 = cx.add_window({
82 let events = events.clone();
83 |window, cx| {
84 let entity = cx.entity();
85 cx.subscribe_in(
86 &entity,
87 window,
88 move |_, _, event: &EditorEvent, _, _| match event {
89 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
90 EditorEvent::BufferEdited => {
91 events.borrow_mut().push(("editor1", "buffer edited"))
92 }
93 _ => {}
94 },
95 )
96 .detach();
97 Editor::for_buffer(buffer.clone(), None, window, cx)
98 }
99 });
100
101 let editor2 = cx.add_window({
102 let events = events.clone();
103 |window, cx| {
104 cx.subscribe_in(
105 &cx.entity(),
106 window,
107 move |_, _, event: &EditorEvent, _, _| match event {
108 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
109 EditorEvent::BufferEdited => {
110 events.borrow_mut().push(("editor2", "buffer edited"))
111 }
112 _ => {}
113 },
114 )
115 .detach();
116 Editor::for_buffer(buffer.clone(), None, window, cx)
117 }
118 });
119
120 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
121
122 // Mutating editor 1 will emit an `Edited` event only for that editor.
123 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
124 assert_eq!(
125 mem::take(&mut *events.borrow_mut()),
126 [
127 ("editor1", "edited"),
128 ("editor1", "buffer edited"),
129 ("editor2", "buffer edited"),
130 ]
131 );
132
133 // Mutating editor 2 will emit an `Edited` event only for that editor.
134 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
135 assert_eq!(
136 mem::take(&mut *events.borrow_mut()),
137 [
138 ("editor2", "edited"),
139 ("editor1", "buffer edited"),
140 ("editor2", "buffer edited"),
141 ]
142 );
143
144 // Undoing on editor 1 will emit an `Edited` event only for that editor.
145 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
146 assert_eq!(
147 mem::take(&mut *events.borrow_mut()),
148 [
149 ("editor1", "edited"),
150 ("editor1", "buffer edited"),
151 ("editor2", "buffer edited"),
152 ]
153 );
154
155 // Redoing on editor 1 will emit an `Edited` event only for that editor.
156 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
157 assert_eq!(
158 mem::take(&mut *events.borrow_mut()),
159 [
160 ("editor1", "edited"),
161 ("editor1", "buffer edited"),
162 ("editor2", "buffer edited"),
163 ]
164 );
165
166 // Undoing on editor 2 will emit an `Edited` event only for that editor.
167 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
168 assert_eq!(
169 mem::take(&mut *events.borrow_mut()),
170 [
171 ("editor2", "edited"),
172 ("editor1", "buffer edited"),
173 ("editor2", "buffer edited"),
174 ]
175 );
176
177 // Redoing on editor 2 will emit an `Edited` event only for that editor.
178 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
179 assert_eq!(
180 mem::take(&mut *events.borrow_mut()),
181 [
182 ("editor2", "edited"),
183 ("editor1", "buffer edited"),
184 ("editor2", "buffer edited"),
185 ]
186 );
187
188 // No event is emitted when the mutation is a no-op.
189 _ = editor2.update(cx, |editor, window, cx| {
190 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
191 s.select_ranges([0..0])
192 });
193
194 editor.backspace(&Backspace, window, cx);
195 });
196 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
197}
198
199#[gpui::test]
200fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
201 init_test(cx, |_| {});
202
203 let mut now = Instant::now();
204 let group_interval = Duration::from_millis(1);
205 let buffer = cx.new(|cx| {
206 let mut buf = language::Buffer::local("123456", cx);
207 buf.set_group_interval(group_interval);
208 buf
209 });
210 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
211 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
212
213 _ = editor.update(cx, |editor, window, cx| {
214 editor.start_transaction_at(now, window, cx);
215 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
216 s.select_ranges([2..4])
217 });
218
219 editor.insert("cd", window, cx);
220 editor.end_transaction_at(now, cx);
221 assert_eq!(editor.text(cx), "12cd56");
222 assert_eq!(
223 editor.selections.ranges(&editor.display_snapshot(cx)),
224 vec![4..4]
225 );
226
227 editor.start_transaction_at(now, window, cx);
228 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
229 s.select_ranges([4..5])
230 });
231 editor.insert("e", window, cx);
232 editor.end_transaction_at(now, cx);
233 assert_eq!(editor.text(cx), "12cde6");
234 assert_eq!(
235 editor.selections.ranges(&editor.display_snapshot(cx)),
236 vec![5..5]
237 );
238
239 now += group_interval + Duration::from_millis(1);
240 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
241 s.select_ranges([2..2])
242 });
243
244 // Simulate an edit in another editor
245 buffer.update(cx, |buffer, cx| {
246 buffer.start_transaction_at(now, cx);
247 buffer.edit([(0..1, "a")], None, cx);
248 buffer.edit([(1..1, "b")], None, cx);
249 buffer.end_transaction_at(now, cx);
250 });
251
252 assert_eq!(editor.text(cx), "ab2cde6");
253 assert_eq!(
254 editor.selections.ranges(&editor.display_snapshot(cx)),
255 vec![3..3]
256 );
257
258 // Last transaction happened past the group interval in a different editor.
259 // Undo it individually and don't restore selections.
260 editor.undo(&Undo, window, cx);
261 assert_eq!(editor.text(cx), "12cde6");
262 assert_eq!(
263 editor.selections.ranges(&editor.display_snapshot(cx)),
264 vec![2..2]
265 );
266
267 // First two transactions happened within the group interval in this editor.
268 // Undo them together and restore selections.
269 editor.undo(&Undo, window, cx);
270 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
271 assert_eq!(editor.text(cx), "123456");
272 assert_eq!(
273 editor.selections.ranges(&editor.display_snapshot(cx)),
274 vec![0..0]
275 );
276
277 // Redo the first two transactions together.
278 editor.redo(&Redo, window, cx);
279 assert_eq!(editor.text(cx), "12cde6");
280 assert_eq!(
281 editor.selections.ranges(&editor.display_snapshot(cx)),
282 vec![5..5]
283 );
284
285 // Redo the last transaction on its own.
286 editor.redo(&Redo, window, cx);
287 assert_eq!(editor.text(cx), "ab2cde6");
288 assert_eq!(
289 editor.selections.ranges(&editor.display_snapshot(cx)),
290 vec![6..6]
291 );
292
293 // Test empty transactions.
294 editor.start_transaction_at(now, window, cx);
295 editor.end_transaction_at(now, cx);
296 editor.undo(&Undo, window, cx);
297 assert_eq!(editor.text(cx), "12cde6");
298 });
299}
300
301#[gpui::test]
302fn test_ime_composition(cx: &mut TestAppContext) {
303 init_test(cx, |_| {});
304
305 let buffer = cx.new(|cx| {
306 let mut buffer = language::Buffer::local("abcde", cx);
307 // Ensure automatic grouping doesn't occur.
308 buffer.set_group_interval(Duration::ZERO);
309 buffer
310 });
311
312 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
313 cx.add_window(|window, cx| {
314 let mut editor = build_editor(buffer.clone(), window, cx);
315
316 // Start a new IME composition.
317 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
318 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
319 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
320 assert_eq!(editor.text(cx), "äbcde");
321 assert_eq!(
322 editor.marked_text_ranges(cx),
323 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
324 );
325
326 // Finalize IME composition.
327 editor.replace_text_in_range(None, "ā", window, cx);
328 assert_eq!(editor.text(cx), "ābcde");
329 assert_eq!(editor.marked_text_ranges(cx), None);
330
331 // IME composition edits are grouped and are undone/redone at once.
332 editor.undo(&Default::default(), window, cx);
333 assert_eq!(editor.text(cx), "abcde");
334 assert_eq!(editor.marked_text_ranges(cx), None);
335 editor.redo(&Default::default(), window, cx);
336 assert_eq!(editor.text(cx), "ābcde");
337 assert_eq!(editor.marked_text_ranges(cx), None);
338
339 // Start a new IME composition.
340 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
341 assert_eq!(
342 editor.marked_text_ranges(cx),
343 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
344 );
345
346 // Undoing during an IME composition cancels it.
347 editor.undo(&Default::default(), window, cx);
348 assert_eq!(editor.text(cx), "ābcde");
349 assert_eq!(editor.marked_text_ranges(cx), None);
350
351 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
352 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
353 assert_eq!(editor.text(cx), "ābcdè");
354 assert_eq!(
355 editor.marked_text_ranges(cx),
356 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
357 );
358
359 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
360 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
361 assert_eq!(editor.text(cx), "ābcdę");
362 assert_eq!(editor.marked_text_ranges(cx), None);
363
364 // Start a new IME composition with multiple cursors.
365 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
366 s.select_ranges([
367 OffsetUtf16(1)..OffsetUtf16(1),
368 OffsetUtf16(3)..OffsetUtf16(3),
369 OffsetUtf16(5)..OffsetUtf16(5),
370 ])
371 });
372 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
373 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
374 assert_eq!(
375 editor.marked_text_ranges(cx),
376 Some(vec![
377 OffsetUtf16(0)..OffsetUtf16(3),
378 OffsetUtf16(4)..OffsetUtf16(7),
379 OffsetUtf16(8)..OffsetUtf16(11)
380 ])
381 );
382
383 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
384 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
385 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
386 assert_eq!(
387 editor.marked_text_ranges(cx),
388 Some(vec![
389 OffsetUtf16(1)..OffsetUtf16(2),
390 OffsetUtf16(5)..OffsetUtf16(6),
391 OffsetUtf16(9)..OffsetUtf16(10)
392 ])
393 );
394
395 // Finalize IME composition with multiple cursors.
396 editor.replace_text_in_range(Some(9..10), "2", window, cx);
397 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
398 assert_eq!(editor.marked_text_ranges(cx), None);
399
400 editor
401 });
402}
403
404#[gpui::test]
405fn test_selection_with_mouse(cx: &mut TestAppContext) {
406 init_test(cx, |_| {});
407
408 let editor = cx.add_window(|window, cx| {
409 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
410 build_editor(buffer, window, cx)
411 });
412
413 _ = editor.update(cx, |editor, window, cx| {
414 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
415 });
416 assert_eq!(
417 editor
418 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
419 .unwrap(),
420 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
421 );
422
423 _ = editor.update(cx, |editor, window, cx| {
424 editor.update_selection(
425 DisplayPoint::new(DisplayRow(3), 3),
426 0,
427 gpui::Point::<f32>::default(),
428 window,
429 cx,
430 );
431 });
432
433 assert_eq!(
434 editor
435 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
436 .unwrap(),
437 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
438 );
439
440 _ = editor.update(cx, |editor, window, cx| {
441 editor.update_selection(
442 DisplayPoint::new(DisplayRow(1), 1),
443 0,
444 gpui::Point::<f32>::default(),
445 window,
446 cx,
447 );
448 });
449
450 assert_eq!(
451 editor
452 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
453 .unwrap(),
454 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
455 );
456
457 _ = editor.update(cx, |editor, window, cx| {
458 editor.end_selection(window, cx);
459 editor.update_selection(
460 DisplayPoint::new(DisplayRow(3), 3),
461 0,
462 gpui::Point::<f32>::default(),
463 window,
464 cx,
465 );
466 });
467
468 assert_eq!(
469 editor
470 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
471 .unwrap(),
472 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
473 );
474
475 _ = editor.update(cx, |editor, window, cx| {
476 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
477 editor.update_selection(
478 DisplayPoint::new(DisplayRow(0), 0),
479 0,
480 gpui::Point::<f32>::default(),
481 window,
482 cx,
483 );
484 });
485
486 assert_eq!(
487 editor
488 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
489 .unwrap(),
490 [
491 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
492 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
493 ]
494 );
495
496 _ = editor.update(cx, |editor, window, cx| {
497 editor.end_selection(window, cx);
498 });
499
500 assert_eq!(
501 editor
502 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
503 .unwrap(),
504 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
505 );
506}
507
508#[gpui::test]
509fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
510 init_test(cx, |_| {});
511
512 let editor = cx.add_window(|window, cx| {
513 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
514 build_editor(buffer, window, cx)
515 });
516
517 _ = editor.update(cx, |editor, window, cx| {
518 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
519 });
520
521 _ = editor.update(cx, |editor, window, cx| {
522 editor.end_selection(window, cx);
523 });
524
525 _ = editor.update(cx, |editor, window, cx| {
526 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
527 });
528
529 _ = editor.update(cx, |editor, window, cx| {
530 editor.end_selection(window, cx);
531 });
532
533 assert_eq!(
534 editor
535 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
536 .unwrap(),
537 [
538 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
539 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
540 ]
541 );
542
543 _ = editor.update(cx, |editor, window, cx| {
544 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
545 });
546
547 _ = editor.update(cx, |editor, window, cx| {
548 editor.end_selection(window, cx);
549 });
550
551 assert_eq!(
552 editor
553 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
554 .unwrap(),
555 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
556 );
557}
558
559#[gpui::test]
560fn test_canceling_pending_selection(cx: &mut TestAppContext) {
561 init_test(cx, |_| {});
562
563 let editor = cx.add_window(|window, cx| {
564 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
565 build_editor(buffer, window, cx)
566 });
567
568 _ = editor.update(cx, |editor, window, cx| {
569 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
570 assert_eq!(
571 editor.selections.display_ranges(cx),
572 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
573 );
574 });
575
576 _ = editor.update(cx, |editor, window, cx| {
577 editor.update_selection(
578 DisplayPoint::new(DisplayRow(3), 3),
579 0,
580 gpui::Point::<f32>::default(),
581 window,
582 cx,
583 );
584 assert_eq!(
585 editor.selections.display_ranges(cx),
586 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
587 );
588 });
589
590 _ = editor.update(cx, |editor, window, cx| {
591 editor.cancel(&Cancel, window, cx);
592 editor.update_selection(
593 DisplayPoint::new(DisplayRow(1), 1),
594 0,
595 gpui::Point::<f32>::default(),
596 window,
597 cx,
598 );
599 assert_eq!(
600 editor.selections.display_ranges(cx),
601 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
602 );
603 });
604}
605
606#[gpui::test]
607fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
608 init_test(cx, |_| {});
609
610 let editor = cx.add_window(|window, cx| {
611 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
612 build_editor(buffer, window, cx)
613 });
614
615 _ = editor.update(cx, |editor, window, cx| {
616 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
617 assert_eq!(
618 editor.selections.display_ranges(cx),
619 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
620 );
621
622 editor.move_down(&Default::default(), window, cx);
623 assert_eq!(
624 editor.selections.display_ranges(cx),
625 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
626 );
627
628 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
629 assert_eq!(
630 editor.selections.display_ranges(cx),
631 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
632 );
633
634 editor.move_up(&Default::default(), window, cx);
635 assert_eq!(
636 editor.selections.display_ranges(cx),
637 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
638 );
639 });
640}
641
642#[gpui::test]
643fn test_extending_selection(cx: &mut TestAppContext) {
644 init_test(cx, |_| {});
645
646 let editor = cx.add_window(|window, cx| {
647 let buffer = MultiBuffer::build_simple("aaa bbb ccc ddd eee", cx);
648 build_editor(buffer, window, cx)
649 });
650
651 _ = editor.update(cx, |editor, window, cx| {
652 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), false, 1, window, cx);
653 editor.end_selection(window, cx);
654 assert_eq!(
655 editor.selections.display_ranges(cx),
656 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)]
657 );
658
659 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
660 editor.end_selection(window, cx);
661 assert_eq!(
662 editor.selections.display_ranges(cx),
663 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 10)]
664 );
665
666 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
667 editor.end_selection(window, cx);
668 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 2, window, cx);
669 assert_eq!(
670 editor.selections.display_ranges(cx),
671 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 11)]
672 );
673
674 editor.update_selection(
675 DisplayPoint::new(DisplayRow(0), 1),
676 0,
677 gpui::Point::<f32>::default(),
678 window,
679 cx,
680 );
681 editor.end_selection(window, cx);
682 assert_eq!(
683 editor.selections.display_ranges(cx),
684 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 0)]
685 );
686
687 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), true, 1, window, cx);
688 editor.end_selection(window, cx);
689 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), true, 2, window, cx);
690 editor.end_selection(window, cx);
691 assert_eq!(
692 editor.selections.display_ranges(cx),
693 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 7)]
694 );
695
696 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
697 assert_eq!(
698 editor.selections.display_ranges(cx),
699 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 11)]
700 );
701
702 editor.update_selection(
703 DisplayPoint::new(DisplayRow(0), 6),
704 0,
705 gpui::Point::<f32>::default(),
706 window,
707 cx,
708 );
709 assert_eq!(
710 editor.selections.display_ranges(cx),
711 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 7)]
712 );
713
714 editor.update_selection(
715 DisplayPoint::new(DisplayRow(0), 1),
716 0,
717 gpui::Point::<f32>::default(),
718 window,
719 cx,
720 );
721 editor.end_selection(window, cx);
722 assert_eq!(
723 editor.selections.display_ranges(cx),
724 [DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 0)]
725 );
726 });
727}
728
729#[gpui::test]
730fn test_clone(cx: &mut TestAppContext) {
731 init_test(cx, |_| {});
732
733 let (text, selection_ranges) = marked_text_ranges(
734 indoc! {"
735 one
736 two
737 threeˇ
738 four
739 fiveˇ
740 "},
741 true,
742 );
743
744 let editor = cx.add_window(|window, cx| {
745 let buffer = MultiBuffer::build_simple(&text, cx);
746 build_editor(buffer, window, cx)
747 });
748
749 _ = editor.update(cx, |editor, window, cx| {
750 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
751 s.select_ranges(selection_ranges.clone())
752 });
753 editor.fold_creases(
754 vec![
755 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
756 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
757 ],
758 true,
759 window,
760 cx,
761 );
762 });
763
764 let cloned_editor = editor
765 .update(cx, |editor, _, cx| {
766 cx.open_window(Default::default(), |window, cx| {
767 cx.new(|cx| editor.clone(window, cx))
768 })
769 })
770 .unwrap()
771 .unwrap();
772
773 let snapshot = editor
774 .update(cx, |e, window, cx| e.snapshot(window, cx))
775 .unwrap();
776 let cloned_snapshot = cloned_editor
777 .update(cx, |e, window, cx| e.snapshot(window, cx))
778 .unwrap();
779
780 assert_eq!(
781 cloned_editor
782 .update(cx, |e, _, cx| e.display_text(cx))
783 .unwrap(),
784 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
785 );
786 assert_eq!(
787 cloned_snapshot
788 .folds_in_range(0..text.len())
789 .collect::<Vec<_>>(),
790 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
791 );
792 assert_set_eq!(
793 cloned_editor
794 .update(cx, |editor, _, cx| editor
795 .selections
796 .ranges::<Point>(&editor.display_snapshot(cx)))
797 .unwrap(),
798 editor
799 .update(cx, |editor, _, cx| editor
800 .selections
801 .ranges(&editor.display_snapshot(cx)))
802 .unwrap()
803 );
804 assert_set_eq!(
805 cloned_editor
806 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
807 .unwrap(),
808 editor
809 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
810 .unwrap()
811 );
812}
813
814#[gpui::test]
815async fn test_navigation_history(cx: &mut TestAppContext) {
816 init_test(cx, |_| {});
817
818 use workspace::item::Item;
819
820 let fs = FakeFs::new(cx.executor());
821 let project = Project::test(fs, [], cx).await;
822 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
823 let pane = workspace
824 .update(cx, |workspace, _, _| workspace.active_pane().clone())
825 .unwrap();
826
827 _ = workspace.update(cx, |_v, window, cx| {
828 cx.new(|cx| {
829 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
830 let mut editor = build_editor(buffer, window, cx);
831 let handle = cx.entity();
832 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
833
834 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
835 editor.nav_history.as_mut().unwrap().pop_backward(cx)
836 }
837
838 // Move the cursor a small distance.
839 // Nothing is added to the navigation history.
840 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
841 s.select_display_ranges([
842 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
843 ])
844 });
845 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
846 s.select_display_ranges([
847 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
848 ])
849 });
850 assert!(pop_history(&mut editor, cx).is_none());
851
852 // Move the cursor a large distance.
853 // The history can jump back to the previous position.
854 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
855 s.select_display_ranges([
856 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
857 ])
858 });
859 let nav_entry = pop_history(&mut editor, cx).unwrap();
860 editor.navigate(nav_entry.data.unwrap(), window, cx);
861 assert_eq!(nav_entry.item.id(), cx.entity_id());
862 assert_eq!(
863 editor.selections.display_ranges(cx),
864 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
865 );
866 assert!(pop_history(&mut editor, cx).is_none());
867
868 // Move the cursor a small distance via the mouse.
869 // Nothing is added to the navigation history.
870 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
871 editor.end_selection(window, cx);
872 assert_eq!(
873 editor.selections.display_ranges(cx),
874 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
875 );
876 assert!(pop_history(&mut editor, cx).is_none());
877
878 // Move the cursor a large distance via the mouse.
879 // The history can jump back to the previous position.
880 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
881 editor.end_selection(window, cx);
882 assert_eq!(
883 editor.selections.display_ranges(cx),
884 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
885 );
886 let nav_entry = pop_history(&mut editor, cx).unwrap();
887 editor.navigate(nav_entry.data.unwrap(), window, cx);
888 assert_eq!(nav_entry.item.id(), cx.entity_id());
889 assert_eq!(
890 editor.selections.display_ranges(cx),
891 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
892 );
893 assert!(pop_history(&mut editor, cx).is_none());
894
895 // Set scroll position to check later
896 editor.set_scroll_position(gpui::Point::<f64>::new(5.5, 5.5), window, cx);
897 let original_scroll_position = editor.scroll_manager.anchor();
898
899 // Jump to the end of the document and adjust scroll
900 editor.move_to_end(&MoveToEnd, window, cx);
901 editor.set_scroll_position(gpui::Point::<f64>::new(-2.5, -0.5), window, cx);
902 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
903
904 let nav_entry = pop_history(&mut editor, cx).unwrap();
905 editor.navigate(nav_entry.data.unwrap(), window, cx);
906 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
907
908 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
909 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
910 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
911 let invalid_point = Point::new(9999, 0);
912 editor.navigate(
913 Box::new(NavigationData {
914 cursor_anchor: invalid_anchor,
915 cursor_position: invalid_point,
916 scroll_anchor: ScrollAnchor {
917 anchor: invalid_anchor,
918 offset: Default::default(),
919 },
920 scroll_top_row: invalid_point.row,
921 }),
922 window,
923 cx,
924 );
925 assert_eq!(
926 editor.selections.display_ranges(cx),
927 &[editor.max_point(cx)..editor.max_point(cx)]
928 );
929 assert_eq!(
930 editor.scroll_position(cx),
931 gpui::Point::new(0., editor.max_point(cx).row().as_f64())
932 );
933
934 editor
935 })
936 });
937}
938
939#[gpui::test]
940fn test_cancel(cx: &mut TestAppContext) {
941 init_test(cx, |_| {});
942
943 let editor = cx.add_window(|window, cx| {
944 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
945 build_editor(buffer, window, cx)
946 });
947
948 _ = editor.update(cx, |editor, window, cx| {
949 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
950 editor.update_selection(
951 DisplayPoint::new(DisplayRow(1), 1),
952 0,
953 gpui::Point::<f32>::default(),
954 window,
955 cx,
956 );
957 editor.end_selection(window, cx);
958
959 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
960 editor.update_selection(
961 DisplayPoint::new(DisplayRow(0), 3),
962 0,
963 gpui::Point::<f32>::default(),
964 window,
965 cx,
966 );
967 editor.end_selection(window, cx);
968 assert_eq!(
969 editor.selections.display_ranges(cx),
970 [
971 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
972 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
973 ]
974 );
975 });
976
977 _ = editor.update(cx, |editor, window, cx| {
978 editor.cancel(&Cancel, window, cx);
979 assert_eq!(
980 editor.selections.display_ranges(cx),
981 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
982 );
983 });
984
985 _ = editor.update(cx, |editor, window, cx| {
986 editor.cancel(&Cancel, window, cx);
987 assert_eq!(
988 editor.selections.display_ranges(cx),
989 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
990 );
991 });
992}
993
994#[gpui::test]
995fn test_fold_action(cx: &mut TestAppContext) {
996 init_test(cx, |_| {});
997
998 let editor = cx.add_window(|window, cx| {
999 let buffer = MultiBuffer::build_simple(
1000 &"
1001 impl Foo {
1002 // Hello!
1003
1004 fn a() {
1005 1
1006 }
1007
1008 fn b() {
1009 2
1010 }
1011
1012 fn c() {
1013 3
1014 }
1015 }
1016 "
1017 .unindent(),
1018 cx,
1019 );
1020 build_editor(buffer, window, cx)
1021 });
1022
1023 _ = editor.update(cx, |editor, window, cx| {
1024 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1025 s.select_display_ranges([
1026 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
1027 ]);
1028 });
1029 editor.fold(&Fold, window, cx);
1030 assert_eq!(
1031 editor.display_text(cx),
1032 "
1033 impl Foo {
1034 // Hello!
1035
1036 fn a() {
1037 1
1038 }
1039
1040 fn b() {⋯
1041 }
1042
1043 fn c() {⋯
1044 }
1045 }
1046 "
1047 .unindent(),
1048 );
1049
1050 editor.fold(&Fold, window, cx);
1051 assert_eq!(
1052 editor.display_text(cx),
1053 "
1054 impl Foo {⋯
1055 }
1056 "
1057 .unindent(),
1058 );
1059
1060 editor.unfold_lines(&UnfoldLines, window, cx);
1061 assert_eq!(
1062 editor.display_text(cx),
1063 "
1064 impl Foo {
1065 // Hello!
1066
1067 fn a() {
1068 1
1069 }
1070
1071 fn b() {⋯
1072 }
1073
1074 fn c() {⋯
1075 }
1076 }
1077 "
1078 .unindent(),
1079 );
1080
1081 editor.unfold_lines(&UnfoldLines, window, cx);
1082 assert_eq!(
1083 editor.display_text(cx),
1084 editor.buffer.read(cx).read(cx).text()
1085 );
1086 });
1087}
1088
1089#[gpui::test]
1090fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
1091 init_test(cx, |_| {});
1092
1093 let editor = cx.add_window(|window, cx| {
1094 let buffer = MultiBuffer::build_simple(
1095 &"
1096 class Foo:
1097 # Hello!
1098
1099 def a():
1100 print(1)
1101
1102 def b():
1103 print(2)
1104
1105 def c():
1106 print(3)
1107 "
1108 .unindent(),
1109 cx,
1110 );
1111 build_editor(buffer, window, cx)
1112 });
1113
1114 _ = editor.update(cx, |editor, window, cx| {
1115 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1116 s.select_display_ranges([
1117 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
1118 ]);
1119 });
1120 editor.fold(&Fold, window, cx);
1121 assert_eq!(
1122 editor.display_text(cx),
1123 "
1124 class Foo:
1125 # Hello!
1126
1127 def a():
1128 print(1)
1129
1130 def b():⋯
1131
1132 def c():⋯
1133 "
1134 .unindent(),
1135 );
1136
1137 editor.fold(&Fold, window, cx);
1138 assert_eq!(
1139 editor.display_text(cx),
1140 "
1141 class Foo:⋯
1142 "
1143 .unindent(),
1144 );
1145
1146 editor.unfold_lines(&UnfoldLines, window, cx);
1147 assert_eq!(
1148 editor.display_text(cx),
1149 "
1150 class Foo:
1151 # Hello!
1152
1153 def a():
1154 print(1)
1155
1156 def b():⋯
1157
1158 def c():⋯
1159 "
1160 .unindent(),
1161 );
1162
1163 editor.unfold_lines(&UnfoldLines, window, cx);
1164 assert_eq!(
1165 editor.display_text(cx),
1166 editor.buffer.read(cx).read(cx).text()
1167 );
1168 });
1169}
1170
1171#[gpui::test]
1172fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1173 init_test(cx, |_| {});
1174
1175 let editor = cx.add_window(|window, cx| {
1176 let buffer = MultiBuffer::build_simple(
1177 &"
1178 class Foo:
1179 # Hello!
1180
1181 def a():
1182 print(1)
1183
1184 def b():
1185 print(2)
1186
1187
1188 def c():
1189 print(3)
1190
1191
1192 "
1193 .unindent(),
1194 cx,
1195 );
1196 build_editor(buffer, window, cx)
1197 });
1198
1199 _ = editor.update(cx, |editor, window, cx| {
1200 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1201 s.select_display_ranges([
1202 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1203 ]);
1204 });
1205 editor.fold(&Fold, window, cx);
1206 assert_eq!(
1207 editor.display_text(cx),
1208 "
1209 class Foo:
1210 # Hello!
1211
1212 def a():
1213 print(1)
1214
1215 def b():⋯
1216
1217
1218 def c():⋯
1219
1220
1221 "
1222 .unindent(),
1223 );
1224
1225 editor.fold(&Fold, window, cx);
1226 assert_eq!(
1227 editor.display_text(cx),
1228 "
1229 class Foo:⋯
1230
1231
1232 "
1233 .unindent(),
1234 );
1235
1236 editor.unfold_lines(&UnfoldLines, window, cx);
1237 assert_eq!(
1238 editor.display_text(cx),
1239 "
1240 class Foo:
1241 # Hello!
1242
1243 def a():
1244 print(1)
1245
1246 def b():⋯
1247
1248
1249 def c():⋯
1250
1251
1252 "
1253 .unindent(),
1254 );
1255
1256 editor.unfold_lines(&UnfoldLines, window, cx);
1257 assert_eq!(
1258 editor.display_text(cx),
1259 editor.buffer.read(cx).read(cx).text()
1260 );
1261 });
1262}
1263
1264#[gpui::test]
1265fn test_fold_at_level(cx: &mut TestAppContext) {
1266 init_test(cx, |_| {});
1267
1268 let editor = cx.add_window(|window, cx| {
1269 let buffer = MultiBuffer::build_simple(
1270 &"
1271 class Foo:
1272 # Hello!
1273
1274 def a():
1275 print(1)
1276
1277 def b():
1278 print(2)
1279
1280
1281 class Bar:
1282 # World!
1283
1284 def a():
1285 print(1)
1286
1287 def b():
1288 print(2)
1289
1290
1291 "
1292 .unindent(),
1293 cx,
1294 );
1295 build_editor(buffer, window, cx)
1296 });
1297
1298 _ = editor.update(cx, |editor, window, cx| {
1299 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1300 assert_eq!(
1301 editor.display_text(cx),
1302 "
1303 class Foo:
1304 # Hello!
1305
1306 def a():⋯
1307
1308 def b():⋯
1309
1310
1311 class Bar:
1312 # World!
1313
1314 def a():⋯
1315
1316 def b():⋯
1317
1318
1319 "
1320 .unindent(),
1321 );
1322
1323 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1324 assert_eq!(
1325 editor.display_text(cx),
1326 "
1327 class Foo:⋯
1328
1329
1330 class Bar:⋯
1331
1332
1333 "
1334 .unindent(),
1335 );
1336
1337 editor.unfold_all(&UnfoldAll, window, cx);
1338 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1339 assert_eq!(
1340 editor.display_text(cx),
1341 "
1342 class Foo:
1343 # Hello!
1344
1345 def a():
1346 print(1)
1347
1348 def b():
1349 print(2)
1350
1351
1352 class Bar:
1353 # World!
1354
1355 def a():
1356 print(1)
1357
1358 def b():
1359 print(2)
1360
1361
1362 "
1363 .unindent(),
1364 );
1365
1366 assert_eq!(
1367 editor.display_text(cx),
1368 editor.buffer.read(cx).read(cx).text()
1369 );
1370 let (_, positions) = marked_text_ranges(
1371 &"
1372 class Foo:
1373 # Hello!
1374
1375 def a():
1376 print(1)
1377
1378 def b():
1379 p«riˇ»nt(2)
1380
1381
1382 class Bar:
1383 # World!
1384
1385 def a():
1386 «ˇprint(1)
1387
1388 def b():
1389 print(2)»
1390
1391
1392 "
1393 .unindent(),
1394 true,
1395 );
1396
1397 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
1398 s.select_ranges(positions)
1399 });
1400
1401 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1402 assert_eq!(
1403 editor.display_text(cx),
1404 "
1405 class Foo:
1406 # Hello!
1407
1408 def a():⋯
1409
1410 def b():
1411 print(2)
1412
1413
1414 class Bar:
1415 # World!
1416
1417 def a():
1418 print(1)
1419
1420 def b():
1421 print(2)
1422
1423
1424 "
1425 .unindent(),
1426 );
1427 });
1428}
1429
1430#[gpui::test]
1431fn test_move_cursor(cx: &mut TestAppContext) {
1432 init_test(cx, |_| {});
1433
1434 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1435 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1436
1437 buffer.update(cx, |buffer, cx| {
1438 buffer.edit(
1439 vec![
1440 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1441 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1442 ],
1443 None,
1444 cx,
1445 );
1446 });
1447 _ = editor.update(cx, |editor, window, cx| {
1448 assert_eq!(
1449 editor.selections.display_ranges(cx),
1450 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1451 );
1452
1453 editor.move_down(&MoveDown, window, cx);
1454 assert_eq!(
1455 editor.selections.display_ranges(cx),
1456 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1457 );
1458
1459 editor.move_right(&MoveRight, window, cx);
1460 assert_eq!(
1461 editor.selections.display_ranges(cx),
1462 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1463 );
1464
1465 editor.move_left(&MoveLeft, window, cx);
1466 assert_eq!(
1467 editor.selections.display_ranges(cx),
1468 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1469 );
1470
1471 editor.move_up(&MoveUp, window, cx);
1472 assert_eq!(
1473 editor.selections.display_ranges(cx),
1474 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1475 );
1476
1477 editor.move_to_end(&MoveToEnd, window, cx);
1478 assert_eq!(
1479 editor.selections.display_ranges(cx),
1480 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1481 );
1482
1483 editor.move_to_beginning(&MoveToBeginning, window, cx);
1484 assert_eq!(
1485 editor.selections.display_ranges(cx),
1486 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1487 );
1488
1489 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1490 s.select_display_ranges([
1491 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1492 ]);
1493 });
1494 editor.select_to_beginning(&SelectToBeginning, window, cx);
1495 assert_eq!(
1496 editor.selections.display_ranges(cx),
1497 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1498 );
1499
1500 editor.select_to_end(&SelectToEnd, window, cx);
1501 assert_eq!(
1502 editor.selections.display_ranges(cx),
1503 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1504 );
1505 });
1506}
1507
1508#[gpui::test]
1509fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1510 init_test(cx, |_| {});
1511
1512 let editor = cx.add_window(|window, cx| {
1513 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1514 build_editor(buffer, window, cx)
1515 });
1516
1517 assert_eq!('🟥'.len_utf8(), 4);
1518 assert_eq!('α'.len_utf8(), 2);
1519
1520 _ = editor.update(cx, |editor, window, cx| {
1521 editor.fold_creases(
1522 vec![
1523 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1524 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1525 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1526 ],
1527 true,
1528 window,
1529 cx,
1530 );
1531 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1532
1533 editor.move_right(&MoveRight, window, cx);
1534 assert_eq!(
1535 editor.selections.display_ranges(cx),
1536 &[empty_range(0, "🟥".len())]
1537 );
1538 editor.move_right(&MoveRight, window, cx);
1539 assert_eq!(
1540 editor.selections.display_ranges(cx),
1541 &[empty_range(0, "🟥🟧".len())]
1542 );
1543 editor.move_right(&MoveRight, window, cx);
1544 assert_eq!(
1545 editor.selections.display_ranges(cx),
1546 &[empty_range(0, "🟥🟧⋯".len())]
1547 );
1548
1549 editor.move_down(&MoveDown, window, cx);
1550 assert_eq!(
1551 editor.selections.display_ranges(cx),
1552 &[empty_range(1, "ab⋯e".len())]
1553 );
1554 editor.move_left(&MoveLeft, window, cx);
1555 assert_eq!(
1556 editor.selections.display_ranges(cx),
1557 &[empty_range(1, "ab⋯".len())]
1558 );
1559 editor.move_left(&MoveLeft, window, cx);
1560 assert_eq!(
1561 editor.selections.display_ranges(cx),
1562 &[empty_range(1, "ab".len())]
1563 );
1564 editor.move_left(&MoveLeft, window, cx);
1565 assert_eq!(
1566 editor.selections.display_ranges(cx),
1567 &[empty_range(1, "a".len())]
1568 );
1569
1570 editor.move_down(&MoveDown, window, cx);
1571 assert_eq!(
1572 editor.selections.display_ranges(cx),
1573 &[empty_range(2, "α".len())]
1574 );
1575 editor.move_right(&MoveRight, window, cx);
1576 assert_eq!(
1577 editor.selections.display_ranges(cx),
1578 &[empty_range(2, "αβ".len())]
1579 );
1580 editor.move_right(&MoveRight, window, cx);
1581 assert_eq!(
1582 editor.selections.display_ranges(cx),
1583 &[empty_range(2, "αβ⋯".len())]
1584 );
1585 editor.move_right(&MoveRight, window, cx);
1586 assert_eq!(
1587 editor.selections.display_ranges(cx),
1588 &[empty_range(2, "αβ⋯ε".len())]
1589 );
1590
1591 editor.move_up(&MoveUp, window, cx);
1592 assert_eq!(
1593 editor.selections.display_ranges(cx),
1594 &[empty_range(1, "ab⋯e".len())]
1595 );
1596 editor.move_down(&MoveDown, window, cx);
1597 assert_eq!(
1598 editor.selections.display_ranges(cx),
1599 &[empty_range(2, "αβ⋯ε".len())]
1600 );
1601 editor.move_up(&MoveUp, window, cx);
1602 assert_eq!(
1603 editor.selections.display_ranges(cx),
1604 &[empty_range(1, "ab⋯e".len())]
1605 );
1606
1607 editor.move_up(&MoveUp, window, cx);
1608 assert_eq!(
1609 editor.selections.display_ranges(cx),
1610 &[empty_range(0, "🟥🟧".len())]
1611 );
1612 editor.move_left(&MoveLeft, window, cx);
1613 assert_eq!(
1614 editor.selections.display_ranges(cx),
1615 &[empty_range(0, "🟥".len())]
1616 );
1617 editor.move_left(&MoveLeft, window, cx);
1618 assert_eq!(
1619 editor.selections.display_ranges(cx),
1620 &[empty_range(0, "".len())]
1621 );
1622 });
1623}
1624
1625#[gpui::test]
1626fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1627 init_test(cx, |_| {});
1628
1629 let editor = cx.add_window(|window, cx| {
1630 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1631 build_editor(buffer, window, cx)
1632 });
1633 _ = editor.update(cx, |editor, window, cx| {
1634 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1635 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1636 });
1637
1638 // moving above start of document should move selection to start of document,
1639 // but the next move down should still be at the original goal_x
1640 editor.move_up(&MoveUp, window, cx);
1641 assert_eq!(
1642 editor.selections.display_ranges(cx),
1643 &[empty_range(0, "".len())]
1644 );
1645
1646 editor.move_down(&MoveDown, window, cx);
1647 assert_eq!(
1648 editor.selections.display_ranges(cx),
1649 &[empty_range(1, "abcd".len())]
1650 );
1651
1652 editor.move_down(&MoveDown, window, cx);
1653 assert_eq!(
1654 editor.selections.display_ranges(cx),
1655 &[empty_range(2, "αβγ".len())]
1656 );
1657
1658 editor.move_down(&MoveDown, window, cx);
1659 assert_eq!(
1660 editor.selections.display_ranges(cx),
1661 &[empty_range(3, "abcd".len())]
1662 );
1663
1664 editor.move_down(&MoveDown, window, cx);
1665 assert_eq!(
1666 editor.selections.display_ranges(cx),
1667 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1668 );
1669
1670 // moving past end of document should not change goal_x
1671 editor.move_down(&MoveDown, window, cx);
1672 assert_eq!(
1673 editor.selections.display_ranges(cx),
1674 &[empty_range(5, "".len())]
1675 );
1676
1677 editor.move_down(&MoveDown, window, cx);
1678 assert_eq!(
1679 editor.selections.display_ranges(cx),
1680 &[empty_range(5, "".len())]
1681 );
1682
1683 editor.move_up(&MoveUp, window, cx);
1684 assert_eq!(
1685 editor.selections.display_ranges(cx),
1686 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1687 );
1688
1689 editor.move_up(&MoveUp, window, cx);
1690 assert_eq!(
1691 editor.selections.display_ranges(cx),
1692 &[empty_range(3, "abcd".len())]
1693 );
1694
1695 editor.move_up(&MoveUp, window, cx);
1696 assert_eq!(
1697 editor.selections.display_ranges(cx),
1698 &[empty_range(2, "αβγ".len())]
1699 );
1700 });
1701}
1702
1703#[gpui::test]
1704fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1705 init_test(cx, |_| {});
1706 let move_to_beg = MoveToBeginningOfLine {
1707 stop_at_soft_wraps: true,
1708 stop_at_indent: true,
1709 };
1710
1711 let delete_to_beg = DeleteToBeginningOfLine {
1712 stop_at_indent: false,
1713 };
1714
1715 let move_to_end = MoveToEndOfLine {
1716 stop_at_soft_wraps: true,
1717 };
1718
1719 let editor = cx.add_window(|window, cx| {
1720 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1721 build_editor(buffer, window, cx)
1722 });
1723 _ = editor.update(cx, |editor, window, cx| {
1724 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1725 s.select_display_ranges([
1726 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1727 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1728 ]);
1729 });
1730 });
1731
1732 _ = editor.update(cx, |editor, window, cx| {
1733 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1734 assert_eq!(
1735 editor.selections.display_ranges(cx),
1736 &[
1737 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1738 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1739 ]
1740 );
1741 });
1742
1743 _ = editor.update(cx, |editor, window, cx| {
1744 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1745 assert_eq!(
1746 editor.selections.display_ranges(cx),
1747 &[
1748 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1749 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1750 ]
1751 );
1752 });
1753
1754 _ = editor.update(cx, |editor, window, cx| {
1755 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1756 assert_eq!(
1757 editor.selections.display_ranges(cx),
1758 &[
1759 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1760 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1761 ]
1762 );
1763 });
1764
1765 _ = editor.update(cx, |editor, window, cx| {
1766 editor.move_to_end_of_line(&move_to_end, window, cx);
1767 assert_eq!(
1768 editor.selections.display_ranges(cx),
1769 &[
1770 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1771 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1772 ]
1773 );
1774 });
1775
1776 // Moving to the end of line again is a no-op.
1777 _ = editor.update(cx, |editor, window, cx| {
1778 editor.move_to_end_of_line(&move_to_end, window, cx);
1779 assert_eq!(
1780 editor.selections.display_ranges(cx),
1781 &[
1782 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1783 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1784 ]
1785 );
1786 });
1787
1788 _ = editor.update(cx, |editor, window, cx| {
1789 editor.move_left(&MoveLeft, window, cx);
1790 editor.select_to_beginning_of_line(
1791 &SelectToBeginningOfLine {
1792 stop_at_soft_wraps: true,
1793 stop_at_indent: true,
1794 },
1795 window,
1796 cx,
1797 );
1798 assert_eq!(
1799 editor.selections.display_ranges(cx),
1800 &[
1801 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1802 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1803 ]
1804 );
1805 });
1806
1807 _ = editor.update(cx, |editor, window, cx| {
1808 editor.select_to_beginning_of_line(
1809 &SelectToBeginningOfLine {
1810 stop_at_soft_wraps: true,
1811 stop_at_indent: true,
1812 },
1813 window,
1814 cx,
1815 );
1816 assert_eq!(
1817 editor.selections.display_ranges(cx),
1818 &[
1819 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1820 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1821 ]
1822 );
1823 });
1824
1825 _ = editor.update(cx, |editor, window, cx| {
1826 editor.select_to_beginning_of_line(
1827 &SelectToBeginningOfLine {
1828 stop_at_soft_wraps: true,
1829 stop_at_indent: true,
1830 },
1831 window,
1832 cx,
1833 );
1834 assert_eq!(
1835 editor.selections.display_ranges(cx),
1836 &[
1837 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1838 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1839 ]
1840 );
1841 });
1842
1843 _ = editor.update(cx, |editor, window, cx| {
1844 editor.select_to_end_of_line(
1845 &SelectToEndOfLine {
1846 stop_at_soft_wraps: true,
1847 },
1848 window,
1849 cx,
1850 );
1851 assert_eq!(
1852 editor.selections.display_ranges(cx),
1853 &[
1854 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1855 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1856 ]
1857 );
1858 });
1859
1860 _ = editor.update(cx, |editor, window, cx| {
1861 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1862 assert_eq!(editor.display_text(cx), "ab\n de");
1863 assert_eq!(
1864 editor.selections.display_ranges(cx),
1865 &[
1866 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1867 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1868 ]
1869 );
1870 });
1871
1872 _ = editor.update(cx, |editor, window, cx| {
1873 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1874 assert_eq!(editor.display_text(cx), "\n");
1875 assert_eq!(
1876 editor.selections.display_ranges(cx),
1877 &[
1878 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1879 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1880 ]
1881 );
1882 });
1883}
1884
1885#[gpui::test]
1886fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1887 init_test(cx, |_| {});
1888 let move_to_beg = MoveToBeginningOfLine {
1889 stop_at_soft_wraps: false,
1890 stop_at_indent: false,
1891 };
1892
1893 let move_to_end = MoveToEndOfLine {
1894 stop_at_soft_wraps: false,
1895 };
1896
1897 let editor = cx.add_window(|window, cx| {
1898 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1899 build_editor(buffer, window, cx)
1900 });
1901
1902 _ = editor.update(cx, |editor, window, cx| {
1903 editor.set_wrap_width(Some(140.0.into()), cx);
1904
1905 // We expect the following lines after wrapping
1906 // ```
1907 // thequickbrownfox
1908 // jumpedoverthelazydo
1909 // gs
1910 // ```
1911 // The final `gs` was soft-wrapped onto a new line.
1912 assert_eq!(
1913 "thequickbrownfox\njumpedoverthelaz\nydogs",
1914 editor.display_text(cx),
1915 );
1916
1917 // First, let's assert behavior on the first line, that was not soft-wrapped.
1918 // Start the cursor at the `k` on the first line
1919 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1920 s.select_display_ranges([
1921 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1922 ]);
1923 });
1924
1925 // Moving to the beginning of the line should put us at the beginning of the line.
1926 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1927 assert_eq!(
1928 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1929 editor.selections.display_ranges(cx)
1930 );
1931
1932 // Moving to the end of the line should put us at the end of the line.
1933 editor.move_to_end_of_line(&move_to_end, window, cx);
1934 assert_eq!(
1935 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1936 editor.selections.display_ranges(cx)
1937 );
1938
1939 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1940 // Start the cursor at the last line (`y` that was wrapped to a new line)
1941 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1942 s.select_display_ranges([
1943 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1944 ]);
1945 });
1946
1947 // Moving to the beginning of the line should put us at the start of the second line of
1948 // display text, i.e., the `j`.
1949 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1950 assert_eq!(
1951 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1952 editor.selections.display_ranges(cx)
1953 );
1954
1955 // Moving to the beginning of the line again should be a no-op.
1956 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1957 assert_eq!(
1958 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1959 editor.selections.display_ranges(cx)
1960 );
1961
1962 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1963 // next display line.
1964 editor.move_to_end_of_line(&move_to_end, window, cx);
1965 assert_eq!(
1966 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1967 editor.selections.display_ranges(cx)
1968 );
1969
1970 // Moving to the end of the line again should be a no-op.
1971 editor.move_to_end_of_line(&move_to_end, window, cx);
1972 assert_eq!(
1973 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1974 editor.selections.display_ranges(cx)
1975 );
1976 });
1977}
1978
1979#[gpui::test]
1980fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1981 init_test(cx, |_| {});
1982
1983 let move_to_beg = MoveToBeginningOfLine {
1984 stop_at_soft_wraps: true,
1985 stop_at_indent: true,
1986 };
1987
1988 let select_to_beg = SelectToBeginningOfLine {
1989 stop_at_soft_wraps: true,
1990 stop_at_indent: true,
1991 };
1992
1993 let delete_to_beg = DeleteToBeginningOfLine {
1994 stop_at_indent: true,
1995 };
1996
1997 let move_to_end = MoveToEndOfLine {
1998 stop_at_soft_wraps: false,
1999 };
2000
2001 let editor = cx.add_window(|window, cx| {
2002 let buffer = MultiBuffer::build_simple("abc\n def", cx);
2003 build_editor(buffer, window, cx)
2004 });
2005
2006 _ = editor.update(cx, |editor, window, cx| {
2007 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2008 s.select_display_ranges([
2009 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
2010 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
2011 ]);
2012 });
2013
2014 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
2015 // and the second cursor at the first non-whitespace character in the line.
2016 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2017 assert_eq!(
2018 editor.selections.display_ranges(cx),
2019 &[
2020 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
2021 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2022 ]
2023 );
2024
2025 // Moving to the beginning of the line again should be a no-op for the first cursor,
2026 // and should move the second cursor to the beginning of the line.
2027 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2028 assert_eq!(
2029 editor.selections.display_ranges(cx),
2030 &[
2031 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
2032 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
2033 ]
2034 );
2035
2036 // Moving to the beginning of the line again should still be a no-op for the first cursor,
2037 // and should move the second cursor back to the first non-whitespace character in the line.
2038 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2039 assert_eq!(
2040 editor.selections.display_ranges(cx),
2041 &[
2042 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
2043 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2044 ]
2045 );
2046
2047 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
2048 // and to the first non-whitespace character in the line for the second cursor.
2049 editor.move_to_end_of_line(&move_to_end, window, cx);
2050 editor.move_left(&MoveLeft, window, cx);
2051 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
2052 assert_eq!(
2053 editor.selections.display_ranges(cx),
2054 &[
2055 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
2056 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
2057 ]
2058 );
2059
2060 // Selecting to the beginning of the line again should be a no-op for the first cursor,
2061 // and should select to the beginning of the line for the second cursor.
2062 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
2063 assert_eq!(
2064 editor.selections.display_ranges(cx),
2065 &[
2066 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
2067 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
2068 ]
2069 );
2070
2071 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
2072 // and should delete to the first non-whitespace character in the line for the second cursor.
2073 editor.move_to_end_of_line(&move_to_end, window, cx);
2074 editor.move_left(&MoveLeft, window, cx);
2075 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
2076 assert_eq!(editor.text(cx), "c\n f");
2077 });
2078}
2079
2080#[gpui::test]
2081fn test_beginning_of_line_with_cursor_between_line_start_and_indent(cx: &mut TestAppContext) {
2082 init_test(cx, |_| {});
2083
2084 let move_to_beg = MoveToBeginningOfLine {
2085 stop_at_soft_wraps: true,
2086 stop_at_indent: true,
2087 };
2088
2089 let editor = cx.add_window(|window, cx| {
2090 let buffer = MultiBuffer::build_simple(" hello\nworld", cx);
2091 build_editor(buffer, window, cx)
2092 });
2093
2094 _ = editor.update(cx, |editor, window, cx| {
2095 // test cursor between line_start and indent_start
2096 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2097 s.select_display_ranges([
2098 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3)
2099 ]);
2100 });
2101
2102 // cursor should move to line_start
2103 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2104 assert_eq!(
2105 editor.selections.display_ranges(cx),
2106 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
2107 );
2108
2109 // cursor should move to indent_start
2110 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2111 assert_eq!(
2112 editor.selections.display_ranges(cx),
2113 &[DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 4)]
2114 );
2115
2116 // cursor should move to back to line_start
2117 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2118 assert_eq!(
2119 editor.selections.display_ranges(cx),
2120 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
2121 );
2122 });
2123}
2124
2125#[gpui::test]
2126fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
2127 init_test(cx, |_| {});
2128
2129 let editor = cx.add_window(|window, cx| {
2130 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
2131 build_editor(buffer, window, cx)
2132 });
2133 _ = editor.update(cx, |editor, window, cx| {
2134 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2135 s.select_display_ranges([
2136 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
2137 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
2138 ])
2139 });
2140 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2141 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
2142
2143 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2144 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
2145
2146 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2147 assert_selection_ranges("use ˇstd::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
2148
2149 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2150 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
2151
2152 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2153 assert_selection_ranges("ˇuse std::str::{foo, ˇbar}\n\n {baz.qux()}", editor, cx);
2154
2155 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2156 assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
2157
2158 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2159 assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
2160
2161 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2162 assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
2163
2164 editor.move_right(&MoveRight, window, cx);
2165 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2166 assert_selection_ranges(
2167 "use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}",
2168 editor,
2169 cx,
2170 );
2171
2172 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2173 assert_selection_ranges(
2174 "use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
2175 editor,
2176 cx,
2177 );
2178
2179 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
2180 assert_selection_ranges(
2181 "use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
2182 editor,
2183 cx,
2184 );
2185 });
2186}
2187
2188#[gpui::test]
2189fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
2190 init_test(cx, |_| {});
2191
2192 let editor = cx.add_window(|window, cx| {
2193 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
2194 build_editor(buffer, window, cx)
2195 });
2196
2197 _ = editor.update(cx, |editor, window, cx| {
2198 editor.set_wrap_width(Some(140.0.into()), cx);
2199 assert_eq!(
2200 editor.display_text(cx),
2201 "use one::{\n two::three::\n four::five\n};"
2202 );
2203
2204 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2205 s.select_display_ranges([
2206 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
2207 ]);
2208 });
2209
2210 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2211 assert_eq!(
2212 editor.selections.display_ranges(cx),
2213 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
2214 );
2215
2216 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2217 assert_eq!(
2218 editor.selections.display_ranges(cx),
2219 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2220 );
2221
2222 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2223 assert_eq!(
2224 editor.selections.display_ranges(cx),
2225 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2226 );
2227
2228 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2229 assert_eq!(
2230 editor.selections.display_ranges(cx),
2231 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2232 );
2233
2234 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2235 assert_eq!(
2236 editor.selections.display_ranges(cx),
2237 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2238 );
2239
2240 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2241 assert_eq!(
2242 editor.selections.display_ranges(cx),
2243 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2244 );
2245 });
2246}
2247
2248#[gpui::test]
2249async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2250 init_test(cx, |_| {});
2251 let mut cx = EditorTestContext::new(cx).await;
2252
2253 let line_height = cx.editor(|editor, window, _| {
2254 editor
2255 .style()
2256 .unwrap()
2257 .text
2258 .line_height_in_pixels(window.rem_size())
2259 });
2260 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2261
2262 cx.set_state(
2263 &r#"ˇone
2264 two
2265
2266 three
2267 fourˇ
2268 five
2269
2270 six"#
2271 .unindent(),
2272 );
2273
2274 cx.update_editor(|editor, window, cx| {
2275 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2276 });
2277 cx.assert_editor_state(
2278 &r#"one
2279 two
2280 ˇ
2281 three
2282 four
2283 five
2284 ˇ
2285 six"#
2286 .unindent(),
2287 );
2288
2289 cx.update_editor(|editor, window, cx| {
2290 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2291 });
2292 cx.assert_editor_state(
2293 &r#"one
2294 two
2295
2296 three
2297 four
2298 five
2299 ˇ
2300 sixˇ"#
2301 .unindent(),
2302 );
2303
2304 cx.update_editor(|editor, window, cx| {
2305 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2306 });
2307 cx.assert_editor_state(
2308 &r#"one
2309 two
2310
2311 three
2312 four
2313 five
2314
2315 sixˇ"#
2316 .unindent(),
2317 );
2318
2319 cx.update_editor(|editor, window, cx| {
2320 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2321 });
2322 cx.assert_editor_state(
2323 &r#"one
2324 two
2325
2326 three
2327 four
2328 five
2329 ˇ
2330 six"#
2331 .unindent(),
2332 );
2333
2334 cx.update_editor(|editor, window, cx| {
2335 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2336 });
2337 cx.assert_editor_state(
2338 &r#"one
2339 two
2340 ˇ
2341 three
2342 four
2343 five
2344
2345 six"#
2346 .unindent(),
2347 );
2348
2349 cx.update_editor(|editor, window, cx| {
2350 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2351 });
2352 cx.assert_editor_state(
2353 &r#"ˇone
2354 two
2355
2356 three
2357 four
2358 five
2359
2360 six"#
2361 .unindent(),
2362 );
2363}
2364
2365#[gpui::test]
2366async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2367 init_test(cx, |_| {});
2368 let mut cx = EditorTestContext::new(cx).await;
2369 let line_height = cx.editor(|editor, window, _| {
2370 editor
2371 .style()
2372 .unwrap()
2373 .text
2374 .line_height_in_pixels(window.rem_size())
2375 });
2376 let window = cx.window;
2377 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2378
2379 cx.set_state(
2380 r#"ˇone
2381 two
2382 three
2383 four
2384 five
2385 six
2386 seven
2387 eight
2388 nine
2389 ten
2390 "#,
2391 );
2392
2393 cx.update_editor(|editor, window, cx| {
2394 assert_eq!(
2395 editor.snapshot(window, cx).scroll_position(),
2396 gpui::Point::new(0., 0.)
2397 );
2398 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2399 assert_eq!(
2400 editor.snapshot(window, cx).scroll_position(),
2401 gpui::Point::new(0., 3.)
2402 );
2403 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2404 assert_eq!(
2405 editor.snapshot(window, cx).scroll_position(),
2406 gpui::Point::new(0., 6.)
2407 );
2408 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2409 assert_eq!(
2410 editor.snapshot(window, cx).scroll_position(),
2411 gpui::Point::new(0., 3.)
2412 );
2413
2414 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2415 assert_eq!(
2416 editor.snapshot(window, cx).scroll_position(),
2417 gpui::Point::new(0., 1.)
2418 );
2419 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2420 assert_eq!(
2421 editor.snapshot(window, cx).scroll_position(),
2422 gpui::Point::new(0., 3.)
2423 );
2424 });
2425}
2426
2427#[gpui::test]
2428async fn test_autoscroll(cx: &mut TestAppContext) {
2429 init_test(cx, |_| {});
2430 let mut cx = EditorTestContext::new(cx).await;
2431
2432 let line_height = cx.update_editor(|editor, window, cx| {
2433 editor.set_vertical_scroll_margin(2, cx);
2434 editor
2435 .style()
2436 .unwrap()
2437 .text
2438 .line_height_in_pixels(window.rem_size())
2439 });
2440 let window = cx.window;
2441 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2442
2443 cx.set_state(
2444 r#"ˇone
2445 two
2446 three
2447 four
2448 five
2449 six
2450 seven
2451 eight
2452 nine
2453 ten
2454 "#,
2455 );
2456 cx.update_editor(|editor, window, cx| {
2457 assert_eq!(
2458 editor.snapshot(window, cx).scroll_position(),
2459 gpui::Point::new(0., 0.0)
2460 );
2461 });
2462
2463 // Add a cursor below the visible area. Since both cursors cannot fit
2464 // on screen, the editor autoscrolls to reveal the newest cursor, and
2465 // allows the vertical scroll margin below that cursor.
2466 cx.update_editor(|editor, window, cx| {
2467 editor.change_selections(Default::default(), window, cx, |selections| {
2468 selections.select_ranges([
2469 Point::new(0, 0)..Point::new(0, 0),
2470 Point::new(6, 0)..Point::new(6, 0),
2471 ]);
2472 })
2473 });
2474 cx.update_editor(|editor, window, cx| {
2475 assert_eq!(
2476 editor.snapshot(window, cx).scroll_position(),
2477 gpui::Point::new(0., 3.0)
2478 );
2479 });
2480
2481 // Move down. The editor cursor scrolls down to track the newest cursor.
2482 cx.update_editor(|editor, window, cx| {
2483 editor.move_down(&Default::default(), window, cx);
2484 });
2485 cx.update_editor(|editor, window, cx| {
2486 assert_eq!(
2487 editor.snapshot(window, cx).scroll_position(),
2488 gpui::Point::new(0., 4.0)
2489 );
2490 });
2491
2492 // Add a cursor above the visible area. Since both cursors fit on screen,
2493 // the editor scrolls to show both.
2494 cx.update_editor(|editor, window, cx| {
2495 editor.change_selections(Default::default(), window, cx, |selections| {
2496 selections.select_ranges([
2497 Point::new(1, 0)..Point::new(1, 0),
2498 Point::new(6, 0)..Point::new(6, 0),
2499 ]);
2500 })
2501 });
2502 cx.update_editor(|editor, window, cx| {
2503 assert_eq!(
2504 editor.snapshot(window, cx).scroll_position(),
2505 gpui::Point::new(0., 1.0)
2506 );
2507 });
2508}
2509
2510#[gpui::test]
2511async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2512 init_test(cx, |_| {});
2513 let mut cx = EditorTestContext::new(cx).await;
2514
2515 let line_height = cx.editor(|editor, window, _cx| {
2516 editor
2517 .style()
2518 .unwrap()
2519 .text
2520 .line_height_in_pixels(window.rem_size())
2521 });
2522 let window = cx.window;
2523 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2524 cx.set_state(
2525 &r#"
2526 ˇone
2527 two
2528 threeˇ
2529 four
2530 five
2531 six
2532 seven
2533 eight
2534 nine
2535 ten
2536 "#
2537 .unindent(),
2538 );
2539
2540 cx.update_editor(|editor, window, cx| {
2541 editor.move_page_down(&MovePageDown::default(), window, cx)
2542 });
2543 cx.assert_editor_state(
2544 &r#"
2545 one
2546 two
2547 three
2548 ˇfour
2549 five
2550 sixˇ
2551 seven
2552 eight
2553 nine
2554 ten
2555 "#
2556 .unindent(),
2557 );
2558
2559 cx.update_editor(|editor, window, cx| {
2560 editor.move_page_down(&MovePageDown::default(), window, cx)
2561 });
2562 cx.assert_editor_state(
2563 &r#"
2564 one
2565 two
2566 three
2567 four
2568 five
2569 six
2570 ˇseven
2571 eight
2572 nineˇ
2573 ten
2574 "#
2575 .unindent(),
2576 );
2577
2578 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2579 cx.assert_editor_state(
2580 &r#"
2581 one
2582 two
2583 three
2584 ˇfour
2585 five
2586 sixˇ
2587 seven
2588 eight
2589 nine
2590 ten
2591 "#
2592 .unindent(),
2593 );
2594
2595 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2596 cx.assert_editor_state(
2597 &r#"
2598 ˇone
2599 two
2600 threeˇ
2601 four
2602 five
2603 six
2604 seven
2605 eight
2606 nine
2607 ten
2608 "#
2609 .unindent(),
2610 );
2611
2612 // Test select collapsing
2613 cx.update_editor(|editor, window, cx| {
2614 editor.move_page_down(&MovePageDown::default(), window, cx);
2615 editor.move_page_down(&MovePageDown::default(), window, cx);
2616 editor.move_page_down(&MovePageDown::default(), window, cx);
2617 });
2618 cx.assert_editor_state(
2619 &r#"
2620 one
2621 two
2622 three
2623 four
2624 five
2625 six
2626 seven
2627 eight
2628 nine
2629 ˇten
2630 ˇ"#
2631 .unindent(),
2632 );
2633}
2634
2635#[gpui::test]
2636async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2637 init_test(cx, |_| {});
2638 let mut cx = EditorTestContext::new(cx).await;
2639 cx.set_state("one «two threeˇ» four");
2640 cx.update_editor(|editor, window, cx| {
2641 editor.delete_to_beginning_of_line(
2642 &DeleteToBeginningOfLine {
2643 stop_at_indent: false,
2644 },
2645 window,
2646 cx,
2647 );
2648 assert_eq!(editor.text(cx), " four");
2649 });
2650}
2651
2652#[gpui::test]
2653async fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2654 init_test(cx, |_| {});
2655
2656 let mut cx = EditorTestContext::new(cx).await;
2657
2658 // For an empty selection, the preceding word fragment is deleted.
2659 // For non-empty selections, only selected characters are deleted.
2660 cx.set_state("onˇe two t«hreˇ»e four");
2661 cx.update_editor(|editor, window, cx| {
2662 editor.delete_to_previous_word_start(
2663 &DeleteToPreviousWordStart {
2664 ignore_newlines: false,
2665 ignore_brackets: false,
2666 },
2667 window,
2668 cx,
2669 );
2670 });
2671 cx.assert_editor_state("ˇe two tˇe four");
2672
2673 cx.set_state("e tˇwo te «fˇ»our");
2674 cx.update_editor(|editor, window, cx| {
2675 editor.delete_to_next_word_end(
2676 &DeleteToNextWordEnd {
2677 ignore_newlines: false,
2678 ignore_brackets: false,
2679 },
2680 window,
2681 cx,
2682 );
2683 });
2684 cx.assert_editor_state("e tˇ te ˇour");
2685}
2686
2687#[gpui::test]
2688async fn test_delete_whitespaces(cx: &mut TestAppContext) {
2689 init_test(cx, |_| {});
2690
2691 let mut cx = EditorTestContext::new(cx).await;
2692
2693 cx.set_state("here is some text ˇwith a space");
2694 cx.update_editor(|editor, window, cx| {
2695 editor.delete_to_previous_word_start(
2696 &DeleteToPreviousWordStart {
2697 ignore_newlines: false,
2698 ignore_brackets: true,
2699 },
2700 window,
2701 cx,
2702 );
2703 });
2704 // Continuous whitespace sequences are removed entirely, words behind them are not affected by the deletion action.
2705 cx.assert_editor_state("here is some textˇwith a space");
2706
2707 cx.set_state("here is some text ˇwith a space");
2708 cx.update_editor(|editor, window, cx| {
2709 editor.delete_to_previous_word_start(
2710 &DeleteToPreviousWordStart {
2711 ignore_newlines: false,
2712 ignore_brackets: false,
2713 },
2714 window,
2715 cx,
2716 );
2717 });
2718 cx.assert_editor_state("here is some textˇwith a space");
2719
2720 cx.set_state("here is some textˇ with a space");
2721 cx.update_editor(|editor, window, cx| {
2722 editor.delete_to_next_word_end(
2723 &DeleteToNextWordEnd {
2724 ignore_newlines: false,
2725 ignore_brackets: true,
2726 },
2727 window,
2728 cx,
2729 );
2730 });
2731 // Same happens in the other direction.
2732 cx.assert_editor_state("here is some textˇwith a space");
2733
2734 cx.set_state("here is some textˇ with a space");
2735 cx.update_editor(|editor, window, cx| {
2736 editor.delete_to_next_word_end(
2737 &DeleteToNextWordEnd {
2738 ignore_newlines: false,
2739 ignore_brackets: false,
2740 },
2741 window,
2742 cx,
2743 );
2744 });
2745 cx.assert_editor_state("here is some textˇwith a space");
2746
2747 cx.set_state("here is some textˇ with a space");
2748 cx.update_editor(|editor, window, cx| {
2749 editor.delete_to_next_word_end(
2750 &DeleteToNextWordEnd {
2751 ignore_newlines: true,
2752 ignore_brackets: false,
2753 },
2754 window,
2755 cx,
2756 );
2757 });
2758 cx.assert_editor_state("here is some textˇwith a space");
2759 cx.update_editor(|editor, window, cx| {
2760 editor.delete_to_previous_word_start(
2761 &DeleteToPreviousWordStart {
2762 ignore_newlines: true,
2763 ignore_brackets: false,
2764 },
2765 window,
2766 cx,
2767 );
2768 });
2769 cx.assert_editor_state("here is some ˇwith a space");
2770 cx.update_editor(|editor, window, cx| {
2771 editor.delete_to_previous_word_start(
2772 &DeleteToPreviousWordStart {
2773 ignore_newlines: true,
2774 ignore_brackets: false,
2775 },
2776 window,
2777 cx,
2778 );
2779 });
2780 // Single whitespaces are removed with the word behind them.
2781 cx.assert_editor_state("here is ˇwith a space");
2782 cx.update_editor(|editor, window, cx| {
2783 editor.delete_to_previous_word_start(
2784 &DeleteToPreviousWordStart {
2785 ignore_newlines: true,
2786 ignore_brackets: false,
2787 },
2788 window,
2789 cx,
2790 );
2791 });
2792 cx.assert_editor_state("here ˇwith a space");
2793 cx.update_editor(|editor, window, cx| {
2794 editor.delete_to_previous_word_start(
2795 &DeleteToPreviousWordStart {
2796 ignore_newlines: true,
2797 ignore_brackets: false,
2798 },
2799 window,
2800 cx,
2801 );
2802 });
2803 cx.assert_editor_state("ˇwith a space");
2804 cx.update_editor(|editor, window, cx| {
2805 editor.delete_to_previous_word_start(
2806 &DeleteToPreviousWordStart {
2807 ignore_newlines: true,
2808 ignore_brackets: false,
2809 },
2810 window,
2811 cx,
2812 );
2813 });
2814 cx.assert_editor_state("ˇwith a space");
2815 cx.update_editor(|editor, window, cx| {
2816 editor.delete_to_next_word_end(
2817 &DeleteToNextWordEnd {
2818 ignore_newlines: true,
2819 ignore_brackets: false,
2820 },
2821 window,
2822 cx,
2823 );
2824 });
2825 // Same happens in the other direction.
2826 cx.assert_editor_state("ˇ a space");
2827 cx.update_editor(|editor, window, cx| {
2828 editor.delete_to_next_word_end(
2829 &DeleteToNextWordEnd {
2830 ignore_newlines: true,
2831 ignore_brackets: false,
2832 },
2833 window,
2834 cx,
2835 );
2836 });
2837 cx.assert_editor_state("ˇ space");
2838 cx.update_editor(|editor, window, cx| {
2839 editor.delete_to_next_word_end(
2840 &DeleteToNextWordEnd {
2841 ignore_newlines: true,
2842 ignore_brackets: false,
2843 },
2844 window,
2845 cx,
2846 );
2847 });
2848 cx.assert_editor_state("ˇ");
2849 cx.update_editor(|editor, window, cx| {
2850 editor.delete_to_next_word_end(
2851 &DeleteToNextWordEnd {
2852 ignore_newlines: true,
2853 ignore_brackets: false,
2854 },
2855 window,
2856 cx,
2857 );
2858 });
2859 cx.assert_editor_state("ˇ");
2860 cx.update_editor(|editor, window, cx| {
2861 editor.delete_to_previous_word_start(
2862 &DeleteToPreviousWordStart {
2863 ignore_newlines: true,
2864 ignore_brackets: false,
2865 },
2866 window,
2867 cx,
2868 );
2869 });
2870 cx.assert_editor_state("ˇ");
2871}
2872
2873#[gpui::test]
2874async fn test_delete_to_bracket(cx: &mut TestAppContext) {
2875 init_test(cx, |_| {});
2876
2877 let language = Arc::new(
2878 Language::new(
2879 LanguageConfig {
2880 brackets: BracketPairConfig {
2881 pairs: vec![
2882 BracketPair {
2883 start: "\"".to_string(),
2884 end: "\"".to_string(),
2885 close: true,
2886 surround: true,
2887 newline: false,
2888 },
2889 BracketPair {
2890 start: "(".to_string(),
2891 end: ")".to_string(),
2892 close: true,
2893 surround: true,
2894 newline: true,
2895 },
2896 ],
2897 ..BracketPairConfig::default()
2898 },
2899 ..LanguageConfig::default()
2900 },
2901 Some(tree_sitter_rust::LANGUAGE.into()),
2902 )
2903 .with_brackets_query(
2904 r#"
2905 ("(" @open ")" @close)
2906 ("\"" @open "\"" @close)
2907 "#,
2908 )
2909 .unwrap(),
2910 );
2911
2912 let mut cx = EditorTestContext::new(cx).await;
2913 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2914
2915 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2916 cx.update_editor(|editor, window, cx| {
2917 editor.delete_to_previous_word_start(
2918 &DeleteToPreviousWordStart {
2919 ignore_newlines: true,
2920 ignore_brackets: false,
2921 },
2922 window,
2923 cx,
2924 );
2925 });
2926 // Deletion stops before brackets if asked to not ignore them.
2927 cx.assert_editor_state(r#"macro!("ˇCOMMENT");"#);
2928 cx.update_editor(|editor, window, cx| {
2929 editor.delete_to_previous_word_start(
2930 &DeleteToPreviousWordStart {
2931 ignore_newlines: true,
2932 ignore_brackets: false,
2933 },
2934 window,
2935 cx,
2936 );
2937 });
2938 // Deletion has to remove a single bracket and then stop again.
2939 cx.assert_editor_state(r#"macro!(ˇCOMMENT");"#);
2940
2941 cx.update_editor(|editor, window, cx| {
2942 editor.delete_to_previous_word_start(
2943 &DeleteToPreviousWordStart {
2944 ignore_newlines: true,
2945 ignore_brackets: false,
2946 },
2947 window,
2948 cx,
2949 );
2950 });
2951 cx.assert_editor_state(r#"macro!ˇCOMMENT");"#);
2952
2953 cx.update_editor(|editor, window, cx| {
2954 editor.delete_to_previous_word_start(
2955 &DeleteToPreviousWordStart {
2956 ignore_newlines: true,
2957 ignore_brackets: false,
2958 },
2959 window,
2960 cx,
2961 );
2962 });
2963 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2964
2965 cx.update_editor(|editor, window, cx| {
2966 editor.delete_to_previous_word_start(
2967 &DeleteToPreviousWordStart {
2968 ignore_newlines: true,
2969 ignore_brackets: false,
2970 },
2971 window,
2972 cx,
2973 );
2974 });
2975 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2976
2977 cx.update_editor(|editor, window, cx| {
2978 editor.delete_to_next_word_end(
2979 &DeleteToNextWordEnd {
2980 ignore_newlines: true,
2981 ignore_brackets: false,
2982 },
2983 window,
2984 cx,
2985 );
2986 });
2987 // Brackets on the right are not paired anymore, hence deletion does not stop at them
2988 cx.assert_editor_state(r#"ˇ");"#);
2989
2990 cx.update_editor(|editor, window, cx| {
2991 editor.delete_to_next_word_end(
2992 &DeleteToNextWordEnd {
2993 ignore_newlines: true,
2994 ignore_brackets: false,
2995 },
2996 window,
2997 cx,
2998 );
2999 });
3000 cx.assert_editor_state(r#"ˇ"#);
3001
3002 cx.update_editor(|editor, window, cx| {
3003 editor.delete_to_next_word_end(
3004 &DeleteToNextWordEnd {
3005 ignore_newlines: true,
3006 ignore_brackets: false,
3007 },
3008 window,
3009 cx,
3010 );
3011 });
3012 cx.assert_editor_state(r#"ˇ"#);
3013
3014 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
3015 cx.update_editor(|editor, window, cx| {
3016 editor.delete_to_previous_word_start(
3017 &DeleteToPreviousWordStart {
3018 ignore_newlines: true,
3019 ignore_brackets: true,
3020 },
3021 window,
3022 cx,
3023 );
3024 });
3025 cx.assert_editor_state(r#"macroˇCOMMENT");"#);
3026}
3027
3028#[gpui::test]
3029fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
3030 init_test(cx, |_| {});
3031
3032 let editor = cx.add_window(|window, cx| {
3033 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
3034 build_editor(buffer, window, cx)
3035 });
3036 let del_to_prev_word_start = DeleteToPreviousWordStart {
3037 ignore_newlines: false,
3038 ignore_brackets: false,
3039 };
3040 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
3041 ignore_newlines: true,
3042 ignore_brackets: false,
3043 };
3044
3045 _ = editor.update(cx, |editor, window, cx| {
3046 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3047 s.select_display_ranges([
3048 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
3049 ])
3050 });
3051 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3052 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
3053 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3054 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
3055 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3056 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
3057 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3058 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
3059 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
3060 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
3061 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
3062 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3063 });
3064}
3065
3066#[gpui::test]
3067fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
3068 init_test(cx, |_| {});
3069
3070 let editor = cx.add_window(|window, cx| {
3071 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
3072 build_editor(buffer, window, cx)
3073 });
3074 let del_to_next_word_end = DeleteToNextWordEnd {
3075 ignore_newlines: false,
3076 ignore_brackets: false,
3077 };
3078 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
3079 ignore_newlines: true,
3080 ignore_brackets: false,
3081 };
3082
3083 _ = editor.update(cx, |editor, window, cx| {
3084 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3085 s.select_display_ranges([
3086 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
3087 ])
3088 });
3089 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3090 assert_eq!(
3091 editor.buffer.read(cx).read(cx).text(),
3092 "one\n two\nthree\n four"
3093 );
3094 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3095 assert_eq!(
3096 editor.buffer.read(cx).read(cx).text(),
3097 "\n two\nthree\n four"
3098 );
3099 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3100 assert_eq!(
3101 editor.buffer.read(cx).read(cx).text(),
3102 "two\nthree\n four"
3103 );
3104 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3105 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
3106 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3107 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
3108 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3109 assert_eq!(editor.buffer.read(cx).read(cx).text(), "four");
3110 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3111 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3112 });
3113}
3114
3115#[gpui::test]
3116fn test_newline(cx: &mut TestAppContext) {
3117 init_test(cx, |_| {});
3118
3119 let editor = cx.add_window(|window, cx| {
3120 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
3121 build_editor(buffer, window, cx)
3122 });
3123
3124 _ = editor.update(cx, |editor, window, cx| {
3125 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3126 s.select_display_ranges([
3127 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
3128 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
3129 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
3130 ])
3131 });
3132
3133 editor.newline(&Newline, window, cx);
3134 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
3135 });
3136}
3137
3138#[gpui::test]
3139fn test_newline_with_old_selections(cx: &mut TestAppContext) {
3140 init_test(cx, |_| {});
3141
3142 let editor = cx.add_window(|window, cx| {
3143 let buffer = MultiBuffer::build_simple(
3144 "
3145 a
3146 b(
3147 X
3148 )
3149 c(
3150 X
3151 )
3152 "
3153 .unindent()
3154 .as_str(),
3155 cx,
3156 );
3157 let mut editor = build_editor(buffer, window, cx);
3158 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3159 s.select_ranges([
3160 Point::new(2, 4)..Point::new(2, 5),
3161 Point::new(5, 4)..Point::new(5, 5),
3162 ])
3163 });
3164 editor
3165 });
3166
3167 _ = editor.update(cx, |editor, window, cx| {
3168 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3169 editor.buffer.update(cx, |buffer, cx| {
3170 buffer.edit(
3171 [
3172 (Point::new(1, 2)..Point::new(3, 0), ""),
3173 (Point::new(4, 2)..Point::new(6, 0), ""),
3174 ],
3175 None,
3176 cx,
3177 );
3178 assert_eq!(
3179 buffer.read(cx).text(),
3180 "
3181 a
3182 b()
3183 c()
3184 "
3185 .unindent()
3186 );
3187 });
3188 assert_eq!(
3189 editor.selections.ranges(&editor.display_snapshot(cx)),
3190 &[
3191 Point::new(1, 2)..Point::new(1, 2),
3192 Point::new(2, 2)..Point::new(2, 2),
3193 ],
3194 );
3195
3196 editor.newline(&Newline, window, cx);
3197 assert_eq!(
3198 editor.text(cx),
3199 "
3200 a
3201 b(
3202 )
3203 c(
3204 )
3205 "
3206 .unindent()
3207 );
3208
3209 // The selections are moved after the inserted newlines
3210 assert_eq!(
3211 editor.selections.ranges(&editor.display_snapshot(cx)),
3212 &[
3213 Point::new(2, 0)..Point::new(2, 0),
3214 Point::new(4, 0)..Point::new(4, 0),
3215 ],
3216 );
3217 });
3218}
3219
3220#[gpui::test]
3221async fn test_newline_above(cx: &mut TestAppContext) {
3222 init_test(cx, |settings| {
3223 settings.defaults.tab_size = NonZeroU32::new(4)
3224 });
3225
3226 let language = Arc::new(
3227 Language::new(
3228 LanguageConfig::default(),
3229 Some(tree_sitter_rust::LANGUAGE.into()),
3230 )
3231 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3232 .unwrap(),
3233 );
3234
3235 let mut cx = EditorTestContext::new(cx).await;
3236 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3237 cx.set_state(indoc! {"
3238 const a: ˇA = (
3239 (ˇ
3240 «const_functionˇ»(ˇ),
3241 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3242 )ˇ
3243 ˇ);ˇ
3244 "});
3245
3246 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
3247 cx.assert_editor_state(indoc! {"
3248 ˇ
3249 const a: A = (
3250 ˇ
3251 (
3252 ˇ
3253 ˇ
3254 const_function(),
3255 ˇ
3256 ˇ
3257 ˇ
3258 ˇ
3259 something_else,
3260 ˇ
3261 )
3262 ˇ
3263 ˇ
3264 );
3265 "});
3266}
3267
3268#[gpui::test]
3269async fn test_newline_below(cx: &mut TestAppContext) {
3270 init_test(cx, |settings| {
3271 settings.defaults.tab_size = NonZeroU32::new(4)
3272 });
3273
3274 let language = Arc::new(
3275 Language::new(
3276 LanguageConfig::default(),
3277 Some(tree_sitter_rust::LANGUAGE.into()),
3278 )
3279 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3280 .unwrap(),
3281 );
3282
3283 let mut cx = EditorTestContext::new(cx).await;
3284 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3285 cx.set_state(indoc! {"
3286 const a: ˇA = (
3287 (ˇ
3288 «const_functionˇ»(ˇ),
3289 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3290 )ˇ
3291 ˇ);ˇ
3292 "});
3293
3294 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
3295 cx.assert_editor_state(indoc! {"
3296 const a: A = (
3297 ˇ
3298 (
3299 ˇ
3300 const_function(),
3301 ˇ
3302 ˇ
3303 something_else,
3304 ˇ
3305 ˇ
3306 ˇ
3307 ˇ
3308 )
3309 ˇ
3310 );
3311 ˇ
3312 ˇ
3313 "});
3314}
3315
3316#[gpui::test]
3317async fn test_newline_comments(cx: &mut TestAppContext) {
3318 init_test(cx, |settings| {
3319 settings.defaults.tab_size = NonZeroU32::new(4)
3320 });
3321
3322 let language = Arc::new(Language::new(
3323 LanguageConfig {
3324 line_comments: vec!["// ".into()],
3325 ..LanguageConfig::default()
3326 },
3327 None,
3328 ));
3329 {
3330 let mut cx = EditorTestContext::new(cx).await;
3331 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3332 cx.set_state(indoc! {"
3333 // Fooˇ
3334 "});
3335
3336 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3337 cx.assert_editor_state(indoc! {"
3338 // Foo
3339 // ˇ
3340 "});
3341 // Ensure that we add comment prefix when existing line contains space
3342 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3343 cx.assert_editor_state(
3344 indoc! {"
3345 // Foo
3346 //s
3347 // ˇ
3348 "}
3349 .replace("s", " ") // s is used as space placeholder to prevent format on save
3350 .as_str(),
3351 );
3352 // Ensure that we add comment prefix when existing line does not contain space
3353 cx.set_state(indoc! {"
3354 // Foo
3355 //ˇ
3356 "});
3357 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3358 cx.assert_editor_state(indoc! {"
3359 // Foo
3360 //
3361 // ˇ
3362 "});
3363 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
3364 cx.set_state(indoc! {"
3365 ˇ// Foo
3366 "});
3367 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3368 cx.assert_editor_state(indoc! {"
3369
3370 ˇ// Foo
3371 "});
3372 }
3373 // Ensure that comment continuations can be disabled.
3374 update_test_language_settings(cx, |settings| {
3375 settings.defaults.extend_comment_on_newline = Some(false);
3376 });
3377 let mut cx = EditorTestContext::new(cx).await;
3378 cx.set_state(indoc! {"
3379 // Fooˇ
3380 "});
3381 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3382 cx.assert_editor_state(indoc! {"
3383 // Foo
3384 ˇ
3385 "});
3386}
3387
3388#[gpui::test]
3389async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
3390 init_test(cx, |settings| {
3391 settings.defaults.tab_size = NonZeroU32::new(4)
3392 });
3393
3394 let language = Arc::new(Language::new(
3395 LanguageConfig {
3396 line_comments: vec!["// ".into(), "/// ".into()],
3397 ..LanguageConfig::default()
3398 },
3399 None,
3400 ));
3401 {
3402 let mut cx = EditorTestContext::new(cx).await;
3403 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3404 cx.set_state(indoc! {"
3405 //ˇ
3406 "});
3407 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3408 cx.assert_editor_state(indoc! {"
3409 //
3410 // ˇ
3411 "});
3412
3413 cx.set_state(indoc! {"
3414 ///ˇ
3415 "});
3416 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3417 cx.assert_editor_state(indoc! {"
3418 ///
3419 /// ˇ
3420 "});
3421 }
3422}
3423
3424#[gpui::test]
3425async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
3426 init_test(cx, |settings| {
3427 settings.defaults.tab_size = NonZeroU32::new(4)
3428 });
3429
3430 let language = Arc::new(
3431 Language::new(
3432 LanguageConfig {
3433 documentation_comment: Some(language::BlockCommentConfig {
3434 start: "/**".into(),
3435 end: "*/".into(),
3436 prefix: "* ".into(),
3437 tab_size: 1,
3438 }),
3439
3440 ..LanguageConfig::default()
3441 },
3442 Some(tree_sitter_rust::LANGUAGE.into()),
3443 )
3444 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
3445 .unwrap(),
3446 );
3447
3448 {
3449 let mut cx = EditorTestContext::new(cx).await;
3450 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3451 cx.set_state(indoc! {"
3452 /**ˇ
3453 "});
3454
3455 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3456 cx.assert_editor_state(indoc! {"
3457 /**
3458 * ˇ
3459 "});
3460 // Ensure that if cursor is before the comment start,
3461 // we do not actually insert a comment prefix.
3462 cx.set_state(indoc! {"
3463 ˇ/**
3464 "});
3465 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3466 cx.assert_editor_state(indoc! {"
3467
3468 ˇ/**
3469 "});
3470 // Ensure that if cursor is between it doesn't add comment prefix.
3471 cx.set_state(indoc! {"
3472 /*ˇ*
3473 "});
3474 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3475 cx.assert_editor_state(indoc! {"
3476 /*
3477 ˇ*
3478 "});
3479 // Ensure that if suffix exists on same line after cursor it adds new line.
3480 cx.set_state(indoc! {"
3481 /**ˇ*/
3482 "});
3483 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3484 cx.assert_editor_state(indoc! {"
3485 /**
3486 * ˇ
3487 */
3488 "});
3489 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3490 cx.set_state(indoc! {"
3491 /**ˇ */
3492 "});
3493 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3494 cx.assert_editor_state(indoc! {"
3495 /**
3496 * ˇ
3497 */
3498 "});
3499 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3500 cx.set_state(indoc! {"
3501 /** ˇ*/
3502 "});
3503 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3504 cx.assert_editor_state(
3505 indoc! {"
3506 /**s
3507 * ˇ
3508 */
3509 "}
3510 .replace("s", " ") // s is used as space placeholder to prevent format on save
3511 .as_str(),
3512 );
3513 // Ensure that delimiter space is preserved when newline on already
3514 // spaced delimiter.
3515 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3516 cx.assert_editor_state(
3517 indoc! {"
3518 /**s
3519 *s
3520 * ˇ
3521 */
3522 "}
3523 .replace("s", " ") // s is used as space placeholder to prevent format on save
3524 .as_str(),
3525 );
3526 // Ensure that delimiter space is preserved when space is not
3527 // on existing delimiter.
3528 cx.set_state(indoc! {"
3529 /**
3530 *ˇ
3531 */
3532 "});
3533 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3534 cx.assert_editor_state(indoc! {"
3535 /**
3536 *
3537 * ˇ
3538 */
3539 "});
3540 // Ensure that if suffix exists on same line after cursor it
3541 // doesn't add extra new line if prefix is not on same line.
3542 cx.set_state(indoc! {"
3543 /**
3544 ˇ*/
3545 "});
3546 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3547 cx.assert_editor_state(indoc! {"
3548 /**
3549
3550 ˇ*/
3551 "});
3552 // Ensure that it detects suffix after existing prefix.
3553 cx.set_state(indoc! {"
3554 /**ˇ/
3555 "});
3556 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3557 cx.assert_editor_state(indoc! {"
3558 /**
3559 ˇ/
3560 "});
3561 // Ensure that if suffix exists on same line before
3562 // cursor it does not add comment prefix.
3563 cx.set_state(indoc! {"
3564 /** */ˇ
3565 "});
3566 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3567 cx.assert_editor_state(indoc! {"
3568 /** */
3569 ˇ
3570 "});
3571 // Ensure that if suffix exists on same line before
3572 // cursor it does not add comment prefix.
3573 cx.set_state(indoc! {"
3574 /**
3575 *
3576 */ˇ
3577 "});
3578 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3579 cx.assert_editor_state(indoc! {"
3580 /**
3581 *
3582 */
3583 ˇ
3584 "});
3585
3586 // Ensure that inline comment followed by code
3587 // doesn't add comment prefix on newline
3588 cx.set_state(indoc! {"
3589 /** */ textˇ
3590 "});
3591 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3592 cx.assert_editor_state(indoc! {"
3593 /** */ text
3594 ˇ
3595 "});
3596
3597 // Ensure that text after comment end tag
3598 // doesn't add comment prefix on newline
3599 cx.set_state(indoc! {"
3600 /**
3601 *
3602 */ˇtext
3603 "});
3604 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3605 cx.assert_editor_state(indoc! {"
3606 /**
3607 *
3608 */
3609 ˇtext
3610 "});
3611
3612 // Ensure if not comment block it doesn't
3613 // add comment prefix on newline
3614 cx.set_state(indoc! {"
3615 * textˇ
3616 "});
3617 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3618 cx.assert_editor_state(indoc! {"
3619 * text
3620 ˇ
3621 "});
3622 }
3623 // Ensure that comment continuations can be disabled.
3624 update_test_language_settings(cx, |settings| {
3625 settings.defaults.extend_comment_on_newline = Some(false);
3626 });
3627 let mut cx = EditorTestContext::new(cx).await;
3628 cx.set_state(indoc! {"
3629 /**ˇ
3630 "});
3631 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3632 cx.assert_editor_state(indoc! {"
3633 /**
3634 ˇ
3635 "});
3636}
3637
3638#[gpui::test]
3639async fn test_newline_comments_with_block_comment(cx: &mut TestAppContext) {
3640 init_test(cx, |settings| {
3641 settings.defaults.tab_size = NonZeroU32::new(4)
3642 });
3643
3644 let lua_language = Arc::new(Language::new(
3645 LanguageConfig {
3646 line_comments: vec!["--".into()],
3647 block_comment: Some(language::BlockCommentConfig {
3648 start: "--[[".into(),
3649 prefix: "".into(),
3650 end: "]]".into(),
3651 tab_size: 0,
3652 }),
3653 ..LanguageConfig::default()
3654 },
3655 None,
3656 ));
3657
3658 let mut cx = EditorTestContext::new(cx).await;
3659 cx.update_buffer(|buffer, cx| buffer.set_language(Some(lua_language), cx));
3660
3661 // Line with line comment should extend
3662 cx.set_state(indoc! {"
3663 --ˇ
3664 "});
3665 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3666 cx.assert_editor_state(indoc! {"
3667 --
3668 --ˇ
3669 "});
3670
3671 // Line with block comment that matches line comment should not extend
3672 cx.set_state(indoc! {"
3673 --[[ˇ
3674 "});
3675 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3676 cx.assert_editor_state(indoc! {"
3677 --[[
3678 ˇ
3679 "});
3680}
3681
3682#[gpui::test]
3683fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3684 init_test(cx, |_| {});
3685
3686 let editor = cx.add_window(|window, cx| {
3687 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3688 let mut editor = build_editor(buffer, window, cx);
3689 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3690 s.select_ranges([3..4, 11..12, 19..20])
3691 });
3692 editor
3693 });
3694
3695 _ = editor.update(cx, |editor, window, cx| {
3696 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3697 editor.buffer.update(cx, |buffer, cx| {
3698 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3699 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3700 });
3701 assert_eq!(
3702 editor.selections.ranges(&editor.display_snapshot(cx)),
3703 &[2..2, 7..7, 12..12],
3704 );
3705
3706 editor.insert("Z", window, cx);
3707 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3708
3709 // The selections are moved after the inserted characters
3710 assert_eq!(
3711 editor.selections.ranges(&editor.display_snapshot(cx)),
3712 &[3..3, 9..9, 15..15],
3713 );
3714 });
3715}
3716
3717#[gpui::test]
3718async fn test_tab(cx: &mut TestAppContext) {
3719 init_test(cx, |settings| {
3720 settings.defaults.tab_size = NonZeroU32::new(3)
3721 });
3722
3723 let mut cx = EditorTestContext::new(cx).await;
3724 cx.set_state(indoc! {"
3725 ˇabˇc
3726 ˇ🏀ˇ🏀ˇefg
3727 dˇ
3728 "});
3729 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3730 cx.assert_editor_state(indoc! {"
3731 ˇab ˇc
3732 ˇ🏀 ˇ🏀 ˇefg
3733 d ˇ
3734 "});
3735
3736 cx.set_state(indoc! {"
3737 a
3738 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3739 "});
3740 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3741 cx.assert_editor_state(indoc! {"
3742 a
3743 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3744 "});
3745}
3746
3747#[gpui::test]
3748async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3749 init_test(cx, |_| {});
3750
3751 let mut cx = EditorTestContext::new(cx).await;
3752 let language = Arc::new(
3753 Language::new(
3754 LanguageConfig::default(),
3755 Some(tree_sitter_rust::LANGUAGE.into()),
3756 )
3757 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3758 .unwrap(),
3759 );
3760 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3761
3762 // test when all cursors are not at suggested indent
3763 // then simply move to their suggested indent location
3764 cx.set_state(indoc! {"
3765 const a: B = (
3766 c(
3767 ˇ
3768 ˇ )
3769 );
3770 "});
3771 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3772 cx.assert_editor_state(indoc! {"
3773 const a: B = (
3774 c(
3775 ˇ
3776 ˇ)
3777 );
3778 "});
3779
3780 // test cursor already at suggested indent not moving when
3781 // other cursors are yet to reach their suggested indents
3782 cx.set_state(indoc! {"
3783 ˇ
3784 const a: B = (
3785 c(
3786 d(
3787 ˇ
3788 )
3789 ˇ
3790 ˇ )
3791 );
3792 "});
3793 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3794 cx.assert_editor_state(indoc! {"
3795 ˇ
3796 const a: B = (
3797 c(
3798 d(
3799 ˇ
3800 )
3801 ˇ
3802 ˇ)
3803 );
3804 "});
3805 // test when all cursors are at suggested indent then tab is inserted
3806 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3807 cx.assert_editor_state(indoc! {"
3808 ˇ
3809 const a: B = (
3810 c(
3811 d(
3812 ˇ
3813 )
3814 ˇ
3815 ˇ)
3816 );
3817 "});
3818
3819 // test when current indent is less than suggested indent,
3820 // we adjust line to match suggested indent and move cursor to it
3821 //
3822 // when no other cursor is at word boundary, all of them should move
3823 cx.set_state(indoc! {"
3824 const a: B = (
3825 c(
3826 d(
3827 ˇ
3828 ˇ )
3829 ˇ )
3830 );
3831 "});
3832 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3833 cx.assert_editor_state(indoc! {"
3834 const a: B = (
3835 c(
3836 d(
3837 ˇ
3838 ˇ)
3839 ˇ)
3840 );
3841 "});
3842
3843 // test when current indent is less than suggested indent,
3844 // we adjust line to match suggested indent and move cursor to it
3845 //
3846 // when some other cursor is at word boundary, it should not move
3847 cx.set_state(indoc! {"
3848 const a: B = (
3849 c(
3850 d(
3851 ˇ
3852 ˇ )
3853 ˇ)
3854 );
3855 "});
3856 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3857 cx.assert_editor_state(indoc! {"
3858 const a: B = (
3859 c(
3860 d(
3861 ˇ
3862 ˇ)
3863 ˇ)
3864 );
3865 "});
3866
3867 // test when current indent is more than suggested indent,
3868 // we just move cursor to current indent instead of suggested indent
3869 //
3870 // when no other cursor is at word boundary, all of them should move
3871 cx.set_state(indoc! {"
3872 const a: B = (
3873 c(
3874 d(
3875 ˇ
3876 ˇ )
3877 ˇ )
3878 );
3879 "});
3880 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3881 cx.assert_editor_state(indoc! {"
3882 const a: B = (
3883 c(
3884 d(
3885 ˇ
3886 ˇ)
3887 ˇ)
3888 );
3889 "});
3890 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3891 cx.assert_editor_state(indoc! {"
3892 const a: B = (
3893 c(
3894 d(
3895 ˇ
3896 ˇ)
3897 ˇ)
3898 );
3899 "});
3900
3901 // test when current indent is more than suggested indent,
3902 // we just move cursor to current indent instead of suggested indent
3903 //
3904 // when some other cursor is at word boundary, it doesn't move
3905 cx.set_state(indoc! {"
3906 const a: B = (
3907 c(
3908 d(
3909 ˇ
3910 ˇ )
3911 ˇ)
3912 );
3913 "});
3914 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3915 cx.assert_editor_state(indoc! {"
3916 const a: B = (
3917 c(
3918 d(
3919 ˇ
3920 ˇ)
3921 ˇ)
3922 );
3923 "});
3924
3925 // handle auto-indent when there are multiple cursors on the same line
3926 cx.set_state(indoc! {"
3927 const a: B = (
3928 c(
3929 ˇ ˇ
3930 ˇ )
3931 );
3932 "});
3933 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3934 cx.assert_editor_state(indoc! {"
3935 const a: B = (
3936 c(
3937 ˇ
3938 ˇ)
3939 );
3940 "});
3941}
3942
3943#[gpui::test]
3944async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3945 init_test(cx, |settings| {
3946 settings.defaults.tab_size = NonZeroU32::new(3)
3947 });
3948
3949 let mut cx = EditorTestContext::new(cx).await;
3950 cx.set_state(indoc! {"
3951 ˇ
3952 \t ˇ
3953 \t ˇ
3954 \t ˇ
3955 \t \t\t \t \t\t \t\t \t \t ˇ
3956 "});
3957
3958 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3959 cx.assert_editor_state(indoc! {"
3960 ˇ
3961 \t ˇ
3962 \t ˇ
3963 \t ˇ
3964 \t \t\t \t \t\t \t\t \t \t ˇ
3965 "});
3966}
3967
3968#[gpui::test]
3969async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3970 init_test(cx, |settings| {
3971 settings.defaults.tab_size = NonZeroU32::new(4)
3972 });
3973
3974 let language = Arc::new(
3975 Language::new(
3976 LanguageConfig::default(),
3977 Some(tree_sitter_rust::LANGUAGE.into()),
3978 )
3979 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3980 .unwrap(),
3981 );
3982
3983 let mut cx = EditorTestContext::new(cx).await;
3984 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3985 cx.set_state(indoc! {"
3986 fn a() {
3987 if b {
3988 \t ˇc
3989 }
3990 }
3991 "});
3992
3993 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3994 cx.assert_editor_state(indoc! {"
3995 fn a() {
3996 if b {
3997 ˇc
3998 }
3999 }
4000 "});
4001}
4002
4003#[gpui::test]
4004async fn test_indent_outdent(cx: &mut TestAppContext) {
4005 init_test(cx, |settings| {
4006 settings.defaults.tab_size = NonZeroU32::new(4);
4007 });
4008
4009 let mut cx = EditorTestContext::new(cx).await;
4010
4011 cx.set_state(indoc! {"
4012 «oneˇ» «twoˇ»
4013 three
4014 four
4015 "});
4016 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4017 cx.assert_editor_state(indoc! {"
4018 «oneˇ» «twoˇ»
4019 three
4020 four
4021 "});
4022
4023 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4024 cx.assert_editor_state(indoc! {"
4025 «oneˇ» «twoˇ»
4026 three
4027 four
4028 "});
4029
4030 // select across line ending
4031 cx.set_state(indoc! {"
4032 one two
4033 t«hree
4034 ˇ» four
4035 "});
4036 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4037 cx.assert_editor_state(indoc! {"
4038 one two
4039 t«hree
4040 ˇ» four
4041 "});
4042
4043 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4044 cx.assert_editor_state(indoc! {"
4045 one two
4046 t«hree
4047 ˇ» four
4048 "});
4049
4050 // Ensure that indenting/outdenting works when the cursor is at column 0.
4051 cx.set_state(indoc! {"
4052 one two
4053 ˇthree
4054 four
4055 "});
4056 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4057 cx.assert_editor_state(indoc! {"
4058 one two
4059 ˇthree
4060 four
4061 "});
4062
4063 cx.set_state(indoc! {"
4064 one two
4065 ˇ three
4066 four
4067 "});
4068 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4069 cx.assert_editor_state(indoc! {"
4070 one two
4071 ˇthree
4072 four
4073 "});
4074}
4075
4076#[gpui::test]
4077async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) {
4078 // This is a regression test for issue #33761
4079 init_test(cx, |_| {});
4080
4081 let mut cx = EditorTestContext::new(cx).await;
4082 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
4083 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
4084
4085 cx.set_state(
4086 r#"ˇ# ingress:
4087ˇ# api:
4088ˇ# enabled: false
4089ˇ# pathType: Prefix
4090ˇ# console:
4091ˇ# enabled: false
4092ˇ# pathType: Prefix
4093"#,
4094 );
4095
4096 // Press tab to indent all lines
4097 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4098
4099 cx.assert_editor_state(
4100 r#" ˇ# ingress:
4101 ˇ# api:
4102 ˇ# enabled: false
4103 ˇ# pathType: Prefix
4104 ˇ# console:
4105 ˇ# enabled: false
4106 ˇ# pathType: Prefix
4107"#,
4108 );
4109}
4110
4111#[gpui::test]
4112async fn test_indent_yaml_non_comments_with_multiple_cursors(cx: &mut TestAppContext) {
4113 // This is a test to make sure our fix for issue #33761 didn't break anything
4114 init_test(cx, |_| {});
4115
4116 let mut cx = EditorTestContext::new(cx).await;
4117 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
4118 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
4119
4120 cx.set_state(
4121 r#"ˇingress:
4122ˇ api:
4123ˇ enabled: false
4124ˇ pathType: Prefix
4125"#,
4126 );
4127
4128 // Press tab to indent all lines
4129 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4130
4131 cx.assert_editor_state(
4132 r#"ˇingress:
4133 ˇapi:
4134 ˇenabled: false
4135 ˇpathType: Prefix
4136"#,
4137 );
4138}
4139
4140#[gpui::test]
4141async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
4142 init_test(cx, |settings| {
4143 settings.defaults.hard_tabs = Some(true);
4144 });
4145
4146 let mut cx = EditorTestContext::new(cx).await;
4147
4148 // select two ranges on one line
4149 cx.set_state(indoc! {"
4150 «oneˇ» «twoˇ»
4151 three
4152 four
4153 "});
4154 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4155 cx.assert_editor_state(indoc! {"
4156 \t«oneˇ» «twoˇ»
4157 three
4158 four
4159 "});
4160 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4161 cx.assert_editor_state(indoc! {"
4162 \t\t«oneˇ» «twoˇ»
4163 three
4164 four
4165 "});
4166 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4167 cx.assert_editor_state(indoc! {"
4168 \t«oneˇ» «twoˇ»
4169 three
4170 four
4171 "});
4172 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4173 cx.assert_editor_state(indoc! {"
4174 «oneˇ» «twoˇ»
4175 three
4176 four
4177 "});
4178
4179 // select across a line ending
4180 cx.set_state(indoc! {"
4181 one two
4182 t«hree
4183 ˇ»four
4184 "});
4185 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4186 cx.assert_editor_state(indoc! {"
4187 one two
4188 \tt«hree
4189 ˇ»four
4190 "});
4191 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4192 cx.assert_editor_state(indoc! {"
4193 one two
4194 \t\tt«hree
4195 ˇ»four
4196 "});
4197 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4198 cx.assert_editor_state(indoc! {"
4199 one two
4200 \tt«hree
4201 ˇ»four
4202 "});
4203 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4204 cx.assert_editor_state(indoc! {"
4205 one two
4206 t«hree
4207 ˇ»four
4208 "});
4209
4210 // Ensure that indenting/outdenting works when the cursor is at column 0.
4211 cx.set_state(indoc! {"
4212 one two
4213 ˇthree
4214 four
4215 "});
4216 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4217 cx.assert_editor_state(indoc! {"
4218 one two
4219 ˇthree
4220 four
4221 "});
4222 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4223 cx.assert_editor_state(indoc! {"
4224 one two
4225 \tˇthree
4226 four
4227 "});
4228 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4229 cx.assert_editor_state(indoc! {"
4230 one two
4231 ˇthree
4232 four
4233 "});
4234}
4235
4236#[gpui::test]
4237fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
4238 init_test(cx, |settings| {
4239 settings.languages.0.extend([
4240 (
4241 "TOML".into(),
4242 LanguageSettingsContent {
4243 tab_size: NonZeroU32::new(2),
4244 ..Default::default()
4245 },
4246 ),
4247 (
4248 "Rust".into(),
4249 LanguageSettingsContent {
4250 tab_size: NonZeroU32::new(4),
4251 ..Default::default()
4252 },
4253 ),
4254 ]);
4255 });
4256
4257 let toml_language = Arc::new(Language::new(
4258 LanguageConfig {
4259 name: "TOML".into(),
4260 ..Default::default()
4261 },
4262 None,
4263 ));
4264 let rust_language = Arc::new(Language::new(
4265 LanguageConfig {
4266 name: "Rust".into(),
4267 ..Default::default()
4268 },
4269 None,
4270 ));
4271
4272 let toml_buffer =
4273 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
4274 let rust_buffer =
4275 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
4276 let multibuffer = cx.new(|cx| {
4277 let mut multibuffer = MultiBuffer::new(ReadWrite);
4278 multibuffer.push_excerpts(
4279 toml_buffer.clone(),
4280 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
4281 cx,
4282 );
4283 multibuffer.push_excerpts(
4284 rust_buffer.clone(),
4285 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
4286 cx,
4287 );
4288 multibuffer
4289 });
4290
4291 cx.add_window(|window, cx| {
4292 let mut editor = build_editor(multibuffer, window, cx);
4293
4294 assert_eq!(
4295 editor.text(cx),
4296 indoc! {"
4297 a = 1
4298 b = 2
4299
4300 const c: usize = 3;
4301 "}
4302 );
4303
4304 select_ranges(
4305 &mut editor,
4306 indoc! {"
4307 «aˇ» = 1
4308 b = 2
4309
4310 «const c:ˇ» usize = 3;
4311 "},
4312 window,
4313 cx,
4314 );
4315
4316 editor.tab(&Tab, window, cx);
4317 assert_text_with_selections(
4318 &mut editor,
4319 indoc! {"
4320 «aˇ» = 1
4321 b = 2
4322
4323 «const c:ˇ» usize = 3;
4324 "},
4325 cx,
4326 );
4327 editor.backtab(&Backtab, window, cx);
4328 assert_text_with_selections(
4329 &mut editor,
4330 indoc! {"
4331 «aˇ» = 1
4332 b = 2
4333
4334 «const c:ˇ» usize = 3;
4335 "},
4336 cx,
4337 );
4338
4339 editor
4340 });
4341}
4342
4343#[gpui::test]
4344async fn test_backspace(cx: &mut TestAppContext) {
4345 init_test(cx, |_| {});
4346
4347 let mut cx = EditorTestContext::new(cx).await;
4348
4349 // Basic backspace
4350 cx.set_state(indoc! {"
4351 onˇe two three
4352 fou«rˇ» five six
4353 seven «ˇeight nine
4354 »ten
4355 "});
4356 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4357 cx.assert_editor_state(indoc! {"
4358 oˇe two three
4359 fouˇ five six
4360 seven ˇten
4361 "});
4362
4363 // Test backspace inside and around indents
4364 cx.set_state(indoc! {"
4365 zero
4366 ˇone
4367 ˇtwo
4368 ˇ ˇ ˇ three
4369 ˇ ˇ four
4370 "});
4371 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4372 cx.assert_editor_state(indoc! {"
4373 zero
4374 ˇone
4375 ˇtwo
4376 ˇ threeˇ four
4377 "});
4378}
4379
4380#[gpui::test]
4381async fn test_delete(cx: &mut TestAppContext) {
4382 init_test(cx, |_| {});
4383
4384 let mut cx = EditorTestContext::new(cx).await;
4385 cx.set_state(indoc! {"
4386 onˇe two three
4387 fou«rˇ» five six
4388 seven «ˇeight nine
4389 »ten
4390 "});
4391 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
4392 cx.assert_editor_state(indoc! {"
4393 onˇ two three
4394 fouˇ five six
4395 seven ˇten
4396 "});
4397}
4398
4399#[gpui::test]
4400fn test_delete_line(cx: &mut TestAppContext) {
4401 init_test(cx, |_| {});
4402
4403 let editor = cx.add_window(|window, cx| {
4404 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4405 build_editor(buffer, window, cx)
4406 });
4407 _ = editor.update(cx, |editor, window, cx| {
4408 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4409 s.select_display_ranges([
4410 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4411 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4412 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4413 ])
4414 });
4415 editor.delete_line(&DeleteLine, window, cx);
4416 assert_eq!(editor.display_text(cx), "ghi");
4417 assert_eq!(
4418 editor.selections.display_ranges(cx),
4419 vec![
4420 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
4421 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4422 ]
4423 );
4424 });
4425
4426 let editor = cx.add_window(|window, cx| {
4427 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4428 build_editor(buffer, window, cx)
4429 });
4430 _ = editor.update(cx, |editor, window, cx| {
4431 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4432 s.select_display_ranges([
4433 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
4434 ])
4435 });
4436 editor.delete_line(&DeleteLine, window, cx);
4437 assert_eq!(editor.display_text(cx), "ghi\n");
4438 assert_eq!(
4439 editor.selections.display_ranges(cx),
4440 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
4441 );
4442 });
4443
4444 let editor = cx.add_window(|window, cx| {
4445 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n\njkl\nmno", cx);
4446 build_editor(buffer, window, cx)
4447 });
4448 _ = editor.update(cx, |editor, window, cx| {
4449 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4450 s.select_display_ranges([
4451 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(2), 1)
4452 ])
4453 });
4454 editor.delete_line(&DeleteLine, window, cx);
4455 assert_eq!(editor.display_text(cx), "\njkl\nmno");
4456 assert_eq!(
4457 editor.selections.display_ranges(cx),
4458 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
4459 );
4460 });
4461}
4462
4463#[gpui::test]
4464fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
4465 init_test(cx, |_| {});
4466
4467 cx.add_window(|window, cx| {
4468 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4469 let mut editor = build_editor(buffer.clone(), window, cx);
4470 let buffer = buffer.read(cx).as_singleton().unwrap();
4471
4472 assert_eq!(
4473 editor
4474 .selections
4475 .ranges::<Point>(&editor.display_snapshot(cx)),
4476 &[Point::new(0, 0)..Point::new(0, 0)]
4477 );
4478
4479 // When on single line, replace newline at end by space
4480 editor.join_lines(&JoinLines, window, cx);
4481 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4482 assert_eq!(
4483 editor
4484 .selections
4485 .ranges::<Point>(&editor.display_snapshot(cx)),
4486 &[Point::new(0, 3)..Point::new(0, 3)]
4487 );
4488
4489 // When multiple lines are selected, remove newlines that are spanned by the selection
4490 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4491 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
4492 });
4493 editor.join_lines(&JoinLines, window, cx);
4494 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
4495 assert_eq!(
4496 editor
4497 .selections
4498 .ranges::<Point>(&editor.display_snapshot(cx)),
4499 &[Point::new(0, 11)..Point::new(0, 11)]
4500 );
4501
4502 // Undo should be transactional
4503 editor.undo(&Undo, window, cx);
4504 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4505 assert_eq!(
4506 editor
4507 .selections
4508 .ranges::<Point>(&editor.display_snapshot(cx)),
4509 &[Point::new(0, 5)..Point::new(2, 2)]
4510 );
4511
4512 // When joining an empty line don't insert a space
4513 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4514 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
4515 });
4516 editor.join_lines(&JoinLines, window, cx);
4517 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
4518 assert_eq!(
4519 editor
4520 .selections
4521 .ranges::<Point>(&editor.display_snapshot(cx)),
4522 [Point::new(2, 3)..Point::new(2, 3)]
4523 );
4524
4525 // We can remove trailing newlines
4526 editor.join_lines(&JoinLines, window, cx);
4527 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4528 assert_eq!(
4529 editor
4530 .selections
4531 .ranges::<Point>(&editor.display_snapshot(cx)),
4532 [Point::new(2, 3)..Point::new(2, 3)]
4533 );
4534
4535 // We don't blow up on the last line
4536 editor.join_lines(&JoinLines, window, cx);
4537 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4538 assert_eq!(
4539 editor
4540 .selections
4541 .ranges::<Point>(&editor.display_snapshot(cx)),
4542 [Point::new(2, 3)..Point::new(2, 3)]
4543 );
4544
4545 // reset to test indentation
4546 editor.buffer.update(cx, |buffer, cx| {
4547 buffer.edit(
4548 [
4549 (Point::new(1, 0)..Point::new(1, 2), " "),
4550 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
4551 ],
4552 None,
4553 cx,
4554 )
4555 });
4556
4557 // We remove any leading spaces
4558 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
4559 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4560 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
4561 });
4562 editor.join_lines(&JoinLines, window, cx);
4563 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
4564
4565 // We don't insert a space for a line containing only spaces
4566 editor.join_lines(&JoinLines, window, cx);
4567 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
4568
4569 // We ignore any leading tabs
4570 editor.join_lines(&JoinLines, window, cx);
4571 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
4572
4573 editor
4574 });
4575}
4576
4577#[gpui::test]
4578fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
4579 init_test(cx, |_| {});
4580
4581 cx.add_window(|window, cx| {
4582 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4583 let mut editor = build_editor(buffer.clone(), window, cx);
4584 let buffer = buffer.read(cx).as_singleton().unwrap();
4585
4586 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4587 s.select_ranges([
4588 Point::new(0, 2)..Point::new(1, 1),
4589 Point::new(1, 2)..Point::new(1, 2),
4590 Point::new(3, 1)..Point::new(3, 2),
4591 ])
4592 });
4593
4594 editor.join_lines(&JoinLines, window, cx);
4595 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
4596
4597 assert_eq!(
4598 editor
4599 .selections
4600 .ranges::<Point>(&editor.display_snapshot(cx)),
4601 [
4602 Point::new(0, 7)..Point::new(0, 7),
4603 Point::new(1, 3)..Point::new(1, 3)
4604 ]
4605 );
4606 editor
4607 });
4608}
4609
4610#[gpui::test]
4611async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
4612 init_test(cx, |_| {});
4613
4614 let mut cx = EditorTestContext::new(cx).await;
4615
4616 let diff_base = r#"
4617 Line 0
4618 Line 1
4619 Line 2
4620 Line 3
4621 "#
4622 .unindent();
4623
4624 cx.set_state(
4625 &r#"
4626 ˇLine 0
4627 Line 1
4628 Line 2
4629 Line 3
4630 "#
4631 .unindent(),
4632 );
4633
4634 cx.set_head_text(&diff_base);
4635 executor.run_until_parked();
4636
4637 // Join lines
4638 cx.update_editor(|editor, window, cx| {
4639 editor.join_lines(&JoinLines, window, cx);
4640 });
4641 executor.run_until_parked();
4642
4643 cx.assert_editor_state(
4644 &r#"
4645 Line 0ˇ Line 1
4646 Line 2
4647 Line 3
4648 "#
4649 .unindent(),
4650 );
4651 // Join again
4652 cx.update_editor(|editor, window, cx| {
4653 editor.join_lines(&JoinLines, window, cx);
4654 });
4655 executor.run_until_parked();
4656
4657 cx.assert_editor_state(
4658 &r#"
4659 Line 0 Line 1ˇ Line 2
4660 Line 3
4661 "#
4662 .unindent(),
4663 );
4664}
4665
4666#[gpui::test]
4667async fn test_custom_newlines_cause_no_false_positive_diffs(
4668 executor: BackgroundExecutor,
4669 cx: &mut TestAppContext,
4670) {
4671 init_test(cx, |_| {});
4672 let mut cx = EditorTestContext::new(cx).await;
4673 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
4674 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
4675 executor.run_until_parked();
4676
4677 cx.update_editor(|editor, window, cx| {
4678 let snapshot = editor.snapshot(window, cx);
4679 assert_eq!(
4680 snapshot
4681 .buffer_snapshot()
4682 .diff_hunks_in_range(0..snapshot.buffer_snapshot().len())
4683 .collect::<Vec<_>>(),
4684 Vec::new(),
4685 "Should not have any diffs for files with custom newlines"
4686 );
4687 });
4688}
4689
4690#[gpui::test]
4691async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
4692 init_test(cx, |_| {});
4693
4694 let mut cx = EditorTestContext::new(cx).await;
4695
4696 // Test sort_lines_case_insensitive()
4697 cx.set_state(indoc! {"
4698 «z
4699 y
4700 x
4701 Z
4702 Y
4703 Xˇ»
4704 "});
4705 cx.update_editor(|e, window, cx| {
4706 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
4707 });
4708 cx.assert_editor_state(indoc! {"
4709 «x
4710 X
4711 y
4712 Y
4713 z
4714 Zˇ»
4715 "});
4716
4717 // Test sort_lines_by_length()
4718 //
4719 // Demonstrates:
4720 // - ∞ is 3 bytes UTF-8, but sorted by its char count (1)
4721 // - sort is stable
4722 cx.set_state(indoc! {"
4723 «123
4724 æ
4725 12
4726 ∞
4727 1
4728 æˇ»
4729 "});
4730 cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
4731 cx.assert_editor_state(indoc! {"
4732 «æ
4733 ∞
4734 1
4735 æ
4736 12
4737 123ˇ»
4738 "});
4739
4740 // Test reverse_lines()
4741 cx.set_state(indoc! {"
4742 «5
4743 4
4744 3
4745 2
4746 1ˇ»
4747 "});
4748 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4749 cx.assert_editor_state(indoc! {"
4750 «1
4751 2
4752 3
4753 4
4754 5ˇ»
4755 "});
4756
4757 // Skip testing shuffle_line()
4758
4759 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4760 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4761
4762 // Don't manipulate when cursor is on single line, but expand the selection
4763 cx.set_state(indoc! {"
4764 ddˇdd
4765 ccc
4766 bb
4767 a
4768 "});
4769 cx.update_editor(|e, window, cx| {
4770 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4771 });
4772 cx.assert_editor_state(indoc! {"
4773 «ddddˇ»
4774 ccc
4775 bb
4776 a
4777 "});
4778
4779 // Basic manipulate case
4780 // Start selection moves to column 0
4781 // End of selection shrinks to fit shorter line
4782 cx.set_state(indoc! {"
4783 dd«d
4784 ccc
4785 bb
4786 aaaaaˇ»
4787 "});
4788 cx.update_editor(|e, window, cx| {
4789 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4790 });
4791 cx.assert_editor_state(indoc! {"
4792 «aaaaa
4793 bb
4794 ccc
4795 dddˇ»
4796 "});
4797
4798 // Manipulate case with newlines
4799 cx.set_state(indoc! {"
4800 dd«d
4801 ccc
4802
4803 bb
4804 aaaaa
4805
4806 ˇ»
4807 "});
4808 cx.update_editor(|e, window, cx| {
4809 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4810 });
4811 cx.assert_editor_state(indoc! {"
4812 «
4813
4814 aaaaa
4815 bb
4816 ccc
4817 dddˇ»
4818
4819 "});
4820
4821 // Adding new line
4822 cx.set_state(indoc! {"
4823 aa«a
4824 bbˇ»b
4825 "});
4826 cx.update_editor(|e, window, cx| {
4827 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4828 });
4829 cx.assert_editor_state(indoc! {"
4830 «aaa
4831 bbb
4832 added_lineˇ»
4833 "});
4834
4835 // Removing line
4836 cx.set_state(indoc! {"
4837 aa«a
4838 bbbˇ»
4839 "});
4840 cx.update_editor(|e, window, cx| {
4841 e.manipulate_immutable_lines(window, cx, |lines| {
4842 lines.pop();
4843 })
4844 });
4845 cx.assert_editor_state(indoc! {"
4846 «aaaˇ»
4847 "});
4848
4849 // Removing all lines
4850 cx.set_state(indoc! {"
4851 aa«a
4852 bbbˇ»
4853 "});
4854 cx.update_editor(|e, window, cx| {
4855 e.manipulate_immutable_lines(window, cx, |lines| {
4856 lines.drain(..);
4857 })
4858 });
4859 cx.assert_editor_state(indoc! {"
4860 ˇ
4861 "});
4862}
4863
4864#[gpui::test]
4865async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4866 init_test(cx, |_| {});
4867
4868 let mut cx = EditorTestContext::new(cx).await;
4869
4870 // Consider continuous selection as single selection
4871 cx.set_state(indoc! {"
4872 Aaa«aa
4873 cˇ»c«c
4874 bb
4875 aaaˇ»aa
4876 "});
4877 cx.update_editor(|e, window, cx| {
4878 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4879 });
4880 cx.assert_editor_state(indoc! {"
4881 «Aaaaa
4882 ccc
4883 bb
4884 aaaaaˇ»
4885 "});
4886
4887 cx.set_state(indoc! {"
4888 Aaa«aa
4889 cˇ»c«c
4890 bb
4891 aaaˇ»aa
4892 "});
4893 cx.update_editor(|e, window, cx| {
4894 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4895 });
4896 cx.assert_editor_state(indoc! {"
4897 «Aaaaa
4898 ccc
4899 bbˇ»
4900 "});
4901
4902 // Consider non continuous selection as distinct dedup operations
4903 cx.set_state(indoc! {"
4904 «aaaaa
4905 bb
4906 aaaaa
4907 aaaaaˇ»
4908
4909 aaa«aaˇ»
4910 "});
4911 cx.update_editor(|e, window, cx| {
4912 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4913 });
4914 cx.assert_editor_state(indoc! {"
4915 «aaaaa
4916 bbˇ»
4917
4918 «aaaaaˇ»
4919 "});
4920}
4921
4922#[gpui::test]
4923async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4924 init_test(cx, |_| {});
4925
4926 let mut cx = EditorTestContext::new(cx).await;
4927
4928 cx.set_state(indoc! {"
4929 «Aaa
4930 aAa
4931 Aaaˇ»
4932 "});
4933 cx.update_editor(|e, window, cx| {
4934 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4935 });
4936 cx.assert_editor_state(indoc! {"
4937 «Aaa
4938 aAaˇ»
4939 "});
4940
4941 cx.set_state(indoc! {"
4942 «Aaa
4943 aAa
4944 aaAˇ»
4945 "});
4946 cx.update_editor(|e, window, cx| {
4947 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4948 });
4949 cx.assert_editor_state(indoc! {"
4950 «Aaaˇ»
4951 "});
4952}
4953
4954#[gpui::test]
4955async fn test_wrap_in_tag_single_selection(cx: &mut TestAppContext) {
4956 init_test(cx, |_| {});
4957
4958 let mut cx = EditorTestContext::new(cx).await;
4959
4960 let js_language = Arc::new(Language::new(
4961 LanguageConfig {
4962 name: "JavaScript".into(),
4963 wrap_characters: Some(language::WrapCharactersConfig {
4964 start_prefix: "<".into(),
4965 start_suffix: ">".into(),
4966 end_prefix: "</".into(),
4967 end_suffix: ">".into(),
4968 }),
4969 ..LanguageConfig::default()
4970 },
4971 None,
4972 ));
4973
4974 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
4975
4976 cx.set_state(indoc! {"
4977 «testˇ»
4978 "});
4979 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4980 cx.assert_editor_state(indoc! {"
4981 <«ˇ»>test</«ˇ»>
4982 "});
4983
4984 cx.set_state(indoc! {"
4985 «test
4986 testˇ»
4987 "});
4988 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4989 cx.assert_editor_state(indoc! {"
4990 <«ˇ»>test
4991 test</«ˇ»>
4992 "});
4993
4994 cx.set_state(indoc! {"
4995 teˇst
4996 "});
4997 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4998 cx.assert_editor_state(indoc! {"
4999 te<«ˇ»></«ˇ»>st
5000 "});
5001}
5002
5003#[gpui::test]
5004async fn test_wrap_in_tag_multi_selection(cx: &mut TestAppContext) {
5005 init_test(cx, |_| {});
5006
5007 let mut cx = EditorTestContext::new(cx).await;
5008
5009 let js_language = Arc::new(Language::new(
5010 LanguageConfig {
5011 name: "JavaScript".into(),
5012 wrap_characters: Some(language::WrapCharactersConfig {
5013 start_prefix: "<".into(),
5014 start_suffix: ">".into(),
5015 end_prefix: "</".into(),
5016 end_suffix: ">".into(),
5017 }),
5018 ..LanguageConfig::default()
5019 },
5020 None,
5021 ));
5022
5023 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
5024
5025 cx.set_state(indoc! {"
5026 «testˇ»
5027 «testˇ» «testˇ»
5028 «testˇ»
5029 "});
5030 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5031 cx.assert_editor_state(indoc! {"
5032 <«ˇ»>test</«ˇ»>
5033 <«ˇ»>test</«ˇ»> <«ˇ»>test</«ˇ»>
5034 <«ˇ»>test</«ˇ»>
5035 "});
5036
5037 cx.set_state(indoc! {"
5038 «test
5039 testˇ»
5040 «test
5041 testˇ»
5042 "});
5043 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5044 cx.assert_editor_state(indoc! {"
5045 <«ˇ»>test
5046 test</«ˇ»>
5047 <«ˇ»>test
5048 test</«ˇ»>
5049 "});
5050}
5051
5052#[gpui::test]
5053async fn test_wrap_in_tag_does_nothing_in_unsupported_languages(cx: &mut TestAppContext) {
5054 init_test(cx, |_| {});
5055
5056 let mut cx = EditorTestContext::new(cx).await;
5057
5058 let plaintext_language = Arc::new(Language::new(
5059 LanguageConfig {
5060 name: "Plain Text".into(),
5061 ..LanguageConfig::default()
5062 },
5063 None,
5064 ));
5065
5066 cx.update_buffer(|buffer, cx| buffer.set_language(Some(plaintext_language), cx));
5067
5068 cx.set_state(indoc! {"
5069 «testˇ»
5070 "});
5071 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5072 cx.assert_editor_state(indoc! {"
5073 «testˇ»
5074 "});
5075}
5076
5077#[gpui::test]
5078async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
5079 init_test(cx, |_| {});
5080
5081 let mut cx = EditorTestContext::new(cx).await;
5082
5083 // Manipulate with multiple selections on a single line
5084 cx.set_state(indoc! {"
5085 dd«dd
5086 cˇ»c«c
5087 bb
5088 aaaˇ»aa
5089 "});
5090 cx.update_editor(|e, window, cx| {
5091 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5092 });
5093 cx.assert_editor_state(indoc! {"
5094 «aaaaa
5095 bb
5096 ccc
5097 ddddˇ»
5098 "});
5099
5100 // Manipulate with multiple disjoin selections
5101 cx.set_state(indoc! {"
5102 5«
5103 4
5104 3
5105 2
5106 1ˇ»
5107
5108 dd«dd
5109 ccc
5110 bb
5111 aaaˇ»aa
5112 "});
5113 cx.update_editor(|e, window, cx| {
5114 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5115 });
5116 cx.assert_editor_state(indoc! {"
5117 «1
5118 2
5119 3
5120 4
5121 5ˇ»
5122
5123 «aaaaa
5124 bb
5125 ccc
5126 ddddˇ»
5127 "});
5128
5129 // Adding lines on each selection
5130 cx.set_state(indoc! {"
5131 2«
5132 1ˇ»
5133
5134 bb«bb
5135 aaaˇ»aa
5136 "});
5137 cx.update_editor(|e, window, cx| {
5138 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
5139 });
5140 cx.assert_editor_state(indoc! {"
5141 «2
5142 1
5143 added lineˇ»
5144
5145 «bbbb
5146 aaaaa
5147 added lineˇ»
5148 "});
5149
5150 // Removing lines on each selection
5151 cx.set_state(indoc! {"
5152 2«
5153 1ˇ»
5154
5155 bb«bb
5156 aaaˇ»aa
5157 "});
5158 cx.update_editor(|e, window, cx| {
5159 e.manipulate_immutable_lines(window, cx, |lines| {
5160 lines.pop();
5161 })
5162 });
5163 cx.assert_editor_state(indoc! {"
5164 «2ˇ»
5165
5166 «bbbbˇ»
5167 "});
5168}
5169
5170#[gpui::test]
5171async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
5172 init_test(cx, |settings| {
5173 settings.defaults.tab_size = NonZeroU32::new(3)
5174 });
5175
5176 let mut cx = EditorTestContext::new(cx).await;
5177
5178 // MULTI SELECTION
5179 // Ln.1 "«" tests empty lines
5180 // Ln.9 tests just leading whitespace
5181 cx.set_state(indoc! {"
5182 «
5183 abc // No indentationˇ»
5184 «\tabc // 1 tabˇ»
5185 \t\tabc « ˇ» // 2 tabs
5186 \t ab«c // Tab followed by space
5187 \tabc // Space followed by tab (3 spaces should be the result)
5188 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5189 abˇ»ˇc ˇ ˇ // Already space indented«
5190 \t
5191 \tabc\tdef // Only the leading tab is manipulatedˇ»
5192 "});
5193 cx.update_editor(|e, window, cx| {
5194 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5195 });
5196 cx.assert_editor_state(
5197 indoc! {"
5198 «
5199 abc // No indentation
5200 abc // 1 tab
5201 abc // 2 tabs
5202 abc // Tab followed by space
5203 abc // Space followed by tab (3 spaces should be the result)
5204 abc // Mixed indentation (tab conversion depends on the column)
5205 abc // Already space indented
5206 ·
5207 abc\tdef // Only the leading tab is manipulatedˇ»
5208 "}
5209 .replace("·", "")
5210 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5211 );
5212
5213 // Test on just a few lines, the others should remain unchanged
5214 // Only lines (3, 5, 10, 11) should change
5215 cx.set_state(
5216 indoc! {"
5217 ·
5218 abc // No indentation
5219 \tabcˇ // 1 tab
5220 \t\tabc // 2 tabs
5221 \t abcˇ // Tab followed by space
5222 \tabc // Space followed by tab (3 spaces should be the result)
5223 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5224 abc // Already space indented
5225 «\t
5226 \tabc\tdef // Only the leading tab is manipulatedˇ»
5227 "}
5228 .replace("·", "")
5229 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5230 );
5231 cx.update_editor(|e, window, cx| {
5232 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5233 });
5234 cx.assert_editor_state(
5235 indoc! {"
5236 ·
5237 abc // No indentation
5238 « abc // 1 tabˇ»
5239 \t\tabc // 2 tabs
5240 « abc // Tab followed by spaceˇ»
5241 \tabc // Space followed by tab (3 spaces should be the result)
5242 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5243 abc // Already space indented
5244 « ·
5245 abc\tdef // Only the leading tab is manipulatedˇ»
5246 "}
5247 .replace("·", "")
5248 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5249 );
5250
5251 // SINGLE SELECTION
5252 // Ln.1 "«" tests empty lines
5253 // Ln.9 tests just leading whitespace
5254 cx.set_state(indoc! {"
5255 «
5256 abc // No indentation
5257 \tabc // 1 tab
5258 \t\tabc // 2 tabs
5259 \t abc // Tab followed by space
5260 \tabc // Space followed by tab (3 spaces should be the result)
5261 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5262 abc // Already space indented
5263 \t
5264 \tabc\tdef // Only the leading tab is manipulatedˇ»
5265 "});
5266 cx.update_editor(|e, window, cx| {
5267 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5268 });
5269 cx.assert_editor_state(
5270 indoc! {"
5271 «
5272 abc // No indentation
5273 abc // 1 tab
5274 abc // 2 tabs
5275 abc // Tab followed by space
5276 abc // Space followed by tab (3 spaces should be the result)
5277 abc // Mixed indentation (tab conversion depends on the column)
5278 abc // Already space indented
5279 ·
5280 abc\tdef // Only the leading tab is manipulatedˇ»
5281 "}
5282 .replace("·", "")
5283 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5284 );
5285}
5286
5287#[gpui::test]
5288async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
5289 init_test(cx, |settings| {
5290 settings.defaults.tab_size = NonZeroU32::new(3)
5291 });
5292
5293 let mut cx = EditorTestContext::new(cx).await;
5294
5295 // MULTI SELECTION
5296 // Ln.1 "«" tests empty lines
5297 // Ln.11 tests just leading whitespace
5298 cx.set_state(indoc! {"
5299 «
5300 abˇ»ˇc // No indentation
5301 abc ˇ ˇ // 1 space (< 3 so dont convert)
5302 abc « // 2 spaces (< 3 so dont convert)
5303 abc // 3 spaces (convert)
5304 abc ˇ» // 5 spaces (1 tab + 2 spaces)
5305 «\tˇ»\t«\tˇ»abc // Already tab indented
5306 «\t abc // Tab followed by space
5307 \tabc // Space followed by tab (should be consumed due to tab)
5308 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5309 \tˇ» «\t
5310 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
5311 "});
5312 cx.update_editor(|e, window, cx| {
5313 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5314 });
5315 cx.assert_editor_state(indoc! {"
5316 «
5317 abc // No indentation
5318 abc // 1 space (< 3 so dont convert)
5319 abc // 2 spaces (< 3 so dont convert)
5320 \tabc // 3 spaces (convert)
5321 \t abc // 5 spaces (1 tab + 2 spaces)
5322 \t\t\tabc // Already tab indented
5323 \t abc // Tab followed by space
5324 \tabc // Space followed by tab (should be consumed due to tab)
5325 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5326 \t\t\t
5327 \tabc \t // Only the leading spaces should be convertedˇ»
5328 "});
5329
5330 // Test on just a few lines, the other should remain unchanged
5331 // Only lines (4, 8, 11, 12) should change
5332 cx.set_state(
5333 indoc! {"
5334 ·
5335 abc // No indentation
5336 abc // 1 space (< 3 so dont convert)
5337 abc // 2 spaces (< 3 so dont convert)
5338 « abc // 3 spaces (convert)ˇ»
5339 abc // 5 spaces (1 tab + 2 spaces)
5340 \t\t\tabc // Already tab indented
5341 \t abc // Tab followed by space
5342 \tabc ˇ // Space followed by tab (should be consumed due to tab)
5343 \t\t \tabc // Mixed indentation
5344 \t \t \t \tabc // Mixed indentation
5345 \t \tˇ
5346 « abc \t // Only the leading spaces should be convertedˇ»
5347 "}
5348 .replace("·", "")
5349 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5350 );
5351 cx.update_editor(|e, window, cx| {
5352 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5353 });
5354 cx.assert_editor_state(
5355 indoc! {"
5356 ·
5357 abc // No indentation
5358 abc // 1 space (< 3 so dont convert)
5359 abc // 2 spaces (< 3 so dont convert)
5360 «\tabc // 3 spaces (convert)ˇ»
5361 abc // 5 spaces (1 tab + 2 spaces)
5362 \t\t\tabc // Already tab indented
5363 \t abc // Tab followed by space
5364 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
5365 \t\t \tabc // Mixed indentation
5366 \t \t \t \tabc // Mixed indentation
5367 «\t\t\t
5368 \tabc \t // Only the leading spaces should be convertedˇ»
5369 "}
5370 .replace("·", "")
5371 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5372 );
5373
5374 // SINGLE SELECTION
5375 // Ln.1 "«" tests empty lines
5376 // Ln.11 tests just leading whitespace
5377 cx.set_state(indoc! {"
5378 «
5379 abc // No indentation
5380 abc // 1 space (< 3 so dont convert)
5381 abc // 2 spaces (< 3 so dont convert)
5382 abc // 3 spaces (convert)
5383 abc // 5 spaces (1 tab + 2 spaces)
5384 \t\t\tabc // Already tab indented
5385 \t abc // Tab followed by space
5386 \tabc // Space followed by tab (should be consumed due to tab)
5387 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5388 \t \t
5389 abc \t // Only the leading spaces should be convertedˇ»
5390 "});
5391 cx.update_editor(|e, window, cx| {
5392 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5393 });
5394 cx.assert_editor_state(indoc! {"
5395 «
5396 abc // No indentation
5397 abc // 1 space (< 3 so dont convert)
5398 abc // 2 spaces (< 3 so dont convert)
5399 \tabc // 3 spaces (convert)
5400 \t abc // 5 spaces (1 tab + 2 spaces)
5401 \t\t\tabc // Already tab indented
5402 \t abc // Tab followed by space
5403 \tabc // Space followed by tab (should be consumed due to tab)
5404 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5405 \t\t\t
5406 \tabc \t // Only the leading spaces should be convertedˇ»
5407 "});
5408}
5409
5410#[gpui::test]
5411async fn test_toggle_case(cx: &mut TestAppContext) {
5412 init_test(cx, |_| {});
5413
5414 let mut cx = EditorTestContext::new(cx).await;
5415
5416 // If all lower case -> upper case
5417 cx.set_state(indoc! {"
5418 «hello worldˇ»
5419 "});
5420 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5421 cx.assert_editor_state(indoc! {"
5422 «HELLO WORLDˇ»
5423 "});
5424
5425 // If all upper case -> lower case
5426 cx.set_state(indoc! {"
5427 «HELLO WORLDˇ»
5428 "});
5429 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5430 cx.assert_editor_state(indoc! {"
5431 «hello worldˇ»
5432 "});
5433
5434 // If any upper case characters are identified -> lower case
5435 // This matches JetBrains IDEs
5436 cx.set_state(indoc! {"
5437 «hEllo worldˇ»
5438 "});
5439 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5440 cx.assert_editor_state(indoc! {"
5441 «hello worldˇ»
5442 "});
5443}
5444
5445#[gpui::test]
5446async fn test_convert_to_sentence_case(cx: &mut TestAppContext) {
5447 init_test(cx, |_| {});
5448
5449 let mut cx = EditorTestContext::new(cx).await;
5450
5451 cx.set_state(indoc! {"
5452 «implement-windows-supportˇ»
5453 "});
5454 cx.update_editor(|e, window, cx| {
5455 e.convert_to_sentence_case(&ConvertToSentenceCase, window, cx)
5456 });
5457 cx.assert_editor_state(indoc! {"
5458 «Implement windows supportˇ»
5459 "});
5460}
5461
5462#[gpui::test]
5463async fn test_manipulate_text(cx: &mut TestAppContext) {
5464 init_test(cx, |_| {});
5465
5466 let mut cx = EditorTestContext::new(cx).await;
5467
5468 // Test convert_to_upper_case()
5469 cx.set_state(indoc! {"
5470 «hello worldˇ»
5471 "});
5472 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5473 cx.assert_editor_state(indoc! {"
5474 «HELLO WORLDˇ»
5475 "});
5476
5477 // Test convert_to_lower_case()
5478 cx.set_state(indoc! {"
5479 «HELLO WORLDˇ»
5480 "});
5481 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
5482 cx.assert_editor_state(indoc! {"
5483 «hello worldˇ»
5484 "});
5485
5486 // Test multiple line, single selection case
5487 cx.set_state(indoc! {"
5488 «The quick brown
5489 fox jumps over
5490 the lazy dogˇ»
5491 "});
5492 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
5493 cx.assert_editor_state(indoc! {"
5494 «The Quick Brown
5495 Fox Jumps Over
5496 The Lazy Dogˇ»
5497 "});
5498
5499 // Test multiple line, single selection case
5500 cx.set_state(indoc! {"
5501 «The quick brown
5502 fox jumps over
5503 the lazy dogˇ»
5504 "});
5505 cx.update_editor(|e, window, cx| {
5506 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
5507 });
5508 cx.assert_editor_state(indoc! {"
5509 «TheQuickBrown
5510 FoxJumpsOver
5511 TheLazyDogˇ»
5512 "});
5513
5514 // From here on out, test more complex cases of manipulate_text()
5515
5516 // Test no selection case - should affect words cursors are in
5517 // Cursor at beginning, middle, and end of word
5518 cx.set_state(indoc! {"
5519 ˇhello big beauˇtiful worldˇ
5520 "});
5521 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5522 cx.assert_editor_state(indoc! {"
5523 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
5524 "});
5525
5526 // Test multiple selections on a single line and across multiple lines
5527 cx.set_state(indoc! {"
5528 «Theˇ» quick «brown
5529 foxˇ» jumps «overˇ»
5530 the «lazyˇ» dog
5531 "});
5532 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5533 cx.assert_editor_state(indoc! {"
5534 «THEˇ» quick «BROWN
5535 FOXˇ» jumps «OVERˇ»
5536 the «LAZYˇ» dog
5537 "});
5538
5539 // Test case where text length grows
5540 cx.set_state(indoc! {"
5541 «tschüߡ»
5542 "});
5543 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5544 cx.assert_editor_state(indoc! {"
5545 «TSCHÜSSˇ»
5546 "});
5547
5548 // Test to make sure we don't crash when text shrinks
5549 cx.set_state(indoc! {"
5550 aaa_bbbˇ
5551 "});
5552 cx.update_editor(|e, window, cx| {
5553 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5554 });
5555 cx.assert_editor_state(indoc! {"
5556 «aaaBbbˇ»
5557 "});
5558
5559 // Test to make sure we all aware of the fact that each word can grow and shrink
5560 // Final selections should be aware of this fact
5561 cx.set_state(indoc! {"
5562 aaa_bˇbb bbˇb_ccc ˇccc_ddd
5563 "});
5564 cx.update_editor(|e, window, cx| {
5565 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5566 });
5567 cx.assert_editor_state(indoc! {"
5568 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
5569 "});
5570
5571 cx.set_state(indoc! {"
5572 «hElLo, WoRld!ˇ»
5573 "});
5574 cx.update_editor(|e, window, cx| {
5575 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
5576 });
5577 cx.assert_editor_state(indoc! {"
5578 «HeLlO, wOrLD!ˇ»
5579 "});
5580
5581 // Test selections with `line_mode() = true`.
5582 cx.update_editor(|editor, _window, _cx| editor.selections.set_line_mode(true));
5583 cx.set_state(indoc! {"
5584 «The quick brown
5585 fox jumps over
5586 tˇ»he lazy dog
5587 "});
5588 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5589 cx.assert_editor_state(indoc! {"
5590 «THE QUICK BROWN
5591 FOX JUMPS OVER
5592 THE LAZY DOGˇ»
5593 "});
5594}
5595
5596#[gpui::test]
5597fn test_duplicate_line(cx: &mut TestAppContext) {
5598 init_test(cx, |_| {});
5599
5600 let editor = cx.add_window(|window, cx| {
5601 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5602 build_editor(buffer, window, cx)
5603 });
5604 _ = editor.update(cx, |editor, window, cx| {
5605 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5606 s.select_display_ranges([
5607 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5608 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5609 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5610 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5611 ])
5612 });
5613 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5614 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5615 assert_eq!(
5616 editor.selections.display_ranges(cx),
5617 vec![
5618 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
5619 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
5620 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5621 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5622 ]
5623 );
5624 });
5625
5626 let editor = cx.add_window(|window, cx| {
5627 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5628 build_editor(buffer, window, cx)
5629 });
5630 _ = editor.update(cx, |editor, window, cx| {
5631 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5632 s.select_display_ranges([
5633 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5634 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5635 ])
5636 });
5637 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5638 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5639 assert_eq!(
5640 editor.selections.display_ranges(cx),
5641 vec![
5642 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
5643 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
5644 ]
5645 );
5646 });
5647
5648 // With `move_upwards` the selections stay in place, except for
5649 // the lines inserted above them
5650 let editor = cx.add_window(|window, cx| {
5651 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5652 build_editor(buffer, window, cx)
5653 });
5654 _ = editor.update(cx, |editor, window, cx| {
5655 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5656 s.select_display_ranges([
5657 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5658 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5659 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5660 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5661 ])
5662 });
5663 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5664 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5665 assert_eq!(
5666 editor.selections.display_ranges(cx),
5667 vec![
5668 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5669 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5670 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
5671 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5672 ]
5673 );
5674 });
5675
5676 let editor = cx.add_window(|window, cx| {
5677 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5678 build_editor(buffer, window, cx)
5679 });
5680 _ = editor.update(cx, |editor, window, cx| {
5681 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5682 s.select_display_ranges([
5683 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5684 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5685 ])
5686 });
5687 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5688 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5689 assert_eq!(
5690 editor.selections.display_ranges(cx),
5691 vec![
5692 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5693 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5694 ]
5695 );
5696 });
5697
5698 let editor = cx.add_window(|window, cx| {
5699 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5700 build_editor(buffer, window, cx)
5701 });
5702 _ = editor.update(cx, |editor, window, cx| {
5703 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5704 s.select_display_ranges([
5705 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5706 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5707 ])
5708 });
5709 editor.duplicate_selection(&DuplicateSelection, window, cx);
5710 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
5711 assert_eq!(
5712 editor.selections.display_ranges(cx),
5713 vec![
5714 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5715 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
5716 ]
5717 );
5718 });
5719}
5720
5721#[gpui::test]
5722fn test_move_line_up_down(cx: &mut TestAppContext) {
5723 init_test(cx, |_| {});
5724
5725 let editor = cx.add_window(|window, cx| {
5726 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5727 build_editor(buffer, window, cx)
5728 });
5729 _ = editor.update(cx, |editor, window, cx| {
5730 editor.fold_creases(
5731 vec![
5732 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5733 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5734 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5735 ],
5736 true,
5737 window,
5738 cx,
5739 );
5740 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5741 s.select_display_ranges([
5742 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5743 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5744 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5745 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
5746 ])
5747 });
5748 assert_eq!(
5749 editor.display_text(cx),
5750 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
5751 );
5752
5753 editor.move_line_up(&MoveLineUp, window, cx);
5754 assert_eq!(
5755 editor.display_text(cx),
5756 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
5757 );
5758 assert_eq!(
5759 editor.selections.display_ranges(cx),
5760 vec![
5761 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5762 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5763 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5764 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5765 ]
5766 );
5767 });
5768
5769 _ = editor.update(cx, |editor, window, cx| {
5770 editor.move_line_down(&MoveLineDown, window, cx);
5771 assert_eq!(
5772 editor.display_text(cx),
5773 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
5774 );
5775 assert_eq!(
5776 editor.selections.display_ranges(cx),
5777 vec![
5778 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5779 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5780 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5781 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5782 ]
5783 );
5784 });
5785
5786 _ = editor.update(cx, |editor, window, cx| {
5787 editor.move_line_down(&MoveLineDown, window, cx);
5788 assert_eq!(
5789 editor.display_text(cx),
5790 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
5791 );
5792 assert_eq!(
5793 editor.selections.display_ranges(cx),
5794 vec![
5795 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5796 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5797 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5798 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5799 ]
5800 );
5801 });
5802
5803 _ = editor.update(cx, |editor, window, cx| {
5804 editor.move_line_up(&MoveLineUp, window, cx);
5805 assert_eq!(
5806 editor.display_text(cx),
5807 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
5808 );
5809 assert_eq!(
5810 editor.selections.display_ranges(cx),
5811 vec![
5812 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5813 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5814 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5815 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5816 ]
5817 );
5818 });
5819}
5820
5821#[gpui::test]
5822fn test_move_line_up_selection_at_end_of_fold(cx: &mut TestAppContext) {
5823 init_test(cx, |_| {});
5824 let editor = cx.add_window(|window, cx| {
5825 let buffer = MultiBuffer::build_simple("\n\n\n\n\n\naaaa\nbbbb\ncccc", cx);
5826 build_editor(buffer, window, cx)
5827 });
5828 _ = editor.update(cx, |editor, window, cx| {
5829 editor.fold_creases(
5830 vec![Crease::simple(
5831 Point::new(6, 4)..Point::new(7, 4),
5832 FoldPlaceholder::test(),
5833 )],
5834 true,
5835 window,
5836 cx,
5837 );
5838 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5839 s.select_ranges([Point::new(7, 4)..Point::new(7, 4)])
5840 });
5841 assert_eq!(editor.display_text(cx), "\n\n\n\n\n\naaaa⋯\ncccc");
5842 editor.move_line_up(&MoveLineUp, window, cx);
5843 let buffer_text = editor.buffer.read(cx).snapshot(cx).text();
5844 assert_eq!(buffer_text, "\n\n\n\n\naaaa\nbbbb\n\ncccc");
5845 });
5846}
5847
5848#[gpui::test]
5849fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
5850 init_test(cx, |_| {});
5851
5852 let editor = cx.add_window(|window, cx| {
5853 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5854 build_editor(buffer, window, cx)
5855 });
5856 _ = editor.update(cx, |editor, window, cx| {
5857 let snapshot = editor.buffer.read(cx).snapshot(cx);
5858 editor.insert_blocks(
5859 [BlockProperties {
5860 style: BlockStyle::Fixed,
5861 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
5862 height: Some(1),
5863 render: Arc::new(|_| div().into_any()),
5864 priority: 0,
5865 }],
5866 Some(Autoscroll::fit()),
5867 cx,
5868 );
5869 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5870 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
5871 });
5872 editor.move_line_down(&MoveLineDown, window, cx);
5873 });
5874}
5875
5876#[gpui::test]
5877async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
5878 init_test(cx, |_| {});
5879
5880 let mut cx = EditorTestContext::new(cx).await;
5881 cx.set_state(
5882 &"
5883 ˇzero
5884 one
5885 two
5886 three
5887 four
5888 five
5889 "
5890 .unindent(),
5891 );
5892
5893 // Create a four-line block that replaces three lines of text.
5894 cx.update_editor(|editor, window, cx| {
5895 let snapshot = editor.snapshot(window, cx);
5896 let snapshot = &snapshot.buffer_snapshot();
5897 let placement = BlockPlacement::Replace(
5898 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
5899 );
5900 editor.insert_blocks(
5901 [BlockProperties {
5902 placement,
5903 height: Some(4),
5904 style: BlockStyle::Sticky,
5905 render: Arc::new(|_| gpui::div().into_any_element()),
5906 priority: 0,
5907 }],
5908 None,
5909 cx,
5910 );
5911 });
5912
5913 // Move down so that the cursor touches the block.
5914 cx.update_editor(|editor, window, cx| {
5915 editor.move_down(&Default::default(), window, cx);
5916 });
5917 cx.assert_editor_state(
5918 &"
5919 zero
5920 «one
5921 two
5922 threeˇ»
5923 four
5924 five
5925 "
5926 .unindent(),
5927 );
5928
5929 // Move down past the block.
5930 cx.update_editor(|editor, window, cx| {
5931 editor.move_down(&Default::default(), window, cx);
5932 });
5933 cx.assert_editor_state(
5934 &"
5935 zero
5936 one
5937 two
5938 three
5939 ˇfour
5940 five
5941 "
5942 .unindent(),
5943 );
5944}
5945
5946#[gpui::test]
5947fn test_transpose(cx: &mut TestAppContext) {
5948 init_test(cx, |_| {});
5949
5950 _ = cx.add_window(|window, cx| {
5951 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
5952 editor.set_style(EditorStyle::default(), window, cx);
5953 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5954 s.select_ranges([1..1])
5955 });
5956 editor.transpose(&Default::default(), window, cx);
5957 assert_eq!(editor.text(cx), "bac");
5958 assert_eq!(
5959 editor.selections.ranges(&editor.display_snapshot(cx)),
5960 [2..2]
5961 );
5962
5963 editor.transpose(&Default::default(), window, cx);
5964 assert_eq!(editor.text(cx), "bca");
5965 assert_eq!(
5966 editor.selections.ranges(&editor.display_snapshot(cx)),
5967 [3..3]
5968 );
5969
5970 editor.transpose(&Default::default(), window, cx);
5971 assert_eq!(editor.text(cx), "bac");
5972 assert_eq!(
5973 editor.selections.ranges(&editor.display_snapshot(cx)),
5974 [3..3]
5975 );
5976
5977 editor
5978 });
5979
5980 _ = cx.add_window(|window, cx| {
5981 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5982 editor.set_style(EditorStyle::default(), window, cx);
5983 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5984 s.select_ranges([3..3])
5985 });
5986 editor.transpose(&Default::default(), window, cx);
5987 assert_eq!(editor.text(cx), "acb\nde");
5988 assert_eq!(
5989 editor.selections.ranges(&editor.display_snapshot(cx)),
5990 [3..3]
5991 );
5992
5993 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5994 s.select_ranges([4..4])
5995 });
5996 editor.transpose(&Default::default(), window, cx);
5997 assert_eq!(editor.text(cx), "acbd\ne");
5998 assert_eq!(
5999 editor.selections.ranges(&editor.display_snapshot(cx)),
6000 [5..5]
6001 );
6002
6003 editor.transpose(&Default::default(), window, cx);
6004 assert_eq!(editor.text(cx), "acbde\n");
6005 assert_eq!(
6006 editor.selections.ranges(&editor.display_snapshot(cx)),
6007 [6..6]
6008 );
6009
6010 editor.transpose(&Default::default(), window, cx);
6011 assert_eq!(editor.text(cx), "acbd\ne");
6012 assert_eq!(
6013 editor.selections.ranges(&editor.display_snapshot(cx)),
6014 [6..6]
6015 );
6016
6017 editor
6018 });
6019
6020 _ = cx.add_window(|window, cx| {
6021 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
6022 editor.set_style(EditorStyle::default(), window, cx);
6023 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6024 s.select_ranges([1..1, 2..2, 4..4])
6025 });
6026 editor.transpose(&Default::default(), window, cx);
6027 assert_eq!(editor.text(cx), "bacd\ne");
6028 assert_eq!(
6029 editor.selections.ranges(&editor.display_snapshot(cx)),
6030 [2..2, 3..3, 5..5]
6031 );
6032
6033 editor.transpose(&Default::default(), window, cx);
6034 assert_eq!(editor.text(cx), "bcade\n");
6035 assert_eq!(
6036 editor.selections.ranges(&editor.display_snapshot(cx)),
6037 [3..3, 4..4, 6..6]
6038 );
6039
6040 editor.transpose(&Default::default(), window, cx);
6041 assert_eq!(editor.text(cx), "bcda\ne");
6042 assert_eq!(
6043 editor.selections.ranges(&editor.display_snapshot(cx)),
6044 [4..4, 6..6]
6045 );
6046
6047 editor.transpose(&Default::default(), window, cx);
6048 assert_eq!(editor.text(cx), "bcade\n");
6049 assert_eq!(
6050 editor.selections.ranges(&editor.display_snapshot(cx)),
6051 [4..4, 6..6]
6052 );
6053
6054 editor.transpose(&Default::default(), window, cx);
6055 assert_eq!(editor.text(cx), "bcaed\n");
6056 assert_eq!(
6057 editor.selections.ranges(&editor.display_snapshot(cx)),
6058 [5..5, 6..6]
6059 );
6060
6061 editor
6062 });
6063
6064 _ = cx.add_window(|window, cx| {
6065 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
6066 editor.set_style(EditorStyle::default(), window, cx);
6067 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6068 s.select_ranges([4..4])
6069 });
6070 editor.transpose(&Default::default(), window, cx);
6071 assert_eq!(editor.text(cx), "🏀🍐✋");
6072 assert_eq!(
6073 editor.selections.ranges(&editor.display_snapshot(cx)),
6074 [8..8]
6075 );
6076
6077 editor.transpose(&Default::default(), window, cx);
6078 assert_eq!(editor.text(cx), "🏀✋🍐");
6079 assert_eq!(
6080 editor.selections.ranges(&editor.display_snapshot(cx)),
6081 [11..11]
6082 );
6083
6084 editor.transpose(&Default::default(), window, cx);
6085 assert_eq!(editor.text(cx), "🏀🍐✋");
6086 assert_eq!(
6087 editor.selections.ranges(&editor.display_snapshot(cx)),
6088 [11..11]
6089 );
6090
6091 editor
6092 });
6093}
6094
6095#[gpui::test]
6096async fn test_rewrap(cx: &mut TestAppContext) {
6097 init_test(cx, |settings| {
6098 settings.languages.0.extend([
6099 (
6100 "Markdown".into(),
6101 LanguageSettingsContent {
6102 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
6103 preferred_line_length: Some(40),
6104 ..Default::default()
6105 },
6106 ),
6107 (
6108 "Plain Text".into(),
6109 LanguageSettingsContent {
6110 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
6111 preferred_line_length: Some(40),
6112 ..Default::default()
6113 },
6114 ),
6115 (
6116 "C++".into(),
6117 LanguageSettingsContent {
6118 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6119 preferred_line_length: Some(40),
6120 ..Default::default()
6121 },
6122 ),
6123 (
6124 "Python".into(),
6125 LanguageSettingsContent {
6126 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6127 preferred_line_length: Some(40),
6128 ..Default::default()
6129 },
6130 ),
6131 (
6132 "Rust".into(),
6133 LanguageSettingsContent {
6134 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6135 preferred_line_length: Some(40),
6136 ..Default::default()
6137 },
6138 ),
6139 ])
6140 });
6141
6142 let mut cx = EditorTestContext::new(cx).await;
6143
6144 let cpp_language = Arc::new(Language::new(
6145 LanguageConfig {
6146 name: "C++".into(),
6147 line_comments: vec!["// ".into()],
6148 ..LanguageConfig::default()
6149 },
6150 None,
6151 ));
6152 let python_language = Arc::new(Language::new(
6153 LanguageConfig {
6154 name: "Python".into(),
6155 line_comments: vec!["# ".into()],
6156 ..LanguageConfig::default()
6157 },
6158 None,
6159 ));
6160 let markdown_language = Arc::new(Language::new(
6161 LanguageConfig {
6162 name: "Markdown".into(),
6163 rewrap_prefixes: vec![
6164 regex::Regex::new("\\d+\\.\\s+").unwrap(),
6165 regex::Regex::new("[-*+]\\s+").unwrap(),
6166 ],
6167 ..LanguageConfig::default()
6168 },
6169 None,
6170 ));
6171 let rust_language = Arc::new(
6172 Language::new(
6173 LanguageConfig {
6174 name: "Rust".into(),
6175 line_comments: vec!["// ".into(), "/// ".into()],
6176 ..LanguageConfig::default()
6177 },
6178 Some(tree_sitter_rust::LANGUAGE.into()),
6179 )
6180 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
6181 .unwrap(),
6182 );
6183
6184 let plaintext_language = Arc::new(Language::new(
6185 LanguageConfig {
6186 name: "Plain Text".into(),
6187 ..LanguageConfig::default()
6188 },
6189 None,
6190 ));
6191
6192 // Test basic rewrapping of a long line with a cursor
6193 assert_rewrap(
6194 indoc! {"
6195 // ˇThis is a long comment that needs to be wrapped.
6196 "},
6197 indoc! {"
6198 // ˇThis is a long comment that needs to
6199 // be wrapped.
6200 "},
6201 cpp_language.clone(),
6202 &mut cx,
6203 );
6204
6205 // Test rewrapping a full selection
6206 assert_rewrap(
6207 indoc! {"
6208 «// This selected long comment needs to be wrapped.ˇ»"
6209 },
6210 indoc! {"
6211 «// This selected long comment needs to
6212 // be wrapped.ˇ»"
6213 },
6214 cpp_language.clone(),
6215 &mut cx,
6216 );
6217
6218 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
6219 assert_rewrap(
6220 indoc! {"
6221 // ˇThis is the first line.
6222 // Thisˇ is the second line.
6223 // This is the thirdˇ line, all part of one paragraph.
6224 "},
6225 indoc! {"
6226 // ˇThis is the first line. Thisˇ is the
6227 // second line. This is the thirdˇ line,
6228 // all part of one paragraph.
6229 "},
6230 cpp_language.clone(),
6231 &mut cx,
6232 );
6233
6234 // Test multiple cursors in different paragraphs trigger separate rewraps
6235 assert_rewrap(
6236 indoc! {"
6237 // ˇThis is the first paragraph, first line.
6238 // ˇThis is the first paragraph, second line.
6239
6240 // ˇThis is the second paragraph, first line.
6241 // ˇThis is the second paragraph, second line.
6242 "},
6243 indoc! {"
6244 // ˇThis is the first paragraph, first
6245 // line. ˇThis is the first paragraph,
6246 // second line.
6247
6248 // ˇThis is the second paragraph, first
6249 // line. ˇThis is the second paragraph,
6250 // second line.
6251 "},
6252 cpp_language.clone(),
6253 &mut cx,
6254 );
6255
6256 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
6257 assert_rewrap(
6258 indoc! {"
6259 «// A regular long long comment to be wrapped.
6260 /// A documentation long comment to be wrapped.ˇ»
6261 "},
6262 indoc! {"
6263 «// A regular long long comment to be
6264 // wrapped.
6265 /// A documentation long comment to be
6266 /// wrapped.ˇ»
6267 "},
6268 rust_language.clone(),
6269 &mut cx,
6270 );
6271
6272 // Test that change in indentation level trigger seperate rewraps
6273 assert_rewrap(
6274 indoc! {"
6275 fn foo() {
6276 «// This is a long comment at the base indent.
6277 // This is a long comment at the next indent.ˇ»
6278 }
6279 "},
6280 indoc! {"
6281 fn foo() {
6282 «// This is a long comment at the
6283 // base indent.
6284 // This is a long comment at the
6285 // next indent.ˇ»
6286 }
6287 "},
6288 rust_language.clone(),
6289 &mut cx,
6290 );
6291
6292 // Test that different comment prefix characters (e.g., '#') are handled correctly
6293 assert_rewrap(
6294 indoc! {"
6295 # ˇThis is a long comment using a pound sign.
6296 "},
6297 indoc! {"
6298 # ˇThis is a long comment using a pound
6299 # sign.
6300 "},
6301 python_language,
6302 &mut cx,
6303 );
6304
6305 // Test rewrapping only affects comments, not code even when selected
6306 assert_rewrap(
6307 indoc! {"
6308 «/// This doc comment is long and should be wrapped.
6309 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6310 "},
6311 indoc! {"
6312 «/// This doc comment is long and should
6313 /// be wrapped.
6314 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6315 "},
6316 rust_language.clone(),
6317 &mut cx,
6318 );
6319
6320 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
6321 assert_rewrap(
6322 indoc! {"
6323 # Header
6324
6325 A long long long line of markdown text to wrap.ˇ
6326 "},
6327 indoc! {"
6328 # Header
6329
6330 A long long long line of markdown text
6331 to wrap.ˇ
6332 "},
6333 markdown_language.clone(),
6334 &mut cx,
6335 );
6336
6337 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
6338 assert_rewrap(
6339 indoc! {"
6340 «1. This is a numbered list item that is very long and needs to be wrapped properly.
6341 2. This is a numbered list item that is very long and needs to be wrapped properly.
6342 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
6343 "},
6344 indoc! {"
6345 «1. This is a numbered list item that is
6346 very long and needs to be wrapped
6347 properly.
6348 2. This is a numbered list item that is
6349 very long and needs to be wrapped
6350 properly.
6351 - This is an unordered list item that is
6352 also very long and should not merge
6353 with the numbered item.ˇ»
6354 "},
6355 markdown_language.clone(),
6356 &mut cx,
6357 );
6358
6359 // Test that rewrapping add indents for rewrapping boundary if not exists already.
6360 assert_rewrap(
6361 indoc! {"
6362 «1. This is a numbered list item that is
6363 very long and needs to be wrapped
6364 properly.
6365 2. This is a numbered list item that is
6366 very long and needs to be wrapped
6367 properly.
6368 - This is an unordered list item that is
6369 also very long and should not merge with
6370 the numbered item.ˇ»
6371 "},
6372 indoc! {"
6373 «1. This is a numbered list item that is
6374 very long and needs to be wrapped
6375 properly.
6376 2. This is a numbered list item that is
6377 very long and needs to be wrapped
6378 properly.
6379 - This is an unordered list item that is
6380 also very long and should not merge
6381 with the numbered item.ˇ»
6382 "},
6383 markdown_language.clone(),
6384 &mut cx,
6385 );
6386
6387 // Test that rewrapping maintain indents even when they already exists.
6388 assert_rewrap(
6389 indoc! {"
6390 «1. This is a numbered list
6391 item that is very long and needs to be wrapped properly.
6392 2. This is a numbered list
6393 item that is very long and needs to be wrapped properly.
6394 - This is an unordered list item that is also very long and
6395 should not merge with the numbered item.ˇ»
6396 "},
6397 indoc! {"
6398 «1. This is a numbered list item that is
6399 very long and needs to be wrapped
6400 properly.
6401 2. This is a numbered list item that is
6402 very long and needs to be wrapped
6403 properly.
6404 - This is an unordered list item that is
6405 also very long and should not merge
6406 with the numbered item.ˇ»
6407 "},
6408 markdown_language,
6409 &mut cx,
6410 );
6411
6412 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
6413 assert_rewrap(
6414 indoc! {"
6415 ˇThis is a very long line of plain text that will be wrapped.
6416 "},
6417 indoc! {"
6418 ˇThis is a very long line of plain text
6419 that will be wrapped.
6420 "},
6421 plaintext_language.clone(),
6422 &mut cx,
6423 );
6424
6425 // Test that non-commented code acts as a paragraph boundary within a selection
6426 assert_rewrap(
6427 indoc! {"
6428 «// This is the first long comment block to be wrapped.
6429 fn my_func(a: u32);
6430 // This is the second long comment block to be wrapped.ˇ»
6431 "},
6432 indoc! {"
6433 «// This is the first long comment block
6434 // to be wrapped.
6435 fn my_func(a: u32);
6436 // This is the second long comment block
6437 // to be wrapped.ˇ»
6438 "},
6439 rust_language,
6440 &mut cx,
6441 );
6442
6443 // Test rewrapping multiple selections, including ones with blank lines or tabs
6444 assert_rewrap(
6445 indoc! {"
6446 «ˇThis is a very long line that will be wrapped.
6447
6448 This is another paragraph in the same selection.»
6449
6450 «\tThis is a very long indented line that will be wrapped.ˇ»
6451 "},
6452 indoc! {"
6453 «ˇThis is a very long line that will be
6454 wrapped.
6455
6456 This is another paragraph in the same
6457 selection.»
6458
6459 «\tThis is a very long indented line
6460 \tthat will be wrapped.ˇ»
6461 "},
6462 plaintext_language,
6463 &mut cx,
6464 );
6465
6466 // Test that an empty comment line acts as a paragraph boundary
6467 assert_rewrap(
6468 indoc! {"
6469 // ˇThis is a long comment that will be wrapped.
6470 //
6471 // And this is another long comment that will also be wrapped.ˇ
6472 "},
6473 indoc! {"
6474 // ˇThis is a long comment that will be
6475 // wrapped.
6476 //
6477 // And this is another long comment that
6478 // will also be wrapped.ˇ
6479 "},
6480 cpp_language,
6481 &mut cx,
6482 );
6483
6484 #[track_caller]
6485 fn assert_rewrap(
6486 unwrapped_text: &str,
6487 wrapped_text: &str,
6488 language: Arc<Language>,
6489 cx: &mut EditorTestContext,
6490 ) {
6491 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6492 cx.set_state(unwrapped_text);
6493 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6494 cx.assert_editor_state(wrapped_text);
6495 }
6496}
6497
6498#[gpui::test]
6499async fn test_rewrap_block_comments(cx: &mut TestAppContext) {
6500 init_test(cx, |settings| {
6501 settings.languages.0.extend([(
6502 "Rust".into(),
6503 LanguageSettingsContent {
6504 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6505 preferred_line_length: Some(40),
6506 ..Default::default()
6507 },
6508 )])
6509 });
6510
6511 let mut cx = EditorTestContext::new(cx).await;
6512
6513 let rust_lang = Arc::new(
6514 Language::new(
6515 LanguageConfig {
6516 name: "Rust".into(),
6517 line_comments: vec!["// ".into()],
6518 block_comment: Some(BlockCommentConfig {
6519 start: "/*".into(),
6520 end: "*/".into(),
6521 prefix: "* ".into(),
6522 tab_size: 1,
6523 }),
6524 documentation_comment: Some(BlockCommentConfig {
6525 start: "/**".into(),
6526 end: "*/".into(),
6527 prefix: "* ".into(),
6528 tab_size: 1,
6529 }),
6530
6531 ..LanguageConfig::default()
6532 },
6533 Some(tree_sitter_rust::LANGUAGE.into()),
6534 )
6535 .with_override_query("[(line_comment) (block_comment)] @comment.inclusive")
6536 .unwrap(),
6537 );
6538
6539 // regular block comment
6540 assert_rewrap(
6541 indoc! {"
6542 /*
6543 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6544 */
6545 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6546 "},
6547 indoc! {"
6548 /*
6549 *ˇ Lorem ipsum dolor sit amet,
6550 * consectetur adipiscing elit.
6551 */
6552 /*
6553 *ˇ Lorem ipsum dolor sit amet,
6554 * consectetur adipiscing elit.
6555 */
6556 "},
6557 rust_lang.clone(),
6558 &mut cx,
6559 );
6560
6561 // indent is respected
6562 assert_rewrap(
6563 indoc! {"
6564 {}
6565 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6566 "},
6567 indoc! {"
6568 {}
6569 /*
6570 *ˇ Lorem ipsum dolor sit amet,
6571 * consectetur adipiscing elit.
6572 */
6573 "},
6574 rust_lang.clone(),
6575 &mut cx,
6576 );
6577
6578 // short block comments with inline delimiters
6579 assert_rewrap(
6580 indoc! {"
6581 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6582 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6583 */
6584 /*
6585 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6586 "},
6587 indoc! {"
6588 /*
6589 *ˇ Lorem ipsum dolor sit amet,
6590 * consectetur adipiscing elit.
6591 */
6592 /*
6593 *ˇ Lorem ipsum dolor sit amet,
6594 * consectetur adipiscing elit.
6595 */
6596 /*
6597 *ˇ Lorem ipsum dolor sit amet,
6598 * consectetur adipiscing elit.
6599 */
6600 "},
6601 rust_lang.clone(),
6602 &mut cx,
6603 );
6604
6605 // multiline block comment with inline start/end delimiters
6606 assert_rewrap(
6607 indoc! {"
6608 /*ˇ Lorem ipsum dolor sit amet,
6609 * consectetur adipiscing elit. */
6610 "},
6611 indoc! {"
6612 /*
6613 *ˇ Lorem ipsum dolor sit amet,
6614 * consectetur adipiscing elit.
6615 */
6616 "},
6617 rust_lang.clone(),
6618 &mut cx,
6619 );
6620
6621 // block comment rewrap still respects paragraph bounds
6622 assert_rewrap(
6623 indoc! {"
6624 /*
6625 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6626 *
6627 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6628 */
6629 "},
6630 indoc! {"
6631 /*
6632 *ˇ Lorem ipsum dolor sit amet,
6633 * consectetur adipiscing elit.
6634 *
6635 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6636 */
6637 "},
6638 rust_lang.clone(),
6639 &mut cx,
6640 );
6641
6642 // documentation comments
6643 assert_rewrap(
6644 indoc! {"
6645 /**ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6646 /**
6647 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6648 */
6649 "},
6650 indoc! {"
6651 /**
6652 *ˇ Lorem ipsum dolor sit amet,
6653 * consectetur adipiscing elit.
6654 */
6655 /**
6656 *ˇ Lorem ipsum dolor sit amet,
6657 * consectetur adipiscing elit.
6658 */
6659 "},
6660 rust_lang.clone(),
6661 &mut cx,
6662 );
6663
6664 // different, adjacent comments
6665 assert_rewrap(
6666 indoc! {"
6667 /**
6668 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6669 */
6670 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6671 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6672 "},
6673 indoc! {"
6674 /**
6675 *ˇ Lorem ipsum dolor sit amet,
6676 * consectetur adipiscing elit.
6677 */
6678 /*
6679 *ˇ Lorem ipsum dolor sit amet,
6680 * consectetur adipiscing elit.
6681 */
6682 //ˇ Lorem ipsum dolor sit amet,
6683 // consectetur adipiscing elit.
6684 "},
6685 rust_lang.clone(),
6686 &mut cx,
6687 );
6688
6689 // selection w/ single short block comment
6690 assert_rewrap(
6691 indoc! {"
6692 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6693 "},
6694 indoc! {"
6695 «/*
6696 * Lorem ipsum dolor sit amet,
6697 * consectetur adipiscing elit.
6698 */ˇ»
6699 "},
6700 rust_lang.clone(),
6701 &mut cx,
6702 );
6703
6704 // rewrapping a single comment w/ abutting comments
6705 assert_rewrap(
6706 indoc! {"
6707 /* ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. */
6708 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6709 "},
6710 indoc! {"
6711 /*
6712 * ˇLorem ipsum dolor sit amet,
6713 * consectetur adipiscing elit.
6714 */
6715 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6716 "},
6717 rust_lang.clone(),
6718 &mut cx,
6719 );
6720
6721 // selection w/ non-abutting short block comments
6722 assert_rewrap(
6723 indoc! {"
6724 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6725
6726 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6727 "},
6728 indoc! {"
6729 «/*
6730 * Lorem ipsum dolor sit amet,
6731 * consectetur adipiscing elit.
6732 */
6733
6734 /*
6735 * Lorem ipsum dolor sit amet,
6736 * consectetur adipiscing elit.
6737 */ˇ»
6738 "},
6739 rust_lang.clone(),
6740 &mut cx,
6741 );
6742
6743 // selection of multiline block comments
6744 assert_rewrap(
6745 indoc! {"
6746 «/* Lorem ipsum dolor sit amet,
6747 * consectetur adipiscing elit. */ˇ»
6748 "},
6749 indoc! {"
6750 «/*
6751 * Lorem ipsum dolor sit amet,
6752 * consectetur adipiscing elit.
6753 */ˇ»
6754 "},
6755 rust_lang.clone(),
6756 &mut cx,
6757 );
6758
6759 // partial selection of multiline block comments
6760 assert_rewrap(
6761 indoc! {"
6762 «/* Lorem ipsum dolor sit amet,ˇ»
6763 * consectetur adipiscing elit. */
6764 /* Lorem ipsum dolor sit amet,
6765 «* consectetur adipiscing elit. */ˇ»
6766 "},
6767 indoc! {"
6768 «/*
6769 * Lorem ipsum dolor sit amet,ˇ»
6770 * consectetur adipiscing elit. */
6771 /* Lorem ipsum dolor sit amet,
6772 «* consectetur adipiscing elit.
6773 */ˇ»
6774 "},
6775 rust_lang.clone(),
6776 &mut cx,
6777 );
6778
6779 // selection w/ abutting short block comments
6780 // TODO: should not be combined; should rewrap as 2 comments
6781 assert_rewrap(
6782 indoc! {"
6783 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6784 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6785 "},
6786 // desired behavior:
6787 // indoc! {"
6788 // «/*
6789 // * Lorem ipsum dolor sit amet,
6790 // * consectetur adipiscing elit.
6791 // */
6792 // /*
6793 // * Lorem ipsum dolor sit amet,
6794 // * consectetur adipiscing elit.
6795 // */ˇ»
6796 // "},
6797 // actual behaviour:
6798 indoc! {"
6799 «/*
6800 * Lorem ipsum dolor sit amet,
6801 * consectetur adipiscing elit. Lorem
6802 * ipsum dolor sit amet, consectetur
6803 * adipiscing elit.
6804 */ˇ»
6805 "},
6806 rust_lang.clone(),
6807 &mut cx,
6808 );
6809
6810 // TODO: same as above, but with delimiters on separate line
6811 // assert_rewrap(
6812 // indoc! {"
6813 // «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6814 // */
6815 // /*
6816 // * Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6817 // "},
6818 // // desired:
6819 // // indoc! {"
6820 // // «/*
6821 // // * Lorem ipsum dolor sit amet,
6822 // // * consectetur adipiscing elit.
6823 // // */
6824 // // /*
6825 // // * Lorem ipsum dolor sit amet,
6826 // // * consectetur adipiscing elit.
6827 // // */ˇ»
6828 // // "},
6829 // // actual: (but with trailing w/s on the empty lines)
6830 // indoc! {"
6831 // «/*
6832 // * Lorem ipsum dolor sit amet,
6833 // * consectetur adipiscing elit.
6834 // *
6835 // */
6836 // /*
6837 // *
6838 // * Lorem ipsum dolor sit amet,
6839 // * consectetur adipiscing elit.
6840 // */ˇ»
6841 // "},
6842 // rust_lang.clone(),
6843 // &mut cx,
6844 // );
6845
6846 // TODO these are unhandled edge cases; not correct, just documenting known issues
6847 assert_rewrap(
6848 indoc! {"
6849 /*
6850 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6851 */
6852 /*
6853 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6854 /*ˇ Lorem ipsum dolor sit amet */ /* consectetur adipiscing elit. */
6855 "},
6856 // desired:
6857 // indoc! {"
6858 // /*
6859 // *ˇ Lorem ipsum dolor sit amet,
6860 // * consectetur adipiscing elit.
6861 // */
6862 // /*
6863 // *ˇ Lorem ipsum dolor sit amet,
6864 // * consectetur adipiscing elit.
6865 // */
6866 // /*
6867 // *ˇ Lorem ipsum dolor sit amet
6868 // */ /* consectetur adipiscing elit. */
6869 // "},
6870 // actual:
6871 indoc! {"
6872 /*
6873 //ˇ Lorem ipsum dolor sit amet,
6874 // consectetur adipiscing elit.
6875 */
6876 /*
6877 * //ˇ Lorem ipsum dolor sit amet,
6878 * consectetur adipiscing elit.
6879 */
6880 /*
6881 *ˇ Lorem ipsum dolor sit amet */ /*
6882 * consectetur adipiscing elit.
6883 */
6884 "},
6885 rust_lang,
6886 &mut cx,
6887 );
6888
6889 #[track_caller]
6890 fn assert_rewrap(
6891 unwrapped_text: &str,
6892 wrapped_text: &str,
6893 language: Arc<Language>,
6894 cx: &mut EditorTestContext,
6895 ) {
6896 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6897 cx.set_state(unwrapped_text);
6898 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6899 cx.assert_editor_state(wrapped_text);
6900 }
6901}
6902
6903#[gpui::test]
6904async fn test_hard_wrap(cx: &mut TestAppContext) {
6905 init_test(cx, |_| {});
6906 let mut cx = EditorTestContext::new(cx).await;
6907
6908 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
6909 cx.update_editor(|editor, _, cx| {
6910 editor.set_hard_wrap(Some(14), cx);
6911 });
6912
6913 cx.set_state(indoc!(
6914 "
6915 one two three ˇ
6916 "
6917 ));
6918 cx.simulate_input("four");
6919 cx.run_until_parked();
6920
6921 cx.assert_editor_state(indoc!(
6922 "
6923 one two three
6924 fourˇ
6925 "
6926 ));
6927
6928 cx.update_editor(|editor, window, cx| {
6929 editor.newline(&Default::default(), window, cx);
6930 });
6931 cx.run_until_parked();
6932 cx.assert_editor_state(indoc!(
6933 "
6934 one two three
6935 four
6936 ˇ
6937 "
6938 ));
6939
6940 cx.simulate_input("five");
6941 cx.run_until_parked();
6942 cx.assert_editor_state(indoc!(
6943 "
6944 one two three
6945 four
6946 fiveˇ
6947 "
6948 ));
6949
6950 cx.update_editor(|editor, window, cx| {
6951 editor.newline(&Default::default(), window, cx);
6952 });
6953 cx.run_until_parked();
6954 cx.simulate_input("# ");
6955 cx.run_until_parked();
6956 cx.assert_editor_state(indoc!(
6957 "
6958 one two three
6959 four
6960 five
6961 # ˇ
6962 "
6963 ));
6964
6965 cx.update_editor(|editor, window, cx| {
6966 editor.newline(&Default::default(), window, cx);
6967 });
6968 cx.run_until_parked();
6969 cx.assert_editor_state(indoc!(
6970 "
6971 one two three
6972 four
6973 five
6974 #\x20
6975 #ˇ
6976 "
6977 ));
6978
6979 cx.simulate_input(" 6");
6980 cx.run_until_parked();
6981 cx.assert_editor_state(indoc!(
6982 "
6983 one two three
6984 four
6985 five
6986 #
6987 # 6ˇ
6988 "
6989 ));
6990}
6991
6992#[gpui::test]
6993async fn test_cut_line_ends(cx: &mut TestAppContext) {
6994 init_test(cx, |_| {});
6995
6996 let mut cx = EditorTestContext::new(cx).await;
6997
6998 cx.set_state(indoc! {"The quick brownˇ"});
6999 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
7000 cx.assert_editor_state(indoc! {"The quick brownˇ"});
7001
7002 cx.set_state(indoc! {"The emacs foxˇ"});
7003 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
7004 cx.assert_editor_state(indoc! {"The emacs foxˇ"});
7005
7006 cx.set_state(indoc! {"
7007 The quick« brownˇ»
7008 fox jumps overˇ
7009 the lazy dog"});
7010 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7011 cx.assert_editor_state(indoc! {"
7012 The quickˇ
7013 ˇthe lazy dog"});
7014
7015 cx.set_state(indoc! {"
7016 The quick« brownˇ»
7017 fox jumps overˇ
7018 the lazy dog"});
7019 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
7020 cx.assert_editor_state(indoc! {"
7021 The quickˇ
7022 fox jumps overˇthe lazy dog"});
7023
7024 cx.set_state(indoc! {"
7025 The quick« brownˇ»
7026 fox jumps overˇ
7027 the lazy dog"});
7028 cx.update_editor(|e, window, cx| {
7029 e.cut_to_end_of_line(
7030 &CutToEndOfLine {
7031 stop_at_newlines: true,
7032 },
7033 window,
7034 cx,
7035 )
7036 });
7037 cx.assert_editor_state(indoc! {"
7038 The quickˇ
7039 fox jumps overˇ
7040 the lazy dog"});
7041
7042 cx.set_state(indoc! {"
7043 The quick« brownˇ»
7044 fox jumps overˇ
7045 the lazy dog"});
7046 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
7047 cx.assert_editor_state(indoc! {"
7048 The quickˇ
7049 fox jumps overˇthe lazy dog"});
7050}
7051
7052#[gpui::test]
7053async fn test_clipboard(cx: &mut TestAppContext) {
7054 init_test(cx, |_| {});
7055
7056 let mut cx = EditorTestContext::new(cx).await;
7057
7058 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
7059 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7060 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
7061
7062 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
7063 cx.set_state("two ˇfour ˇsix ˇ");
7064 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7065 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
7066
7067 // Paste again but with only two cursors. Since the number of cursors doesn't
7068 // match the number of slices in the clipboard, the entire clipboard text
7069 // is pasted at each cursor.
7070 cx.set_state("ˇtwo one✅ four three six five ˇ");
7071 cx.update_editor(|e, window, cx| {
7072 e.handle_input("( ", window, cx);
7073 e.paste(&Paste, window, cx);
7074 e.handle_input(") ", window, cx);
7075 });
7076 cx.assert_editor_state(
7077 &([
7078 "( one✅ ",
7079 "three ",
7080 "five ) ˇtwo one✅ four three six five ( one✅ ",
7081 "three ",
7082 "five ) ˇ",
7083 ]
7084 .join("\n")),
7085 );
7086
7087 // Cut with three selections, one of which is full-line.
7088 cx.set_state(indoc! {"
7089 1«2ˇ»3
7090 4ˇ567
7091 «8ˇ»9"});
7092 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7093 cx.assert_editor_state(indoc! {"
7094 1ˇ3
7095 ˇ9"});
7096
7097 // Paste with three selections, noticing how the copied selection that was full-line
7098 // gets inserted before the second cursor.
7099 cx.set_state(indoc! {"
7100 1ˇ3
7101 9ˇ
7102 «oˇ»ne"});
7103 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7104 cx.assert_editor_state(indoc! {"
7105 12ˇ3
7106 4567
7107 9ˇ
7108 8ˇne"});
7109
7110 // Copy with a single cursor only, which writes the whole line into the clipboard.
7111 cx.set_state(indoc! {"
7112 The quick brown
7113 fox juˇmps over
7114 the lazy dog"});
7115 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7116 assert_eq!(
7117 cx.read_from_clipboard()
7118 .and_then(|item| item.text().as_deref().map(str::to_string)),
7119 Some("fox jumps over\n".to_string())
7120 );
7121
7122 // Paste with three selections, noticing how the copied full-line selection is inserted
7123 // before the empty selections but replaces the selection that is non-empty.
7124 cx.set_state(indoc! {"
7125 Tˇhe quick brown
7126 «foˇ»x jumps over
7127 tˇhe lazy dog"});
7128 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7129 cx.assert_editor_state(indoc! {"
7130 fox jumps over
7131 Tˇhe quick brown
7132 fox jumps over
7133 ˇx jumps over
7134 fox jumps over
7135 tˇhe lazy dog"});
7136}
7137
7138#[gpui::test]
7139async fn test_copy_trim(cx: &mut TestAppContext) {
7140 init_test(cx, |_| {});
7141
7142 let mut cx = EditorTestContext::new(cx).await;
7143 cx.set_state(
7144 r#" «for selection in selections.iter() {
7145 let mut start = selection.start;
7146 let mut end = selection.end;
7147 let is_entire_line = selection.is_empty();
7148 if is_entire_line {
7149 start = Point::new(start.row, 0);ˇ»
7150 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7151 }
7152 "#,
7153 );
7154 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7155 assert_eq!(
7156 cx.read_from_clipboard()
7157 .and_then(|item| item.text().as_deref().map(str::to_string)),
7158 Some(
7159 "for selection in selections.iter() {
7160 let mut start = selection.start;
7161 let mut end = selection.end;
7162 let is_entire_line = selection.is_empty();
7163 if is_entire_line {
7164 start = Point::new(start.row, 0);"
7165 .to_string()
7166 ),
7167 "Regular copying preserves all indentation selected",
7168 );
7169 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7170 assert_eq!(
7171 cx.read_from_clipboard()
7172 .and_then(|item| item.text().as_deref().map(str::to_string)),
7173 Some(
7174 "for selection in selections.iter() {
7175let mut start = selection.start;
7176let mut end = selection.end;
7177let is_entire_line = selection.is_empty();
7178if is_entire_line {
7179 start = Point::new(start.row, 0);"
7180 .to_string()
7181 ),
7182 "Copying with stripping should strip all leading whitespaces"
7183 );
7184
7185 cx.set_state(
7186 r#" « for selection in selections.iter() {
7187 let mut start = selection.start;
7188 let mut end = selection.end;
7189 let is_entire_line = selection.is_empty();
7190 if is_entire_line {
7191 start = Point::new(start.row, 0);ˇ»
7192 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7193 }
7194 "#,
7195 );
7196 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7197 assert_eq!(
7198 cx.read_from_clipboard()
7199 .and_then(|item| item.text().as_deref().map(str::to_string)),
7200 Some(
7201 " for selection in selections.iter() {
7202 let mut start = selection.start;
7203 let mut end = selection.end;
7204 let is_entire_line = selection.is_empty();
7205 if is_entire_line {
7206 start = Point::new(start.row, 0);"
7207 .to_string()
7208 ),
7209 "Regular copying preserves all indentation selected",
7210 );
7211 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7212 assert_eq!(
7213 cx.read_from_clipboard()
7214 .and_then(|item| item.text().as_deref().map(str::to_string)),
7215 Some(
7216 "for selection in selections.iter() {
7217let mut start = selection.start;
7218let mut end = selection.end;
7219let is_entire_line = selection.is_empty();
7220if is_entire_line {
7221 start = Point::new(start.row, 0);"
7222 .to_string()
7223 ),
7224 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
7225 );
7226
7227 cx.set_state(
7228 r#" «ˇ for selection in selections.iter() {
7229 let mut start = selection.start;
7230 let mut end = selection.end;
7231 let is_entire_line = selection.is_empty();
7232 if is_entire_line {
7233 start = Point::new(start.row, 0);»
7234 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7235 }
7236 "#,
7237 );
7238 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7239 assert_eq!(
7240 cx.read_from_clipboard()
7241 .and_then(|item| item.text().as_deref().map(str::to_string)),
7242 Some(
7243 " for selection in selections.iter() {
7244 let mut start = selection.start;
7245 let mut end = selection.end;
7246 let is_entire_line = selection.is_empty();
7247 if is_entire_line {
7248 start = Point::new(start.row, 0);"
7249 .to_string()
7250 ),
7251 "Regular copying for reverse selection works the same",
7252 );
7253 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7254 assert_eq!(
7255 cx.read_from_clipboard()
7256 .and_then(|item| item.text().as_deref().map(str::to_string)),
7257 Some(
7258 "for selection in selections.iter() {
7259let mut start = selection.start;
7260let mut end = selection.end;
7261let is_entire_line = selection.is_empty();
7262if is_entire_line {
7263 start = Point::new(start.row, 0);"
7264 .to_string()
7265 ),
7266 "Copying with stripping for reverse selection works the same"
7267 );
7268
7269 cx.set_state(
7270 r#" for selection «in selections.iter() {
7271 let mut start = selection.start;
7272 let mut end = selection.end;
7273 let is_entire_line = selection.is_empty();
7274 if is_entire_line {
7275 start = Point::new(start.row, 0);ˇ»
7276 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7277 }
7278 "#,
7279 );
7280 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7281 assert_eq!(
7282 cx.read_from_clipboard()
7283 .and_then(|item| item.text().as_deref().map(str::to_string)),
7284 Some(
7285 "in selections.iter() {
7286 let mut start = selection.start;
7287 let mut end = selection.end;
7288 let is_entire_line = selection.is_empty();
7289 if is_entire_line {
7290 start = Point::new(start.row, 0);"
7291 .to_string()
7292 ),
7293 "When selecting past the indent, the copying works as usual",
7294 );
7295 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7296 assert_eq!(
7297 cx.read_from_clipboard()
7298 .and_then(|item| item.text().as_deref().map(str::to_string)),
7299 Some(
7300 "in selections.iter() {
7301 let mut start = selection.start;
7302 let mut end = selection.end;
7303 let is_entire_line = selection.is_empty();
7304 if is_entire_line {
7305 start = Point::new(start.row, 0);"
7306 .to_string()
7307 ),
7308 "When selecting past the indent, nothing is trimmed"
7309 );
7310
7311 cx.set_state(
7312 r#" «for selection in selections.iter() {
7313 let mut start = selection.start;
7314
7315 let mut end = selection.end;
7316 let is_entire_line = selection.is_empty();
7317 if is_entire_line {
7318 start = Point::new(start.row, 0);
7319ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
7320 }
7321 "#,
7322 );
7323 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7324 assert_eq!(
7325 cx.read_from_clipboard()
7326 .and_then(|item| item.text().as_deref().map(str::to_string)),
7327 Some(
7328 "for selection in selections.iter() {
7329let mut start = selection.start;
7330
7331let mut end = selection.end;
7332let is_entire_line = selection.is_empty();
7333if is_entire_line {
7334 start = Point::new(start.row, 0);
7335"
7336 .to_string()
7337 ),
7338 "Copying with stripping should ignore empty lines"
7339 );
7340}
7341
7342#[gpui::test]
7343async fn test_paste_multiline(cx: &mut TestAppContext) {
7344 init_test(cx, |_| {});
7345
7346 let mut cx = EditorTestContext::new(cx).await;
7347 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7348
7349 // Cut an indented block, without the leading whitespace.
7350 cx.set_state(indoc! {"
7351 const a: B = (
7352 c(),
7353 «d(
7354 e,
7355 f
7356 )ˇ»
7357 );
7358 "});
7359 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7360 cx.assert_editor_state(indoc! {"
7361 const a: B = (
7362 c(),
7363 ˇ
7364 );
7365 "});
7366
7367 // Paste it at the same position.
7368 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7369 cx.assert_editor_state(indoc! {"
7370 const a: B = (
7371 c(),
7372 d(
7373 e,
7374 f
7375 )ˇ
7376 );
7377 "});
7378
7379 // Paste it at a line with a lower indent level.
7380 cx.set_state(indoc! {"
7381 ˇ
7382 const a: B = (
7383 c(),
7384 );
7385 "});
7386 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7387 cx.assert_editor_state(indoc! {"
7388 d(
7389 e,
7390 f
7391 )ˇ
7392 const a: B = (
7393 c(),
7394 );
7395 "});
7396
7397 // Cut an indented block, with the leading whitespace.
7398 cx.set_state(indoc! {"
7399 const a: B = (
7400 c(),
7401 « d(
7402 e,
7403 f
7404 )
7405 ˇ»);
7406 "});
7407 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7408 cx.assert_editor_state(indoc! {"
7409 const a: B = (
7410 c(),
7411 ˇ);
7412 "});
7413
7414 // Paste it at the same position.
7415 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7416 cx.assert_editor_state(indoc! {"
7417 const a: B = (
7418 c(),
7419 d(
7420 e,
7421 f
7422 )
7423 ˇ);
7424 "});
7425
7426 // Paste it at a line with a higher indent level.
7427 cx.set_state(indoc! {"
7428 const a: B = (
7429 c(),
7430 d(
7431 e,
7432 fˇ
7433 )
7434 );
7435 "});
7436 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7437 cx.assert_editor_state(indoc! {"
7438 const a: B = (
7439 c(),
7440 d(
7441 e,
7442 f d(
7443 e,
7444 f
7445 )
7446 ˇ
7447 )
7448 );
7449 "});
7450
7451 // Copy an indented block, starting mid-line
7452 cx.set_state(indoc! {"
7453 const a: B = (
7454 c(),
7455 somethin«g(
7456 e,
7457 f
7458 )ˇ»
7459 );
7460 "});
7461 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7462
7463 // Paste it on a line with a lower indent level
7464 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
7465 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7466 cx.assert_editor_state(indoc! {"
7467 const a: B = (
7468 c(),
7469 something(
7470 e,
7471 f
7472 )
7473 );
7474 g(
7475 e,
7476 f
7477 )ˇ"});
7478}
7479
7480#[gpui::test]
7481async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
7482 init_test(cx, |_| {});
7483
7484 cx.write_to_clipboard(ClipboardItem::new_string(
7485 " d(\n e\n );\n".into(),
7486 ));
7487
7488 let mut cx = EditorTestContext::new(cx).await;
7489 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7490
7491 cx.set_state(indoc! {"
7492 fn a() {
7493 b();
7494 if c() {
7495 ˇ
7496 }
7497 }
7498 "});
7499
7500 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7501 cx.assert_editor_state(indoc! {"
7502 fn a() {
7503 b();
7504 if c() {
7505 d(
7506 e
7507 );
7508 ˇ
7509 }
7510 }
7511 "});
7512
7513 cx.set_state(indoc! {"
7514 fn a() {
7515 b();
7516 ˇ
7517 }
7518 "});
7519
7520 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7521 cx.assert_editor_state(indoc! {"
7522 fn a() {
7523 b();
7524 d(
7525 e
7526 );
7527 ˇ
7528 }
7529 "});
7530}
7531
7532#[gpui::test]
7533fn test_select_all(cx: &mut TestAppContext) {
7534 init_test(cx, |_| {});
7535
7536 let editor = cx.add_window(|window, cx| {
7537 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
7538 build_editor(buffer, window, cx)
7539 });
7540 _ = editor.update(cx, |editor, window, cx| {
7541 editor.select_all(&SelectAll, window, cx);
7542 assert_eq!(
7543 editor.selections.display_ranges(cx),
7544 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
7545 );
7546 });
7547}
7548
7549#[gpui::test]
7550fn test_select_line(cx: &mut TestAppContext) {
7551 init_test(cx, |_| {});
7552
7553 let editor = cx.add_window(|window, cx| {
7554 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
7555 build_editor(buffer, window, cx)
7556 });
7557 _ = editor.update(cx, |editor, window, cx| {
7558 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7559 s.select_display_ranges([
7560 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7561 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7562 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7563 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
7564 ])
7565 });
7566 editor.select_line(&SelectLine, window, cx);
7567 assert_eq!(
7568 editor.selections.display_ranges(cx),
7569 vec![
7570 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
7571 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
7572 ]
7573 );
7574 });
7575
7576 _ = editor.update(cx, |editor, window, cx| {
7577 editor.select_line(&SelectLine, window, cx);
7578 assert_eq!(
7579 editor.selections.display_ranges(cx),
7580 vec![
7581 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
7582 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
7583 ]
7584 );
7585 });
7586
7587 _ = editor.update(cx, |editor, window, cx| {
7588 editor.select_line(&SelectLine, window, cx);
7589 assert_eq!(
7590 editor.selections.display_ranges(cx),
7591 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
7592 );
7593 });
7594}
7595
7596#[gpui::test]
7597async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
7598 init_test(cx, |_| {});
7599 let mut cx = EditorTestContext::new(cx).await;
7600
7601 #[track_caller]
7602 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
7603 cx.set_state(initial_state);
7604 cx.update_editor(|e, window, cx| {
7605 e.split_selection_into_lines(&Default::default(), window, cx)
7606 });
7607 cx.assert_editor_state(expected_state);
7608 }
7609
7610 // Selection starts and ends at the middle of lines, left-to-right
7611 test(
7612 &mut cx,
7613 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
7614 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7615 );
7616 // Same thing, right-to-left
7617 test(
7618 &mut cx,
7619 "aa\nb«b\ncc\ndd\neˇ»e\nff",
7620 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7621 );
7622
7623 // Whole buffer, left-to-right, last line *doesn't* end with newline
7624 test(
7625 &mut cx,
7626 "«ˇaa\nbb\ncc\ndd\nee\nff»",
7627 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7628 );
7629 // Same thing, right-to-left
7630 test(
7631 &mut cx,
7632 "«aa\nbb\ncc\ndd\nee\nffˇ»",
7633 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7634 );
7635
7636 // Whole buffer, left-to-right, last line ends with newline
7637 test(
7638 &mut cx,
7639 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
7640 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7641 );
7642 // Same thing, right-to-left
7643 test(
7644 &mut cx,
7645 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
7646 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7647 );
7648
7649 // Starts at the end of a line, ends at the start of another
7650 test(
7651 &mut cx,
7652 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
7653 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
7654 );
7655}
7656
7657#[gpui::test]
7658async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
7659 init_test(cx, |_| {});
7660
7661 let editor = cx.add_window(|window, cx| {
7662 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
7663 build_editor(buffer, window, cx)
7664 });
7665
7666 // setup
7667 _ = editor.update(cx, |editor, window, cx| {
7668 editor.fold_creases(
7669 vec![
7670 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
7671 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
7672 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
7673 ],
7674 true,
7675 window,
7676 cx,
7677 );
7678 assert_eq!(
7679 editor.display_text(cx),
7680 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7681 );
7682 });
7683
7684 _ = editor.update(cx, |editor, window, cx| {
7685 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7686 s.select_display_ranges([
7687 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7688 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7689 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7690 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
7691 ])
7692 });
7693 editor.split_selection_into_lines(&Default::default(), window, cx);
7694 assert_eq!(
7695 editor.display_text(cx),
7696 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7697 );
7698 });
7699 EditorTestContext::for_editor(editor, cx)
7700 .await
7701 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
7702
7703 _ = editor.update(cx, |editor, window, cx| {
7704 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7705 s.select_display_ranges([
7706 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
7707 ])
7708 });
7709 editor.split_selection_into_lines(&Default::default(), window, cx);
7710 assert_eq!(
7711 editor.display_text(cx),
7712 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
7713 );
7714 assert_eq!(
7715 editor.selections.display_ranges(cx),
7716 [
7717 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
7718 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
7719 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
7720 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
7721 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
7722 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
7723 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
7724 ]
7725 );
7726 });
7727 EditorTestContext::for_editor(editor, cx)
7728 .await
7729 .assert_editor_state(
7730 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
7731 );
7732}
7733
7734#[gpui::test]
7735async fn test_add_selection_above_below(cx: &mut TestAppContext) {
7736 init_test(cx, |_| {});
7737
7738 let mut cx = EditorTestContext::new(cx).await;
7739
7740 cx.set_state(indoc!(
7741 r#"abc
7742 defˇghi
7743
7744 jk
7745 nlmo
7746 "#
7747 ));
7748
7749 cx.update_editor(|editor, window, cx| {
7750 editor.add_selection_above(&Default::default(), window, cx);
7751 });
7752
7753 cx.assert_editor_state(indoc!(
7754 r#"abcˇ
7755 defˇghi
7756
7757 jk
7758 nlmo
7759 "#
7760 ));
7761
7762 cx.update_editor(|editor, window, cx| {
7763 editor.add_selection_above(&Default::default(), window, cx);
7764 });
7765
7766 cx.assert_editor_state(indoc!(
7767 r#"abcˇ
7768 defˇghi
7769
7770 jk
7771 nlmo
7772 "#
7773 ));
7774
7775 cx.update_editor(|editor, window, cx| {
7776 editor.add_selection_below(&Default::default(), window, cx);
7777 });
7778
7779 cx.assert_editor_state(indoc!(
7780 r#"abc
7781 defˇghi
7782
7783 jk
7784 nlmo
7785 "#
7786 ));
7787
7788 cx.update_editor(|editor, window, cx| {
7789 editor.undo_selection(&Default::default(), window, cx);
7790 });
7791
7792 cx.assert_editor_state(indoc!(
7793 r#"abcˇ
7794 defˇghi
7795
7796 jk
7797 nlmo
7798 "#
7799 ));
7800
7801 cx.update_editor(|editor, window, cx| {
7802 editor.redo_selection(&Default::default(), window, cx);
7803 });
7804
7805 cx.assert_editor_state(indoc!(
7806 r#"abc
7807 defˇghi
7808
7809 jk
7810 nlmo
7811 "#
7812 ));
7813
7814 cx.update_editor(|editor, window, cx| {
7815 editor.add_selection_below(&Default::default(), window, cx);
7816 });
7817
7818 cx.assert_editor_state(indoc!(
7819 r#"abc
7820 defˇghi
7821 ˇ
7822 jk
7823 nlmo
7824 "#
7825 ));
7826
7827 cx.update_editor(|editor, window, cx| {
7828 editor.add_selection_below(&Default::default(), window, cx);
7829 });
7830
7831 cx.assert_editor_state(indoc!(
7832 r#"abc
7833 defˇghi
7834 ˇ
7835 jkˇ
7836 nlmo
7837 "#
7838 ));
7839
7840 cx.update_editor(|editor, window, cx| {
7841 editor.add_selection_below(&Default::default(), window, cx);
7842 });
7843
7844 cx.assert_editor_state(indoc!(
7845 r#"abc
7846 defˇghi
7847 ˇ
7848 jkˇ
7849 nlmˇo
7850 "#
7851 ));
7852
7853 cx.update_editor(|editor, window, cx| {
7854 editor.add_selection_below(&Default::default(), window, cx);
7855 });
7856
7857 cx.assert_editor_state(indoc!(
7858 r#"abc
7859 defˇghi
7860 ˇ
7861 jkˇ
7862 nlmˇo
7863 ˇ"#
7864 ));
7865
7866 // change selections
7867 cx.set_state(indoc!(
7868 r#"abc
7869 def«ˇg»hi
7870
7871 jk
7872 nlmo
7873 "#
7874 ));
7875
7876 cx.update_editor(|editor, window, cx| {
7877 editor.add_selection_below(&Default::default(), window, cx);
7878 });
7879
7880 cx.assert_editor_state(indoc!(
7881 r#"abc
7882 def«ˇg»hi
7883
7884 jk
7885 nlm«ˇo»
7886 "#
7887 ));
7888
7889 cx.update_editor(|editor, window, cx| {
7890 editor.add_selection_below(&Default::default(), window, cx);
7891 });
7892
7893 cx.assert_editor_state(indoc!(
7894 r#"abc
7895 def«ˇg»hi
7896
7897 jk
7898 nlm«ˇo»
7899 "#
7900 ));
7901
7902 cx.update_editor(|editor, window, cx| {
7903 editor.add_selection_above(&Default::default(), window, cx);
7904 });
7905
7906 cx.assert_editor_state(indoc!(
7907 r#"abc
7908 def«ˇg»hi
7909
7910 jk
7911 nlmo
7912 "#
7913 ));
7914
7915 cx.update_editor(|editor, window, cx| {
7916 editor.add_selection_above(&Default::default(), window, cx);
7917 });
7918
7919 cx.assert_editor_state(indoc!(
7920 r#"abc
7921 def«ˇg»hi
7922
7923 jk
7924 nlmo
7925 "#
7926 ));
7927
7928 // Change selections again
7929 cx.set_state(indoc!(
7930 r#"a«bc
7931 defgˇ»hi
7932
7933 jk
7934 nlmo
7935 "#
7936 ));
7937
7938 cx.update_editor(|editor, window, cx| {
7939 editor.add_selection_below(&Default::default(), window, cx);
7940 });
7941
7942 cx.assert_editor_state(indoc!(
7943 r#"a«bcˇ»
7944 d«efgˇ»hi
7945
7946 j«kˇ»
7947 nlmo
7948 "#
7949 ));
7950
7951 cx.update_editor(|editor, window, cx| {
7952 editor.add_selection_below(&Default::default(), window, cx);
7953 });
7954 cx.assert_editor_state(indoc!(
7955 r#"a«bcˇ»
7956 d«efgˇ»hi
7957
7958 j«kˇ»
7959 n«lmoˇ»
7960 "#
7961 ));
7962 cx.update_editor(|editor, window, cx| {
7963 editor.add_selection_above(&Default::default(), window, cx);
7964 });
7965
7966 cx.assert_editor_state(indoc!(
7967 r#"a«bcˇ»
7968 d«efgˇ»hi
7969
7970 j«kˇ»
7971 nlmo
7972 "#
7973 ));
7974
7975 // Change selections again
7976 cx.set_state(indoc!(
7977 r#"abc
7978 d«ˇefghi
7979
7980 jk
7981 nlm»o
7982 "#
7983 ));
7984
7985 cx.update_editor(|editor, window, cx| {
7986 editor.add_selection_above(&Default::default(), window, cx);
7987 });
7988
7989 cx.assert_editor_state(indoc!(
7990 r#"a«ˇbc»
7991 d«ˇef»ghi
7992
7993 j«ˇk»
7994 n«ˇlm»o
7995 "#
7996 ));
7997
7998 cx.update_editor(|editor, window, cx| {
7999 editor.add_selection_below(&Default::default(), window, cx);
8000 });
8001
8002 cx.assert_editor_state(indoc!(
8003 r#"abc
8004 d«ˇef»ghi
8005
8006 j«ˇk»
8007 n«ˇlm»o
8008 "#
8009 ));
8010}
8011
8012#[gpui::test]
8013async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
8014 init_test(cx, |_| {});
8015 let mut cx = EditorTestContext::new(cx).await;
8016
8017 cx.set_state(indoc!(
8018 r#"line onˇe
8019 liˇne two
8020 line three
8021 line four"#
8022 ));
8023
8024 cx.update_editor(|editor, window, cx| {
8025 editor.add_selection_below(&Default::default(), window, cx);
8026 });
8027
8028 // test multiple cursors expand in the same direction
8029 cx.assert_editor_state(indoc!(
8030 r#"line onˇe
8031 liˇne twˇo
8032 liˇne three
8033 line four"#
8034 ));
8035
8036 cx.update_editor(|editor, window, cx| {
8037 editor.add_selection_below(&Default::default(), window, cx);
8038 });
8039
8040 cx.update_editor(|editor, window, cx| {
8041 editor.add_selection_below(&Default::default(), window, cx);
8042 });
8043
8044 // test multiple cursors expand below overflow
8045 cx.assert_editor_state(indoc!(
8046 r#"line onˇe
8047 liˇne twˇo
8048 liˇne thˇree
8049 liˇne foˇur"#
8050 ));
8051
8052 cx.update_editor(|editor, window, cx| {
8053 editor.add_selection_above(&Default::default(), window, cx);
8054 });
8055
8056 // test multiple cursors retrieves back correctly
8057 cx.assert_editor_state(indoc!(
8058 r#"line onˇe
8059 liˇne twˇo
8060 liˇne thˇree
8061 line four"#
8062 ));
8063
8064 cx.update_editor(|editor, window, cx| {
8065 editor.add_selection_above(&Default::default(), window, cx);
8066 });
8067
8068 cx.update_editor(|editor, window, cx| {
8069 editor.add_selection_above(&Default::default(), window, cx);
8070 });
8071
8072 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
8073 cx.assert_editor_state(indoc!(
8074 r#"liˇne onˇe
8075 liˇne two
8076 line three
8077 line four"#
8078 ));
8079
8080 cx.update_editor(|editor, window, cx| {
8081 editor.undo_selection(&Default::default(), window, cx);
8082 });
8083
8084 // test undo
8085 cx.assert_editor_state(indoc!(
8086 r#"line onˇe
8087 liˇne twˇo
8088 line three
8089 line four"#
8090 ));
8091
8092 cx.update_editor(|editor, window, cx| {
8093 editor.redo_selection(&Default::default(), window, cx);
8094 });
8095
8096 // test redo
8097 cx.assert_editor_state(indoc!(
8098 r#"liˇne onˇe
8099 liˇne two
8100 line three
8101 line four"#
8102 ));
8103
8104 cx.set_state(indoc!(
8105 r#"abcd
8106 ef«ghˇ»
8107 ijkl
8108 «mˇ»nop"#
8109 ));
8110
8111 cx.update_editor(|editor, window, cx| {
8112 editor.add_selection_above(&Default::default(), window, cx);
8113 });
8114
8115 // test multiple selections expand in the same direction
8116 cx.assert_editor_state(indoc!(
8117 r#"ab«cdˇ»
8118 ef«ghˇ»
8119 «iˇ»jkl
8120 «mˇ»nop"#
8121 ));
8122
8123 cx.update_editor(|editor, window, cx| {
8124 editor.add_selection_above(&Default::default(), window, cx);
8125 });
8126
8127 // test multiple selection upward overflow
8128 cx.assert_editor_state(indoc!(
8129 r#"ab«cdˇ»
8130 «eˇ»f«ghˇ»
8131 «iˇ»jkl
8132 «mˇ»nop"#
8133 ));
8134
8135 cx.update_editor(|editor, window, cx| {
8136 editor.add_selection_below(&Default::default(), window, cx);
8137 });
8138
8139 // test multiple selection retrieves back correctly
8140 cx.assert_editor_state(indoc!(
8141 r#"abcd
8142 ef«ghˇ»
8143 «iˇ»jkl
8144 «mˇ»nop"#
8145 ));
8146
8147 cx.update_editor(|editor, window, cx| {
8148 editor.add_selection_below(&Default::default(), window, cx);
8149 });
8150
8151 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
8152 cx.assert_editor_state(indoc!(
8153 r#"abcd
8154 ef«ghˇ»
8155 ij«klˇ»
8156 «mˇ»nop"#
8157 ));
8158
8159 cx.update_editor(|editor, window, cx| {
8160 editor.undo_selection(&Default::default(), window, cx);
8161 });
8162
8163 // test undo
8164 cx.assert_editor_state(indoc!(
8165 r#"abcd
8166 ef«ghˇ»
8167 «iˇ»jkl
8168 «mˇ»nop"#
8169 ));
8170
8171 cx.update_editor(|editor, window, cx| {
8172 editor.redo_selection(&Default::default(), window, cx);
8173 });
8174
8175 // test redo
8176 cx.assert_editor_state(indoc!(
8177 r#"abcd
8178 ef«ghˇ»
8179 ij«klˇ»
8180 «mˇ»nop"#
8181 ));
8182}
8183
8184#[gpui::test]
8185async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
8186 init_test(cx, |_| {});
8187 let mut cx = EditorTestContext::new(cx).await;
8188
8189 cx.set_state(indoc!(
8190 r#"line onˇe
8191 liˇne two
8192 line three
8193 line four"#
8194 ));
8195
8196 cx.update_editor(|editor, window, cx| {
8197 editor.add_selection_below(&Default::default(), window, cx);
8198 editor.add_selection_below(&Default::default(), window, cx);
8199 editor.add_selection_below(&Default::default(), window, cx);
8200 });
8201
8202 // initial state with two multi cursor groups
8203 cx.assert_editor_state(indoc!(
8204 r#"line onˇe
8205 liˇne twˇo
8206 liˇne thˇree
8207 liˇne foˇur"#
8208 ));
8209
8210 // add single cursor in middle - simulate opt click
8211 cx.update_editor(|editor, window, cx| {
8212 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
8213 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8214 editor.end_selection(window, cx);
8215 });
8216
8217 cx.assert_editor_state(indoc!(
8218 r#"line onˇe
8219 liˇne twˇo
8220 liˇneˇ thˇree
8221 liˇne foˇur"#
8222 ));
8223
8224 cx.update_editor(|editor, window, cx| {
8225 editor.add_selection_above(&Default::default(), window, cx);
8226 });
8227
8228 // test new added selection expands above and existing selection shrinks
8229 cx.assert_editor_state(indoc!(
8230 r#"line onˇe
8231 liˇneˇ twˇo
8232 liˇneˇ thˇree
8233 line four"#
8234 ));
8235
8236 cx.update_editor(|editor, window, cx| {
8237 editor.add_selection_above(&Default::default(), window, cx);
8238 });
8239
8240 // test new added selection expands above and existing selection shrinks
8241 cx.assert_editor_state(indoc!(
8242 r#"lineˇ onˇe
8243 liˇneˇ twˇo
8244 lineˇ three
8245 line four"#
8246 ));
8247
8248 // intial state with two selection groups
8249 cx.set_state(indoc!(
8250 r#"abcd
8251 ef«ghˇ»
8252 ijkl
8253 «mˇ»nop"#
8254 ));
8255
8256 cx.update_editor(|editor, window, cx| {
8257 editor.add_selection_above(&Default::default(), window, cx);
8258 editor.add_selection_above(&Default::default(), window, cx);
8259 });
8260
8261 cx.assert_editor_state(indoc!(
8262 r#"ab«cdˇ»
8263 «eˇ»f«ghˇ»
8264 «iˇ»jkl
8265 «mˇ»nop"#
8266 ));
8267
8268 // add single selection in middle - simulate opt drag
8269 cx.update_editor(|editor, window, cx| {
8270 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
8271 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8272 editor.update_selection(
8273 DisplayPoint::new(DisplayRow(2), 4),
8274 0,
8275 gpui::Point::<f32>::default(),
8276 window,
8277 cx,
8278 );
8279 editor.end_selection(window, cx);
8280 });
8281
8282 cx.assert_editor_state(indoc!(
8283 r#"ab«cdˇ»
8284 «eˇ»f«ghˇ»
8285 «iˇ»jk«lˇ»
8286 «mˇ»nop"#
8287 ));
8288
8289 cx.update_editor(|editor, window, cx| {
8290 editor.add_selection_below(&Default::default(), window, cx);
8291 });
8292
8293 // test new added selection expands below, others shrinks from above
8294 cx.assert_editor_state(indoc!(
8295 r#"abcd
8296 ef«ghˇ»
8297 «iˇ»jk«lˇ»
8298 «mˇ»no«pˇ»"#
8299 ));
8300}
8301
8302#[gpui::test]
8303async fn test_select_next(cx: &mut TestAppContext) {
8304 init_test(cx, |_| {});
8305
8306 let mut cx = EditorTestContext::new(cx).await;
8307 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8308
8309 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8310 .unwrap();
8311 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8312
8313 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8314 .unwrap();
8315 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8316
8317 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8318 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8319
8320 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8321 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8322
8323 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8324 .unwrap();
8325 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8326
8327 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8328 .unwrap();
8329 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8330
8331 // Test selection direction should be preserved
8332 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8333
8334 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8335 .unwrap();
8336 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
8337}
8338
8339#[gpui::test]
8340async fn test_select_all_matches(cx: &mut TestAppContext) {
8341 init_test(cx, |_| {});
8342
8343 let mut cx = EditorTestContext::new(cx).await;
8344
8345 // Test caret-only selections
8346 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8347 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8348 .unwrap();
8349 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8350
8351 // Test left-to-right selections
8352 cx.set_state("abc\n«abcˇ»\nabc");
8353 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8354 .unwrap();
8355 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
8356
8357 // Test right-to-left selections
8358 cx.set_state("abc\n«ˇabc»\nabc");
8359 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8360 .unwrap();
8361 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
8362
8363 // Test selecting whitespace with caret selection
8364 cx.set_state("abc\nˇ abc\nabc");
8365 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8366 .unwrap();
8367 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8368
8369 // Test selecting whitespace with left-to-right selection
8370 cx.set_state("abc\n«ˇ »abc\nabc");
8371 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8372 .unwrap();
8373 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
8374
8375 // Test no matches with right-to-left selection
8376 cx.set_state("abc\n« ˇ»abc\nabc");
8377 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8378 .unwrap();
8379 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8380
8381 // Test with a single word and clip_at_line_ends=true (#29823)
8382 cx.set_state("aˇbc");
8383 cx.update_editor(|e, window, cx| {
8384 e.set_clip_at_line_ends(true, cx);
8385 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8386 e.set_clip_at_line_ends(false, cx);
8387 });
8388 cx.assert_editor_state("«abcˇ»");
8389}
8390
8391#[gpui::test]
8392async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
8393 init_test(cx, |_| {});
8394
8395 let mut cx = EditorTestContext::new(cx).await;
8396
8397 let large_body_1 = "\nd".repeat(200);
8398 let large_body_2 = "\ne".repeat(200);
8399
8400 cx.set_state(&format!(
8401 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
8402 ));
8403 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
8404 let scroll_position = editor.scroll_position(cx);
8405 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
8406 scroll_position
8407 });
8408
8409 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8410 .unwrap();
8411 cx.assert_editor_state(&format!(
8412 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
8413 ));
8414 let scroll_position_after_selection =
8415 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
8416 assert_eq!(
8417 initial_scroll_position, scroll_position_after_selection,
8418 "Scroll position should not change after selecting all matches"
8419 );
8420}
8421
8422#[gpui::test]
8423async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
8424 init_test(cx, |_| {});
8425
8426 let mut cx = EditorLspTestContext::new_rust(
8427 lsp::ServerCapabilities {
8428 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8429 ..Default::default()
8430 },
8431 cx,
8432 )
8433 .await;
8434
8435 cx.set_state(indoc! {"
8436 line 1
8437 line 2
8438 linˇe 3
8439 line 4
8440 line 5
8441 "});
8442
8443 // Make an edit
8444 cx.update_editor(|editor, window, cx| {
8445 editor.handle_input("X", window, cx);
8446 });
8447
8448 // Move cursor to a different position
8449 cx.update_editor(|editor, window, cx| {
8450 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8451 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
8452 });
8453 });
8454
8455 cx.assert_editor_state(indoc! {"
8456 line 1
8457 line 2
8458 linXe 3
8459 line 4
8460 liˇne 5
8461 "});
8462
8463 cx.lsp
8464 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8465 Ok(Some(vec![lsp::TextEdit::new(
8466 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8467 "PREFIX ".to_string(),
8468 )]))
8469 });
8470
8471 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
8472 .unwrap()
8473 .await
8474 .unwrap();
8475
8476 cx.assert_editor_state(indoc! {"
8477 PREFIX line 1
8478 line 2
8479 linXe 3
8480 line 4
8481 liˇne 5
8482 "});
8483
8484 // Undo formatting
8485 cx.update_editor(|editor, window, cx| {
8486 editor.undo(&Default::default(), window, cx);
8487 });
8488
8489 // Verify cursor moved back to position after edit
8490 cx.assert_editor_state(indoc! {"
8491 line 1
8492 line 2
8493 linXˇe 3
8494 line 4
8495 line 5
8496 "});
8497}
8498
8499#[gpui::test]
8500async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) {
8501 init_test(cx, |_| {});
8502
8503 let mut cx = EditorTestContext::new(cx).await;
8504
8505 let provider = cx.new(|_| FakeEditPredictionProvider::default());
8506 cx.update_editor(|editor, window, cx| {
8507 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
8508 });
8509
8510 cx.set_state(indoc! {"
8511 line 1
8512 line 2
8513 linˇe 3
8514 line 4
8515 line 5
8516 line 6
8517 line 7
8518 line 8
8519 line 9
8520 line 10
8521 "});
8522
8523 let snapshot = cx.buffer_snapshot();
8524 let edit_position = snapshot.anchor_after(Point::new(2, 4));
8525
8526 cx.update(|_, cx| {
8527 provider.update(cx, |provider, _| {
8528 provider.set_edit_prediction(Some(edit_prediction::EditPrediction::Local {
8529 id: None,
8530 edits: vec![(edit_position..edit_position, "X".into())],
8531 edit_preview: None,
8532 }))
8533 })
8534 });
8535
8536 cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
8537 cx.update_editor(|editor, window, cx| {
8538 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
8539 });
8540
8541 cx.assert_editor_state(indoc! {"
8542 line 1
8543 line 2
8544 lineXˇ 3
8545 line 4
8546 line 5
8547 line 6
8548 line 7
8549 line 8
8550 line 9
8551 line 10
8552 "});
8553
8554 cx.update_editor(|editor, window, cx| {
8555 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8556 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
8557 });
8558 });
8559
8560 cx.assert_editor_state(indoc! {"
8561 line 1
8562 line 2
8563 lineX 3
8564 line 4
8565 line 5
8566 line 6
8567 line 7
8568 line 8
8569 line 9
8570 liˇne 10
8571 "});
8572
8573 cx.update_editor(|editor, window, cx| {
8574 editor.undo(&Default::default(), window, cx);
8575 });
8576
8577 cx.assert_editor_state(indoc! {"
8578 line 1
8579 line 2
8580 lineˇ 3
8581 line 4
8582 line 5
8583 line 6
8584 line 7
8585 line 8
8586 line 9
8587 line 10
8588 "});
8589}
8590
8591#[gpui::test]
8592async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
8593 init_test(cx, |_| {});
8594
8595 let mut cx = EditorTestContext::new(cx).await;
8596 cx.set_state(
8597 r#"let foo = 2;
8598lˇet foo = 2;
8599let fooˇ = 2;
8600let foo = 2;
8601let foo = ˇ2;"#,
8602 );
8603
8604 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8605 .unwrap();
8606 cx.assert_editor_state(
8607 r#"let foo = 2;
8608«letˇ» foo = 2;
8609let «fooˇ» = 2;
8610let foo = 2;
8611let foo = «2ˇ»;"#,
8612 );
8613
8614 // noop for multiple selections with different contents
8615 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8616 .unwrap();
8617 cx.assert_editor_state(
8618 r#"let foo = 2;
8619«letˇ» foo = 2;
8620let «fooˇ» = 2;
8621let foo = 2;
8622let foo = «2ˇ»;"#,
8623 );
8624
8625 // Test last selection direction should be preserved
8626 cx.set_state(
8627 r#"let foo = 2;
8628let foo = 2;
8629let «fooˇ» = 2;
8630let «ˇfoo» = 2;
8631let foo = 2;"#,
8632 );
8633
8634 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8635 .unwrap();
8636 cx.assert_editor_state(
8637 r#"let foo = 2;
8638let foo = 2;
8639let «fooˇ» = 2;
8640let «ˇfoo» = 2;
8641let «ˇfoo» = 2;"#,
8642 );
8643}
8644
8645#[gpui::test]
8646async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
8647 init_test(cx, |_| {});
8648
8649 let mut cx =
8650 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
8651
8652 cx.assert_editor_state(indoc! {"
8653 ˇbbb
8654 ccc
8655
8656 bbb
8657 ccc
8658 "});
8659 cx.dispatch_action(SelectPrevious::default());
8660 cx.assert_editor_state(indoc! {"
8661 «bbbˇ»
8662 ccc
8663
8664 bbb
8665 ccc
8666 "});
8667 cx.dispatch_action(SelectPrevious::default());
8668 cx.assert_editor_state(indoc! {"
8669 «bbbˇ»
8670 ccc
8671
8672 «bbbˇ»
8673 ccc
8674 "});
8675}
8676
8677#[gpui::test]
8678async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
8679 init_test(cx, |_| {});
8680
8681 let mut cx = EditorTestContext::new(cx).await;
8682 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8683
8684 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8685 .unwrap();
8686 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8687
8688 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8689 .unwrap();
8690 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8691
8692 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8693 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8694
8695 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8696 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8697
8698 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8699 .unwrap();
8700 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
8701
8702 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8703 .unwrap();
8704 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8705}
8706
8707#[gpui::test]
8708async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
8709 init_test(cx, |_| {});
8710
8711 let mut cx = EditorTestContext::new(cx).await;
8712 cx.set_state("aˇ");
8713
8714 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8715 .unwrap();
8716 cx.assert_editor_state("«aˇ»");
8717 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8718 .unwrap();
8719 cx.assert_editor_state("«aˇ»");
8720}
8721
8722#[gpui::test]
8723async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
8724 init_test(cx, |_| {});
8725
8726 let mut cx = EditorTestContext::new(cx).await;
8727 cx.set_state(
8728 r#"let foo = 2;
8729lˇet foo = 2;
8730let fooˇ = 2;
8731let foo = 2;
8732let foo = ˇ2;"#,
8733 );
8734
8735 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8736 .unwrap();
8737 cx.assert_editor_state(
8738 r#"let foo = 2;
8739«letˇ» foo = 2;
8740let «fooˇ» = 2;
8741let foo = 2;
8742let foo = «2ˇ»;"#,
8743 );
8744
8745 // noop for multiple selections with different contents
8746 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8747 .unwrap();
8748 cx.assert_editor_state(
8749 r#"let foo = 2;
8750«letˇ» foo = 2;
8751let «fooˇ» = 2;
8752let foo = 2;
8753let foo = «2ˇ»;"#,
8754 );
8755}
8756
8757#[gpui::test]
8758async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
8759 init_test(cx, |_| {});
8760
8761 let mut cx = EditorTestContext::new(cx).await;
8762 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8763
8764 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8765 .unwrap();
8766 // selection direction is preserved
8767 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8768
8769 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8770 .unwrap();
8771 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8772
8773 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8774 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8775
8776 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8777 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8778
8779 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8780 .unwrap();
8781 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
8782
8783 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8784 .unwrap();
8785 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
8786}
8787
8788#[gpui::test]
8789async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
8790 init_test(cx, |_| {});
8791
8792 let language = Arc::new(Language::new(
8793 LanguageConfig::default(),
8794 Some(tree_sitter_rust::LANGUAGE.into()),
8795 ));
8796
8797 let text = r#"
8798 use mod1::mod2::{mod3, mod4};
8799
8800 fn fn_1(param1: bool, param2: &str) {
8801 let var1 = "text";
8802 }
8803 "#
8804 .unindent();
8805
8806 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8807 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8808 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8809
8810 editor
8811 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8812 .await;
8813
8814 editor.update_in(cx, |editor, window, cx| {
8815 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8816 s.select_display_ranges([
8817 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
8818 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
8819 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
8820 ]);
8821 });
8822 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8823 });
8824 editor.update(cx, |editor, cx| {
8825 assert_text_with_selections(
8826 editor,
8827 indoc! {r#"
8828 use mod1::mod2::{mod3, «mod4ˇ»};
8829
8830 fn fn_1«ˇ(param1: bool, param2: &str)» {
8831 let var1 = "«ˇtext»";
8832 }
8833 "#},
8834 cx,
8835 );
8836 });
8837
8838 editor.update_in(cx, |editor, window, cx| {
8839 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8840 });
8841 editor.update(cx, |editor, cx| {
8842 assert_text_with_selections(
8843 editor,
8844 indoc! {r#"
8845 use mod1::mod2::«{mod3, mod4}ˇ»;
8846
8847 «ˇfn fn_1(param1: bool, param2: &str) {
8848 let var1 = "text";
8849 }»
8850 "#},
8851 cx,
8852 );
8853 });
8854
8855 editor.update_in(cx, |editor, window, cx| {
8856 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8857 });
8858 assert_eq!(
8859 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
8860 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8861 );
8862
8863 // Trying to expand the selected syntax node one more time has no effect.
8864 editor.update_in(cx, |editor, window, cx| {
8865 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8866 });
8867 assert_eq!(
8868 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
8869 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8870 );
8871
8872 editor.update_in(cx, |editor, window, cx| {
8873 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8874 });
8875 editor.update(cx, |editor, cx| {
8876 assert_text_with_selections(
8877 editor,
8878 indoc! {r#"
8879 use mod1::mod2::«{mod3, mod4}ˇ»;
8880
8881 «ˇfn fn_1(param1: bool, param2: &str) {
8882 let var1 = "text";
8883 }»
8884 "#},
8885 cx,
8886 );
8887 });
8888
8889 editor.update_in(cx, |editor, window, cx| {
8890 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8891 });
8892 editor.update(cx, |editor, cx| {
8893 assert_text_with_selections(
8894 editor,
8895 indoc! {r#"
8896 use mod1::mod2::{mod3, «mod4ˇ»};
8897
8898 fn fn_1«ˇ(param1: bool, param2: &str)» {
8899 let var1 = "«ˇtext»";
8900 }
8901 "#},
8902 cx,
8903 );
8904 });
8905
8906 editor.update_in(cx, |editor, window, cx| {
8907 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8908 });
8909 editor.update(cx, |editor, cx| {
8910 assert_text_with_selections(
8911 editor,
8912 indoc! {r#"
8913 use mod1::mod2::{mod3, moˇd4};
8914
8915 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
8916 let var1 = "teˇxt";
8917 }
8918 "#},
8919 cx,
8920 );
8921 });
8922
8923 // Trying to shrink the selected syntax node one more time has no effect.
8924 editor.update_in(cx, |editor, window, cx| {
8925 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8926 });
8927 editor.update_in(cx, |editor, _, cx| {
8928 assert_text_with_selections(
8929 editor,
8930 indoc! {r#"
8931 use mod1::mod2::{mod3, moˇd4};
8932
8933 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
8934 let var1 = "teˇxt";
8935 }
8936 "#},
8937 cx,
8938 );
8939 });
8940
8941 // Ensure that we keep expanding the selection if the larger selection starts or ends within
8942 // a fold.
8943 editor.update_in(cx, |editor, window, cx| {
8944 editor.fold_creases(
8945 vec![
8946 Crease::simple(
8947 Point::new(0, 21)..Point::new(0, 24),
8948 FoldPlaceholder::test(),
8949 ),
8950 Crease::simple(
8951 Point::new(3, 20)..Point::new(3, 22),
8952 FoldPlaceholder::test(),
8953 ),
8954 ],
8955 true,
8956 window,
8957 cx,
8958 );
8959 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8960 });
8961 editor.update(cx, |editor, cx| {
8962 assert_text_with_selections(
8963 editor,
8964 indoc! {r#"
8965 use mod1::mod2::«{mod3, mod4}ˇ»;
8966
8967 fn fn_1«ˇ(param1: bool, param2: &str)» {
8968 let var1 = "«ˇtext»";
8969 }
8970 "#},
8971 cx,
8972 );
8973 });
8974}
8975
8976#[gpui::test]
8977async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
8978 init_test(cx, |_| {});
8979
8980 let language = Arc::new(Language::new(
8981 LanguageConfig::default(),
8982 Some(tree_sitter_rust::LANGUAGE.into()),
8983 ));
8984
8985 let text = "let a = 2;";
8986
8987 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8988 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8989 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8990
8991 editor
8992 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8993 .await;
8994
8995 // Test case 1: Cursor at end of word
8996 editor.update_in(cx, |editor, window, cx| {
8997 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8998 s.select_display_ranges([
8999 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
9000 ]);
9001 });
9002 });
9003 editor.update(cx, |editor, cx| {
9004 assert_text_with_selections(editor, "let aˇ = 2;", cx);
9005 });
9006 editor.update_in(cx, |editor, window, cx| {
9007 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9008 });
9009 editor.update(cx, |editor, cx| {
9010 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
9011 });
9012 editor.update_in(cx, |editor, window, cx| {
9013 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9014 });
9015 editor.update(cx, |editor, cx| {
9016 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
9017 });
9018
9019 // Test case 2: Cursor at end of statement
9020 editor.update_in(cx, |editor, window, cx| {
9021 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9022 s.select_display_ranges([
9023 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
9024 ]);
9025 });
9026 });
9027 editor.update(cx, |editor, cx| {
9028 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
9029 });
9030 editor.update_in(cx, |editor, window, cx| {
9031 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9032 });
9033 editor.update(cx, |editor, cx| {
9034 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
9035 });
9036}
9037
9038#[gpui::test]
9039async fn test_select_larger_syntax_node_for_cursor_at_symbol(cx: &mut TestAppContext) {
9040 init_test(cx, |_| {});
9041
9042 let language = Arc::new(Language::new(
9043 LanguageConfig {
9044 name: "JavaScript".into(),
9045 ..Default::default()
9046 },
9047 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
9048 ));
9049
9050 let text = r#"
9051 let a = {
9052 key: "value",
9053 };
9054 "#
9055 .unindent();
9056
9057 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9058 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9059 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9060
9061 editor
9062 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9063 .await;
9064
9065 // Test case 1: Cursor after '{'
9066 editor.update_in(cx, |editor, window, cx| {
9067 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9068 s.select_display_ranges([
9069 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 9)
9070 ]);
9071 });
9072 });
9073 editor.update(cx, |editor, cx| {
9074 assert_text_with_selections(
9075 editor,
9076 indoc! {r#"
9077 let a = {ˇ
9078 key: "value",
9079 };
9080 "#},
9081 cx,
9082 );
9083 });
9084 editor.update_in(cx, |editor, window, cx| {
9085 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9086 });
9087 editor.update(cx, |editor, cx| {
9088 assert_text_with_selections(
9089 editor,
9090 indoc! {r#"
9091 let a = «ˇ{
9092 key: "value",
9093 }»;
9094 "#},
9095 cx,
9096 );
9097 });
9098
9099 // Test case 2: Cursor after ':'
9100 editor.update_in(cx, |editor, window, cx| {
9101 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9102 s.select_display_ranges([
9103 DisplayPoint::new(DisplayRow(1), 8)..DisplayPoint::new(DisplayRow(1), 8)
9104 ]);
9105 });
9106 });
9107 editor.update(cx, |editor, cx| {
9108 assert_text_with_selections(
9109 editor,
9110 indoc! {r#"
9111 let a = {
9112 key:ˇ "value",
9113 };
9114 "#},
9115 cx,
9116 );
9117 });
9118 editor.update_in(cx, |editor, window, cx| {
9119 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9120 });
9121 editor.update(cx, |editor, cx| {
9122 assert_text_with_selections(
9123 editor,
9124 indoc! {r#"
9125 let a = {
9126 «ˇkey: "value"»,
9127 };
9128 "#},
9129 cx,
9130 );
9131 });
9132 editor.update_in(cx, |editor, window, cx| {
9133 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9134 });
9135 editor.update(cx, |editor, cx| {
9136 assert_text_with_selections(
9137 editor,
9138 indoc! {r#"
9139 let a = «ˇ{
9140 key: "value",
9141 }»;
9142 "#},
9143 cx,
9144 );
9145 });
9146
9147 // Test case 3: Cursor after ','
9148 editor.update_in(cx, |editor, window, cx| {
9149 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9150 s.select_display_ranges([
9151 DisplayPoint::new(DisplayRow(1), 17)..DisplayPoint::new(DisplayRow(1), 17)
9152 ]);
9153 });
9154 });
9155 editor.update(cx, |editor, cx| {
9156 assert_text_with_selections(
9157 editor,
9158 indoc! {r#"
9159 let a = {
9160 key: "value",ˇ
9161 };
9162 "#},
9163 cx,
9164 );
9165 });
9166 editor.update_in(cx, |editor, window, cx| {
9167 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9168 });
9169 editor.update(cx, |editor, cx| {
9170 assert_text_with_selections(
9171 editor,
9172 indoc! {r#"
9173 let a = «ˇ{
9174 key: "value",
9175 }»;
9176 "#},
9177 cx,
9178 );
9179 });
9180
9181 // Test case 4: Cursor after ';'
9182 editor.update_in(cx, |editor, window, cx| {
9183 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9184 s.select_display_ranges([
9185 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)
9186 ]);
9187 });
9188 });
9189 editor.update(cx, |editor, cx| {
9190 assert_text_with_selections(
9191 editor,
9192 indoc! {r#"
9193 let a = {
9194 key: "value",
9195 };ˇ
9196 "#},
9197 cx,
9198 );
9199 });
9200 editor.update_in(cx, |editor, window, cx| {
9201 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9202 });
9203 editor.update(cx, |editor, cx| {
9204 assert_text_with_selections(
9205 editor,
9206 indoc! {r#"
9207 «ˇlet a = {
9208 key: "value",
9209 };
9210 »"#},
9211 cx,
9212 );
9213 });
9214}
9215
9216#[gpui::test]
9217async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
9218 init_test(cx, |_| {});
9219
9220 let language = Arc::new(Language::new(
9221 LanguageConfig::default(),
9222 Some(tree_sitter_rust::LANGUAGE.into()),
9223 ));
9224
9225 let text = r#"
9226 use mod1::mod2::{mod3, mod4};
9227
9228 fn fn_1(param1: bool, param2: &str) {
9229 let var1 = "hello world";
9230 }
9231 "#
9232 .unindent();
9233
9234 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9235 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9236 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9237
9238 editor
9239 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9240 .await;
9241
9242 // Test 1: Cursor on a letter of a string word
9243 editor.update_in(cx, |editor, window, cx| {
9244 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9245 s.select_display_ranges([
9246 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
9247 ]);
9248 });
9249 });
9250 editor.update_in(cx, |editor, window, cx| {
9251 assert_text_with_selections(
9252 editor,
9253 indoc! {r#"
9254 use mod1::mod2::{mod3, mod4};
9255
9256 fn fn_1(param1: bool, param2: &str) {
9257 let var1 = "hˇello world";
9258 }
9259 "#},
9260 cx,
9261 );
9262 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9263 assert_text_with_selections(
9264 editor,
9265 indoc! {r#"
9266 use mod1::mod2::{mod3, mod4};
9267
9268 fn fn_1(param1: bool, param2: &str) {
9269 let var1 = "«ˇhello» world";
9270 }
9271 "#},
9272 cx,
9273 );
9274 });
9275
9276 // Test 2: Partial selection within a word
9277 editor.update_in(cx, |editor, window, cx| {
9278 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9279 s.select_display_ranges([
9280 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
9281 ]);
9282 });
9283 });
9284 editor.update_in(cx, |editor, window, cx| {
9285 assert_text_with_selections(
9286 editor,
9287 indoc! {r#"
9288 use mod1::mod2::{mod3, mod4};
9289
9290 fn fn_1(param1: bool, param2: &str) {
9291 let var1 = "h«elˇ»lo world";
9292 }
9293 "#},
9294 cx,
9295 );
9296 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9297 assert_text_with_selections(
9298 editor,
9299 indoc! {r#"
9300 use mod1::mod2::{mod3, mod4};
9301
9302 fn fn_1(param1: bool, param2: &str) {
9303 let var1 = "«ˇhello» world";
9304 }
9305 "#},
9306 cx,
9307 );
9308 });
9309
9310 // Test 3: Complete word already selected
9311 editor.update_in(cx, |editor, window, cx| {
9312 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9313 s.select_display_ranges([
9314 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
9315 ]);
9316 });
9317 });
9318 editor.update_in(cx, |editor, window, cx| {
9319 assert_text_with_selections(
9320 editor,
9321 indoc! {r#"
9322 use mod1::mod2::{mod3, mod4};
9323
9324 fn fn_1(param1: bool, param2: &str) {
9325 let var1 = "«helloˇ» world";
9326 }
9327 "#},
9328 cx,
9329 );
9330 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9331 assert_text_with_selections(
9332 editor,
9333 indoc! {r#"
9334 use mod1::mod2::{mod3, mod4};
9335
9336 fn fn_1(param1: bool, param2: &str) {
9337 let var1 = "«hello worldˇ»";
9338 }
9339 "#},
9340 cx,
9341 );
9342 });
9343
9344 // Test 4: Selection spanning across words
9345 editor.update_in(cx, |editor, window, cx| {
9346 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9347 s.select_display_ranges([
9348 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
9349 ]);
9350 });
9351 });
9352 editor.update_in(cx, |editor, window, cx| {
9353 assert_text_with_selections(
9354 editor,
9355 indoc! {r#"
9356 use mod1::mod2::{mod3, mod4};
9357
9358 fn fn_1(param1: bool, param2: &str) {
9359 let var1 = "hel«lo woˇ»rld";
9360 }
9361 "#},
9362 cx,
9363 );
9364 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9365 assert_text_with_selections(
9366 editor,
9367 indoc! {r#"
9368 use mod1::mod2::{mod3, mod4};
9369
9370 fn fn_1(param1: bool, param2: &str) {
9371 let var1 = "«ˇhello world»";
9372 }
9373 "#},
9374 cx,
9375 );
9376 });
9377
9378 // Test 5: Expansion beyond string
9379 editor.update_in(cx, |editor, window, cx| {
9380 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9381 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9382 assert_text_with_selections(
9383 editor,
9384 indoc! {r#"
9385 use mod1::mod2::{mod3, mod4};
9386
9387 fn fn_1(param1: bool, param2: &str) {
9388 «ˇlet var1 = "hello world";»
9389 }
9390 "#},
9391 cx,
9392 );
9393 });
9394}
9395
9396#[gpui::test]
9397async fn test_unwrap_syntax_nodes(cx: &mut gpui::TestAppContext) {
9398 init_test(cx, |_| {});
9399
9400 let mut cx = EditorTestContext::new(cx).await;
9401
9402 let language = Arc::new(Language::new(
9403 LanguageConfig::default(),
9404 Some(tree_sitter_rust::LANGUAGE.into()),
9405 ));
9406
9407 cx.update_buffer(|buffer, cx| {
9408 buffer.set_language(Some(language), cx);
9409 });
9410
9411 cx.set_state(indoc! { r#"use mod1::{mod2::{«mod3ˇ», mod4}, mod5::{mod6, «mod7ˇ»}};"# });
9412 cx.update_editor(|editor, window, cx| {
9413 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9414 });
9415
9416 cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# });
9417
9418 cx.set_state(indoc! { r#"fn a() {
9419 // what
9420 // a
9421 // ˇlong
9422 // method
9423 // I
9424 // sure
9425 // hope
9426 // it
9427 // works
9428 }"# });
9429
9430 let buffer = cx.update_multibuffer(|multibuffer, _| multibuffer.as_singleton().unwrap());
9431 let multi_buffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
9432 cx.update(|_, cx| {
9433 multi_buffer.update(cx, |multi_buffer, cx| {
9434 multi_buffer.set_excerpts_for_path(
9435 PathKey::for_buffer(&buffer, cx),
9436 buffer,
9437 [Point::new(1, 0)..Point::new(1, 0)],
9438 3,
9439 cx,
9440 );
9441 });
9442 });
9443
9444 let editor2 = cx.new_window_entity(|window, cx| {
9445 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
9446 });
9447
9448 let mut cx = EditorTestContext::for_editor_in(editor2, &mut cx).await;
9449 cx.update_editor(|editor, window, cx| {
9450 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
9451 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)]);
9452 })
9453 });
9454
9455 cx.assert_editor_state(indoc! { "
9456 fn a() {
9457 // what
9458 // a
9459 ˇ // long
9460 // method"});
9461
9462 cx.update_editor(|editor, window, cx| {
9463 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9464 });
9465
9466 // Although we could potentially make the action work when the syntax node
9467 // is half-hidden, it seems a bit dangerous as you can't easily tell what it
9468 // did. Maybe we could also expand the excerpt to contain the range?
9469 cx.assert_editor_state(indoc! { "
9470 fn a() {
9471 // what
9472 // a
9473 ˇ // long
9474 // method"});
9475}
9476
9477#[gpui::test]
9478async fn test_fold_function_bodies(cx: &mut TestAppContext) {
9479 init_test(cx, |_| {});
9480
9481 let base_text = r#"
9482 impl A {
9483 // this is an uncommitted comment
9484
9485 fn b() {
9486 c();
9487 }
9488
9489 // this is another uncommitted comment
9490
9491 fn d() {
9492 // e
9493 // f
9494 }
9495 }
9496
9497 fn g() {
9498 // h
9499 }
9500 "#
9501 .unindent();
9502
9503 let text = r#"
9504 ˇimpl A {
9505
9506 fn b() {
9507 c();
9508 }
9509
9510 fn d() {
9511 // e
9512 // f
9513 }
9514 }
9515
9516 fn g() {
9517 // h
9518 }
9519 "#
9520 .unindent();
9521
9522 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9523 cx.set_state(&text);
9524 cx.set_head_text(&base_text);
9525 cx.update_editor(|editor, window, cx| {
9526 editor.expand_all_diff_hunks(&Default::default(), window, cx);
9527 });
9528
9529 cx.assert_state_with_diff(
9530 "
9531 ˇimpl A {
9532 - // this is an uncommitted comment
9533
9534 fn b() {
9535 c();
9536 }
9537
9538 - // this is another uncommitted comment
9539 -
9540 fn d() {
9541 // e
9542 // f
9543 }
9544 }
9545
9546 fn g() {
9547 // h
9548 }
9549 "
9550 .unindent(),
9551 );
9552
9553 let expected_display_text = "
9554 impl A {
9555 // this is an uncommitted comment
9556
9557 fn b() {
9558 ⋯
9559 }
9560
9561 // this is another uncommitted comment
9562
9563 fn d() {
9564 ⋯
9565 }
9566 }
9567
9568 fn g() {
9569 ⋯
9570 }
9571 "
9572 .unindent();
9573
9574 cx.update_editor(|editor, window, cx| {
9575 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
9576 assert_eq!(editor.display_text(cx), expected_display_text);
9577 });
9578}
9579
9580#[gpui::test]
9581async fn test_autoindent(cx: &mut TestAppContext) {
9582 init_test(cx, |_| {});
9583
9584 let language = Arc::new(
9585 Language::new(
9586 LanguageConfig {
9587 brackets: BracketPairConfig {
9588 pairs: vec![
9589 BracketPair {
9590 start: "{".to_string(),
9591 end: "}".to_string(),
9592 close: false,
9593 surround: false,
9594 newline: true,
9595 },
9596 BracketPair {
9597 start: "(".to_string(),
9598 end: ")".to_string(),
9599 close: false,
9600 surround: false,
9601 newline: true,
9602 },
9603 ],
9604 ..Default::default()
9605 },
9606 ..Default::default()
9607 },
9608 Some(tree_sitter_rust::LANGUAGE.into()),
9609 )
9610 .with_indents_query(
9611 r#"
9612 (_ "(" ")" @end) @indent
9613 (_ "{" "}" @end) @indent
9614 "#,
9615 )
9616 .unwrap(),
9617 );
9618
9619 let text = "fn a() {}";
9620
9621 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9622 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9623 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9624 editor
9625 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9626 .await;
9627
9628 editor.update_in(cx, |editor, window, cx| {
9629 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9630 s.select_ranges([5..5, 8..8, 9..9])
9631 });
9632 editor.newline(&Newline, window, cx);
9633 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
9634 assert_eq!(
9635 editor.selections.ranges(&editor.display_snapshot(cx)),
9636 &[
9637 Point::new(1, 4)..Point::new(1, 4),
9638 Point::new(3, 4)..Point::new(3, 4),
9639 Point::new(5, 0)..Point::new(5, 0)
9640 ]
9641 );
9642 });
9643}
9644
9645#[gpui::test]
9646async fn test_autoindent_disabled(cx: &mut TestAppContext) {
9647 init_test(cx, |settings| settings.defaults.auto_indent = Some(false));
9648
9649 let language = Arc::new(
9650 Language::new(
9651 LanguageConfig {
9652 brackets: BracketPairConfig {
9653 pairs: vec![
9654 BracketPair {
9655 start: "{".to_string(),
9656 end: "}".to_string(),
9657 close: false,
9658 surround: false,
9659 newline: true,
9660 },
9661 BracketPair {
9662 start: "(".to_string(),
9663 end: ")".to_string(),
9664 close: false,
9665 surround: false,
9666 newline: true,
9667 },
9668 ],
9669 ..Default::default()
9670 },
9671 ..Default::default()
9672 },
9673 Some(tree_sitter_rust::LANGUAGE.into()),
9674 )
9675 .with_indents_query(
9676 r#"
9677 (_ "(" ")" @end) @indent
9678 (_ "{" "}" @end) @indent
9679 "#,
9680 )
9681 .unwrap(),
9682 );
9683
9684 let text = "fn a() {}";
9685
9686 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9687 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9688 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9689 editor
9690 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9691 .await;
9692
9693 editor.update_in(cx, |editor, window, cx| {
9694 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9695 s.select_ranges([5..5, 8..8, 9..9])
9696 });
9697 editor.newline(&Newline, window, cx);
9698 assert_eq!(
9699 editor.text(cx),
9700 indoc!(
9701 "
9702 fn a(
9703
9704 ) {
9705
9706 }
9707 "
9708 )
9709 );
9710 assert_eq!(
9711 editor.selections.ranges(&editor.display_snapshot(cx)),
9712 &[
9713 Point::new(1, 0)..Point::new(1, 0),
9714 Point::new(3, 0)..Point::new(3, 0),
9715 Point::new(5, 0)..Point::new(5, 0)
9716 ]
9717 );
9718 });
9719}
9720
9721#[gpui::test]
9722async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) {
9723 init_test(cx, |settings| {
9724 settings.defaults.auto_indent = Some(true);
9725 settings.languages.0.insert(
9726 "python".into(),
9727 LanguageSettingsContent {
9728 auto_indent: Some(false),
9729 ..Default::default()
9730 },
9731 );
9732 });
9733
9734 let mut cx = EditorTestContext::new(cx).await;
9735
9736 let injected_language = Arc::new(
9737 Language::new(
9738 LanguageConfig {
9739 brackets: BracketPairConfig {
9740 pairs: vec![
9741 BracketPair {
9742 start: "{".to_string(),
9743 end: "}".to_string(),
9744 close: false,
9745 surround: false,
9746 newline: true,
9747 },
9748 BracketPair {
9749 start: "(".to_string(),
9750 end: ")".to_string(),
9751 close: true,
9752 surround: false,
9753 newline: true,
9754 },
9755 ],
9756 ..Default::default()
9757 },
9758 name: "python".into(),
9759 ..Default::default()
9760 },
9761 Some(tree_sitter_python::LANGUAGE.into()),
9762 )
9763 .with_indents_query(
9764 r#"
9765 (_ "(" ")" @end) @indent
9766 (_ "{" "}" @end) @indent
9767 "#,
9768 )
9769 .unwrap(),
9770 );
9771
9772 let language = Arc::new(
9773 Language::new(
9774 LanguageConfig {
9775 brackets: BracketPairConfig {
9776 pairs: vec![
9777 BracketPair {
9778 start: "{".to_string(),
9779 end: "}".to_string(),
9780 close: false,
9781 surround: false,
9782 newline: true,
9783 },
9784 BracketPair {
9785 start: "(".to_string(),
9786 end: ")".to_string(),
9787 close: true,
9788 surround: false,
9789 newline: true,
9790 },
9791 ],
9792 ..Default::default()
9793 },
9794 name: LanguageName::new("rust"),
9795 ..Default::default()
9796 },
9797 Some(tree_sitter_rust::LANGUAGE.into()),
9798 )
9799 .with_indents_query(
9800 r#"
9801 (_ "(" ")" @end) @indent
9802 (_ "{" "}" @end) @indent
9803 "#,
9804 )
9805 .unwrap()
9806 .with_injection_query(
9807 r#"
9808 (macro_invocation
9809 macro: (identifier) @_macro_name
9810 (token_tree) @injection.content
9811 (#set! injection.language "python"))
9812 "#,
9813 )
9814 .unwrap(),
9815 );
9816
9817 cx.language_registry().add(injected_language);
9818 cx.language_registry().add(language.clone());
9819
9820 cx.update_buffer(|buffer, cx| {
9821 buffer.set_language(Some(language), cx);
9822 });
9823
9824 cx.set_state(r#"struct A {ˇ}"#);
9825
9826 cx.update_editor(|editor, window, cx| {
9827 editor.newline(&Default::default(), window, cx);
9828 });
9829
9830 cx.assert_editor_state(indoc!(
9831 "struct A {
9832 ˇ
9833 }"
9834 ));
9835
9836 cx.set_state(r#"select_biased!(ˇ)"#);
9837
9838 cx.update_editor(|editor, window, cx| {
9839 editor.newline(&Default::default(), window, cx);
9840 editor.handle_input("def ", window, cx);
9841 editor.handle_input("(", window, cx);
9842 editor.newline(&Default::default(), window, cx);
9843 editor.handle_input("a", window, cx);
9844 });
9845
9846 cx.assert_editor_state(indoc!(
9847 "select_biased!(
9848 def (
9849 aˇ
9850 )
9851 )"
9852 ));
9853}
9854
9855#[gpui::test]
9856async fn test_autoindent_selections(cx: &mut TestAppContext) {
9857 init_test(cx, |_| {});
9858
9859 {
9860 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9861 cx.set_state(indoc! {"
9862 impl A {
9863
9864 fn b() {}
9865
9866 «fn c() {
9867
9868 }ˇ»
9869 }
9870 "});
9871
9872 cx.update_editor(|editor, window, cx| {
9873 editor.autoindent(&Default::default(), window, cx);
9874 });
9875
9876 cx.assert_editor_state(indoc! {"
9877 impl A {
9878
9879 fn b() {}
9880
9881 «fn c() {
9882
9883 }ˇ»
9884 }
9885 "});
9886 }
9887
9888 {
9889 let mut cx = EditorTestContext::new_multibuffer(
9890 cx,
9891 [indoc! { "
9892 impl A {
9893 «
9894 // a
9895 fn b(){}
9896 »
9897 «
9898 }
9899 fn c(){}
9900 »
9901 "}],
9902 );
9903
9904 let buffer = cx.update_editor(|editor, _, cx| {
9905 let buffer = editor.buffer().update(cx, |buffer, _| {
9906 buffer.all_buffers().iter().next().unwrap().clone()
9907 });
9908 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
9909 buffer
9910 });
9911
9912 cx.run_until_parked();
9913 cx.update_editor(|editor, window, cx| {
9914 editor.select_all(&Default::default(), window, cx);
9915 editor.autoindent(&Default::default(), window, cx)
9916 });
9917 cx.run_until_parked();
9918
9919 cx.update(|_, cx| {
9920 assert_eq!(
9921 buffer.read(cx).text(),
9922 indoc! { "
9923 impl A {
9924
9925 // a
9926 fn b(){}
9927
9928
9929 }
9930 fn c(){}
9931
9932 " }
9933 )
9934 });
9935 }
9936}
9937
9938#[gpui::test]
9939async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
9940 init_test(cx, |_| {});
9941
9942 let mut cx = EditorTestContext::new(cx).await;
9943
9944 let language = Arc::new(Language::new(
9945 LanguageConfig {
9946 brackets: BracketPairConfig {
9947 pairs: vec![
9948 BracketPair {
9949 start: "{".to_string(),
9950 end: "}".to_string(),
9951 close: true,
9952 surround: true,
9953 newline: true,
9954 },
9955 BracketPair {
9956 start: "(".to_string(),
9957 end: ")".to_string(),
9958 close: true,
9959 surround: true,
9960 newline: true,
9961 },
9962 BracketPair {
9963 start: "/*".to_string(),
9964 end: " */".to_string(),
9965 close: true,
9966 surround: true,
9967 newline: true,
9968 },
9969 BracketPair {
9970 start: "[".to_string(),
9971 end: "]".to_string(),
9972 close: false,
9973 surround: false,
9974 newline: true,
9975 },
9976 BracketPair {
9977 start: "\"".to_string(),
9978 end: "\"".to_string(),
9979 close: true,
9980 surround: true,
9981 newline: false,
9982 },
9983 BracketPair {
9984 start: "<".to_string(),
9985 end: ">".to_string(),
9986 close: false,
9987 surround: true,
9988 newline: true,
9989 },
9990 ],
9991 ..Default::default()
9992 },
9993 autoclose_before: "})]".to_string(),
9994 ..Default::default()
9995 },
9996 Some(tree_sitter_rust::LANGUAGE.into()),
9997 ));
9998
9999 cx.language_registry().add(language.clone());
10000 cx.update_buffer(|buffer, cx| {
10001 buffer.set_language(Some(language), cx);
10002 });
10003
10004 cx.set_state(
10005 &r#"
10006 🏀ˇ
10007 εˇ
10008 ❤️ˇ
10009 "#
10010 .unindent(),
10011 );
10012
10013 // autoclose multiple nested brackets at multiple cursors
10014 cx.update_editor(|editor, window, cx| {
10015 editor.handle_input("{", window, cx);
10016 editor.handle_input("{", window, cx);
10017 editor.handle_input("{", window, cx);
10018 });
10019 cx.assert_editor_state(
10020 &"
10021 🏀{{{ˇ}}}
10022 ε{{{ˇ}}}
10023 ❤️{{{ˇ}}}
10024 "
10025 .unindent(),
10026 );
10027
10028 // insert a different closing bracket
10029 cx.update_editor(|editor, window, cx| {
10030 editor.handle_input(")", window, cx);
10031 });
10032 cx.assert_editor_state(
10033 &"
10034 🏀{{{)ˇ}}}
10035 ε{{{)ˇ}}}
10036 ❤️{{{)ˇ}}}
10037 "
10038 .unindent(),
10039 );
10040
10041 // skip over the auto-closed brackets when typing a closing bracket
10042 cx.update_editor(|editor, window, cx| {
10043 editor.move_right(&MoveRight, window, cx);
10044 editor.handle_input("}", window, cx);
10045 editor.handle_input("}", window, cx);
10046 editor.handle_input("}", window, cx);
10047 });
10048 cx.assert_editor_state(
10049 &"
10050 🏀{{{)}}}}ˇ
10051 ε{{{)}}}}ˇ
10052 ❤️{{{)}}}}ˇ
10053 "
10054 .unindent(),
10055 );
10056
10057 // autoclose multi-character pairs
10058 cx.set_state(
10059 &"
10060 ˇ
10061 ˇ
10062 "
10063 .unindent(),
10064 );
10065 cx.update_editor(|editor, window, cx| {
10066 editor.handle_input("/", window, cx);
10067 editor.handle_input("*", window, cx);
10068 });
10069 cx.assert_editor_state(
10070 &"
10071 /*ˇ */
10072 /*ˇ */
10073 "
10074 .unindent(),
10075 );
10076
10077 // one cursor autocloses a multi-character pair, one cursor
10078 // does not autoclose.
10079 cx.set_state(
10080 &"
10081 /ˇ
10082 ˇ
10083 "
10084 .unindent(),
10085 );
10086 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
10087 cx.assert_editor_state(
10088 &"
10089 /*ˇ */
10090 *ˇ
10091 "
10092 .unindent(),
10093 );
10094
10095 // Don't autoclose if the next character isn't whitespace and isn't
10096 // listed in the language's "autoclose_before" section.
10097 cx.set_state("ˇa b");
10098 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10099 cx.assert_editor_state("{ˇa b");
10100
10101 // Don't autoclose if `close` is false for the bracket pair
10102 cx.set_state("ˇ");
10103 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
10104 cx.assert_editor_state("[ˇ");
10105
10106 // Surround with brackets if text is selected
10107 cx.set_state("«aˇ» b");
10108 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10109 cx.assert_editor_state("{«aˇ»} b");
10110
10111 // Autoclose when not immediately after a word character
10112 cx.set_state("a ˇ");
10113 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10114 cx.assert_editor_state("a \"ˇ\"");
10115
10116 // Autoclose pair where the start and end characters are the same
10117 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10118 cx.assert_editor_state("a \"\"ˇ");
10119
10120 // Don't autoclose when immediately after a word character
10121 cx.set_state("aˇ");
10122 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10123 cx.assert_editor_state("a\"ˇ");
10124
10125 // Do autoclose when after a non-word character
10126 cx.set_state("{ˇ");
10127 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10128 cx.assert_editor_state("{\"ˇ\"");
10129
10130 // Non identical pairs autoclose regardless of preceding character
10131 cx.set_state("aˇ");
10132 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10133 cx.assert_editor_state("a{ˇ}");
10134
10135 // Don't autoclose pair if autoclose is disabled
10136 cx.set_state("ˇ");
10137 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
10138 cx.assert_editor_state("<ˇ");
10139
10140 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
10141 cx.set_state("«aˇ» b");
10142 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
10143 cx.assert_editor_state("<«aˇ»> b");
10144}
10145
10146#[gpui::test]
10147async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
10148 init_test(cx, |settings| {
10149 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10150 });
10151
10152 let mut cx = EditorTestContext::new(cx).await;
10153
10154 let language = Arc::new(Language::new(
10155 LanguageConfig {
10156 brackets: BracketPairConfig {
10157 pairs: vec![
10158 BracketPair {
10159 start: "{".to_string(),
10160 end: "}".to_string(),
10161 close: true,
10162 surround: true,
10163 newline: true,
10164 },
10165 BracketPair {
10166 start: "(".to_string(),
10167 end: ")".to_string(),
10168 close: true,
10169 surround: true,
10170 newline: true,
10171 },
10172 BracketPair {
10173 start: "[".to_string(),
10174 end: "]".to_string(),
10175 close: false,
10176 surround: false,
10177 newline: true,
10178 },
10179 ],
10180 ..Default::default()
10181 },
10182 autoclose_before: "})]".to_string(),
10183 ..Default::default()
10184 },
10185 Some(tree_sitter_rust::LANGUAGE.into()),
10186 ));
10187
10188 cx.language_registry().add(language.clone());
10189 cx.update_buffer(|buffer, cx| {
10190 buffer.set_language(Some(language), cx);
10191 });
10192
10193 cx.set_state(
10194 &"
10195 ˇ
10196 ˇ
10197 ˇ
10198 "
10199 .unindent(),
10200 );
10201
10202 // ensure only matching closing brackets are skipped over
10203 cx.update_editor(|editor, window, cx| {
10204 editor.handle_input("}", window, cx);
10205 editor.move_left(&MoveLeft, window, cx);
10206 editor.handle_input(")", window, cx);
10207 editor.move_left(&MoveLeft, window, cx);
10208 });
10209 cx.assert_editor_state(
10210 &"
10211 ˇ)}
10212 ˇ)}
10213 ˇ)}
10214 "
10215 .unindent(),
10216 );
10217
10218 // skip-over closing brackets at multiple cursors
10219 cx.update_editor(|editor, window, cx| {
10220 editor.handle_input(")", window, cx);
10221 editor.handle_input("}", window, cx);
10222 });
10223 cx.assert_editor_state(
10224 &"
10225 )}ˇ
10226 )}ˇ
10227 )}ˇ
10228 "
10229 .unindent(),
10230 );
10231
10232 // ignore non-close brackets
10233 cx.update_editor(|editor, window, cx| {
10234 editor.handle_input("]", window, cx);
10235 editor.move_left(&MoveLeft, window, cx);
10236 editor.handle_input("]", window, cx);
10237 });
10238 cx.assert_editor_state(
10239 &"
10240 )}]ˇ]
10241 )}]ˇ]
10242 )}]ˇ]
10243 "
10244 .unindent(),
10245 );
10246}
10247
10248#[gpui::test]
10249async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
10250 init_test(cx, |_| {});
10251
10252 let mut cx = EditorTestContext::new(cx).await;
10253
10254 let html_language = Arc::new(
10255 Language::new(
10256 LanguageConfig {
10257 name: "HTML".into(),
10258 brackets: BracketPairConfig {
10259 pairs: vec![
10260 BracketPair {
10261 start: "<".into(),
10262 end: ">".into(),
10263 close: true,
10264 ..Default::default()
10265 },
10266 BracketPair {
10267 start: "{".into(),
10268 end: "}".into(),
10269 close: true,
10270 ..Default::default()
10271 },
10272 BracketPair {
10273 start: "(".into(),
10274 end: ")".into(),
10275 close: true,
10276 ..Default::default()
10277 },
10278 ],
10279 ..Default::default()
10280 },
10281 autoclose_before: "})]>".into(),
10282 ..Default::default()
10283 },
10284 Some(tree_sitter_html::LANGUAGE.into()),
10285 )
10286 .with_injection_query(
10287 r#"
10288 (script_element
10289 (raw_text) @injection.content
10290 (#set! injection.language "javascript"))
10291 "#,
10292 )
10293 .unwrap(),
10294 );
10295
10296 let javascript_language = Arc::new(Language::new(
10297 LanguageConfig {
10298 name: "JavaScript".into(),
10299 brackets: BracketPairConfig {
10300 pairs: vec![
10301 BracketPair {
10302 start: "/*".into(),
10303 end: " */".into(),
10304 close: true,
10305 ..Default::default()
10306 },
10307 BracketPair {
10308 start: "{".into(),
10309 end: "}".into(),
10310 close: true,
10311 ..Default::default()
10312 },
10313 BracketPair {
10314 start: "(".into(),
10315 end: ")".into(),
10316 close: true,
10317 ..Default::default()
10318 },
10319 ],
10320 ..Default::default()
10321 },
10322 autoclose_before: "})]>".into(),
10323 ..Default::default()
10324 },
10325 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
10326 ));
10327
10328 cx.language_registry().add(html_language.clone());
10329 cx.language_registry().add(javascript_language);
10330 cx.executor().run_until_parked();
10331
10332 cx.update_buffer(|buffer, cx| {
10333 buffer.set_language(Some(html_language), cx);
10334 });
10335
10336 cx.set_state(
10337 &r#"
10338 <body>ˇ
10339 <script>
10340 var x = 1;ˇ
10341 </script>
10342 </body>ˇ
10343 "#
10344 .unindent(),
10345 );
10346
10347 // Precondition: different languages are active at different locations.
10348 cx.update_editor(|editor, window, cx| {
10349 let snapshot = editor.snapshot(window, cx);
10350 let cursors = editor
10351 .selections
10352 .ranges::<usize>(&editor.display_snapshot(cx));
10353 let languages = cursors
10354 .iter()
10355 .map(|c| snapshot.language_at(c.start).unwrap().name())
10356 .collect::<Vec<_>>();
10357 assert_eq!(
10358 languages,
10359 &["HTML".into(), "JavaScript".into(), "HTML".into()]
10360 );
10361 });
10362
10363 // Angle brackets autoclose in HTML, but not JavaScript.
10364 cx.update_editor(|editor, window, cx| {
10365 editor.handle_input("<", window, cx);
10366 editor.handle_input("a", window, cx);
10367 });
10368 cx.assert_editor_state(
10369 &r#"
10370 <body><aˇ>
10371 <script>
10372 var x = 1;<aˇ
10373 </script>
10374 </body><aˇ>
10375 "#
10376 .unindent(),
10377 );
10378
10379 // Curly braces and parens autoclose in both HTML and JavaScript.
10380 cx.update_editor(|editor, window, cx| {
10381 editor.handle_input(" b=", window, cx);
10382 editor.handle_input("{", window, cx);
10383 editor.handle_input("c", window, cx);
10384 editor.handle_input("(", window, cx);
10385 });
10386 cx.assert_editor_state(
10387 &r#"
10388 <body><a b={c(ˇ)}>
10389 <script>
10390 var x = 1;<a b={c(ˇ)}
10391 </script>
10392 </body><a b={c(ˇ)}>
10393 "#
10394 .unindent(),
10395 );
10396
10397 // Brackets that were already autoclosed are skipped.
10398 cx.update_editor(|editor, window, cx| {
10399 editor.handle_input(")", window, cx);
10400 editor.handle_input("d", window, cx);
10401 editor.handle_input("}", window, cx);
10402 });
10403 cx.assert_editor_state(
10404 &r#"
10405 <body><a b={c()d}ˇ>
10406 <script>
10407 var x = 1;<a b={c()d}ˇ
10408 </script>
10409 </body><a b={c()d}ˇ>
10410 "#
10411 .unindent(),
10412 );
10413 cx.update_editor(|editor, window, cx| {
10414 editor.handle_input(">", window, cx);
10415 });
10416 cx.assert_editor_state(
10417 &r#"
10418 <body><a b={c()d}>ˇ
10419 <script>
10420 var x = 1;<a b={c()d}>ˇ
10421 </script>
10422 </body><a b={c()d}>ˇ
10423 "#
10424 .unindent(),
10425 );
10426
10427 // Reset
10428 cx.set_state(
10429 &r#"
10430 <body>ˇ
10431 <script>
10432 var x = 1;ˇ
10433 </script>
10434 </body>ˇ
10435 "#
10436 .unindent(),
10437 );
10438
10439 cx.update_editor(|editor, window, cx| {
10440 editor.handle_input("<", window, cx);
10441 });
10442 cx.assert_editor_state(
10443 &r#"
10444 <body><ˇ>
10445 <script>
10446 var x = 1;<ˇ
10447 </script>
10448 </body><ˇ>
10449 "#
10450 .unindent(),
10451 );
10452
10453 // When backspacing, the closing angle brackets are removed.
10454 cx.update_editor(|editor, window, cx| {
10455 editor.backspace(&Backspace, window, cx);
10456 });
10457 cx.assert_editor_state(
10458 &r#"
10459 <body>ˇ
10460 <script>
10461 var x = 1;ˇ
10462 </script>
10463 </body>ˇ
10464 "#
10465 .unindent(),
10466 );
10467
10468 // Block comments autoclose in JavaScript, but not HTML.
10469 cx.update_editor(|editor, window, cx| {
10470 editor.handle_input("/", window, cx);
10471 editor.handle_input("*", window, cx);
10472 });
10473 cx.assert_editor_state(
10474 &r#"
10475 <body>/*ˇ
10476 <script>
10477 var x = 1;/*ˇ */
10478 </script>
10479 </body>/*ˇ
10480 "#
10481 .unindent(),
10482 );
10483}
10484
10485#[gpui::test]
10486async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
10487 init_test(cx, |_| {});
10488
10489 let mut cx = EditorTestContext::new(cx).await;
10490
10491 let rust_language = Arc::new(
10492 Language::new(
10493 LanguageConfig {
10494 name: "Rust".into(),
10495 brackets: serde_json::from_value(json!([
10496 { "start": "{", "end": "}", "close": true, "newline": true },
10497 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
10498 ]))
10499 .unwrap(),
10500 autoclose_before: "})]>".into(),
10501 ..Default::default()
10502 },
10503 Some(tree_sitter_rust::LANGUAGE.into()),
10504 )
10505 .with_override_query("(string_literal) @string")
10506 .unwrap(),
10507 );
10508
10509 cx.language_registry().add(rust_language.clone());
10510 cx.update_buffer(|buffer, cx| {
10511 buffer.set_language(Some(rust_language), cx);
10512 });
10513
10514 cx.set_state(
10515 &r#"
10516 let x = ˇ
10517 "#
10518 .unindent(),
10519 );
10520
10521 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
10522 cx.update_editor(|editor, window, cx| {
10523 editor.handle_input("\"", window, cx);
10524 });
10525 cx.assert_editor_state(
10526 &r#"
10527 let x = "ˇ"
10528 "#
10529 .unindent(),
10530 );
10531
10532 // Inserting another quotation mark. The cursor moves across the existing
10533 // automatically-inserted quotation mark.
10534 cx.update_editor(|editor, window, cx| {
10535 editor.handle_input("\"", window, cx);
10536 });
10537 cx.assert_editor_state(
10538 &r#"
10539 let x = ""ˇ
10540 "#
10541 .unindent(),
10542 );
10543
10544 // Reset
10545 cx.set_state(
10546 &r#"
10547 let x = ˇ
10548 "#
10549 .unindent(),
10550 );
10551
10552 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
10553 cx.update_editor(|editor, window, cx| {
10554 editor.handle_input("\"", window, cx);
10555 editor.handle_input(" ", window, cx);
10556 editor.move_left(&Default::default(), window, cx);
10557 editor.handle_input("\\", window, cx);
10558 editor.handle_input("\"", window, cx);
10559 });
10560 cx.assert_editor_state(
10561 &r#"
10562 let x = "\"ˇ "
10563 "#
10564 .unindent(),
10565 );
10566
10567 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
10568 // mark. Nothing is inserted.
10569 cx.update_editor(|editor, window, cx| {
10570 editor.move_right(&Default::default(), window, cx);
10571 editor.handle_input("\"", window, cx);
10572 });
10573 cx.assert_editor_state(
10574 &r#"
10575 let x = "\" "ˇ
10576 "#
10577 .unindent(),
10578 );
10579}
10580
10581#[gpui::test]
10582async fn test_surround_with_pair(cx: &mut TestAppContext) {
10583 init_test(cx, |_| {});
10584
10585 let language = Arc::new(Language::new(
10586 LanguageConfig {
10587 brackets: BracketPairConfig {
10588 pairs: vec![
10589 BracketPair {
10590 start: "{".to_string(),
10591 end: "}".to_string(),
10592 close: true,
10593 surround: true,
10594 newline: true,
10595 },
10596 BracketPair {
10597 start: "/* ".to_string(),
10598 end: "*/".to_string(),
10599 close: true,
10600 surround: true,
10601 ..Default::default()
10602 },
10603 ],
10604 ..Default::default()
10605 },
10606 ..Default::default()
10607 },
10608 Some(tree_sitter_rust::LANGUAGE.into()),
10609 ));
10610
10611 let text = r#"
10612 a
10613 b
10614 c
10615 "#
10616 .unindent();
10617
10618 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10619 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10620 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10621 editor
10622 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10623 .await;
10624
10625 editor.update_in(cx, |editor, window, cx| {
10626 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10627 s.select_display_ranges([
10628 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10629 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10630 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
10631 ])
10632 });
10633
10634 editor.handle_input("{", window, cx);
10635 editor.handle_input("{", window, cx);
10636 editor.handle_input("{", window, cx);
10637 assert_eq!(
10638 editor.text(cx),
10639 "
10640 {{{a}}}
10641 {{{b}}}
10642 {{{c}}}
10643 "
10644 .unindent()
10645 );
10646 assert_eq!(
10647 editor.selections.display_ranges(cx),
10648 [
10649 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
10650 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
10651 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
10652 ]
10653 );
10654
10655 editor.undo(&Undo, window, cx);
10656 editor.undo(&Undo, window, cx);
10657 editor.undo(&Undo, window, cx);
10658 assert_eq!(
10659 editor.text(cx),
10660 "
10661 a
10662 b
10663 c
10664 "
10665 .unindent()
10666 );
10667 assert_eq!(
10668 editor.selections.display_ranges(cx),
10669 [
10670 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10671 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10672 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10673 ]
10674 );
10675
10676 // Ensure inserting the first character of a multi-byte bracket pair
10677 // doesn't surround the selections with the bracket.
10678 editor.handle_input("/", window, cx);
10679 assert_eq!(
10680 editor.text(cx),
10681 "
10682 /
10683 /
10684 /
10685 "
10686 .unindent()
10687 );
10688 assert_eq!(
10689 editor.selections.display_ranges(cx),
10690 [
10691 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10692 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10693 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10694 ]
10695 );
10696
10697 editor.undo(&Undo, window, cx);
10698 assert_eq!(
10699 editor.text(cx),
10700 "
10701 a
10702 b
10703 c
10704 "
10705 .unindent()
10706 );
10707 assert_eq!(
10708 editor.selections.display_ranges(cx),
10709 [
10710 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10711 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10712 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10713 ]
10714 );
10715
10716 // Ensure inserting the last character of a multi-byte bracket pair
10717 // doesn't surround the selections with the bracket.
10718 editor.handle_input("*", window, cx);
10719 assert_eq!(
10720 editor.text(cx),
10721 "
10722 *
10723 *
10724 *
10725 "
10726 .unindent()
10727 );
10728 assert_eq!(
10729 editor.selections.display_ranges(cx),
10730 [
10731 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10732 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10733 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10734 ]
10735 );
10736 });
10737}
10738
10739#[gpui::test]
10740async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
10741 init_test(cx, |_| {});
10742
10743 let language = Arc::new(Language::new(
10744 LanguageConfig {
10745 brackets: BracketPairConfig {
10746 pairs: vec![BracketPair {
10747 start: "{".to_string(),
10748 end: "}".to_string(),
10749 close: true,
10750 surround: true,
10751 newline: true,
10752 }],
10753 ..Default::default()
10754 },
10755 autoclose_before: "}".to_string(),
10756 ..Default::default()
10757 },
10758 Some(tree_sitter_rust::LANGUAGE.into()),
10759 ));
10760
10761 let text = r#"
10762 a
10763 b
10764 c
10765 "#
10766 .unindent();
10767
10768 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10769 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10770 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10771 editor
10772 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10773 .await;
10774
10775 editor.update_in(cx, |editor, window, cx| {
10776 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10777 s.select_ranges([
10778 Point::new(0, 1)..Point::new(0, 1),
10779 Point::new(1, 1)..Point::new(1, 1),
10780 Point::new(2, 1)..Point::new(2, 1),
10781 ])
10782 });
10783
10784 editor.handle_input("{", window, cx);
10785 editor.handle_input("{", window, cx);
10786 editor.handle_input("_", window, cx);
10787 assert_eq!(
10788 editor.text(cx),
10789 "
10790 a{{_}}
10791 b{{_}}
10792 c{{_}}
10793 "
10794 .unindent()
10795 );
10796 assert_eq!(
10797 editor
10798 .selections
10799 .ranges::<Point>(&editor.display_snapshot(cx)),
10800 [
10801 Point::new(0, 4)..Point::new(0, 4),
10802 Point::new(1, 4)..Point::new(1, 4),
10803 Point::new(2, 4)..Point::new(2, 4)
10804 ]
10805 );
10806
10807 editor.backspace(&Default::default(), window, cx);
10808 editor.backspace(&Default::default(), window, cx);
10809 assert_eq!(
10810 editor.text(cx),
10811 "
10812 a{}
10813 b{}
10814 c{}
10815 "
10816 .unindent()
10817 );
10818 assert_eq!(
10819 editor
10820 .selections
10821 .ranges::<Point>(&editor.display_snapshot(cx)),
10822 [
10823 Point::new(0, 2)..Point::new(0, 2),
10824 Point::new(1, 2)..Point::new(1, 2),
10825 Point::new(2, 2)..Point::new(2, 2)
10826 ]
10827 );
10828
10829 editor.delete_to_previous_word_start(&Default::default(), window, cx);
10830 assert_eq!(
10831 editor.text(cx),
10832 "
10833 a
10834 b
10835 c
10836 "
10837 .unindent()
10838 );
10839 assert_eq!(
10840 editor
10841 .selections
10842 .ranges::<Point>(&editor.display_snapshot(cx)),
10843 [
10844 Point::new(0, 1)..Point::new(0, 1),
10845 Point::new(1, 1)..Point::new(1, 1),
10846 Point::new(2, 1)..Point::new(2, 1)
10847 ]
10848 );
10849 });
10850}
10851
10852#[gpui::test]
10853async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
10854 init_test(cx, |settings| {
10855 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10856 });
10857
10858 let mut cx = EditorTestContext::new(cx).await;
10859
10860 let language = Arc::new(Language::new(
10861 LanguageConfig {
10862 brackets: BracketPairConfig {
10863 pairs: vec![
10864 BracketPair {
10865 start: "{".to_string(),
10866 end: "}".to_string(),
10867 close: true,
10868 surround: true,
10869 newline: true,
10870 },
10871 BracketPair {
10872 start: "(".to_string(),
10873 end: ")".to_string(),
10874 close: true,
10875 surround: true,
10876 newline: true,
10877 },
10878 BracketPair {
10879 start: "[".to_string(),
10880 end: "]".to_string(),
10881 close: false,
10882 surround: true,
10883 newline: true,
10884 },
10885 ],
10886 ..Default::default()
10887 },
10888 autoclose_before: "})]".to_string(),
10889 ..Default::default()
10890 },
10891 Some(tree_sitter_rust::LANGUAGE.into()),
10892 ));
10893
10894 cx.language_registry().add(language.clone());
10895 cx.update_buffer(|buffer, cx| {
10896 buffer.set_language(Some(language), cx);
10897 });
10898
10899 cx.set_state(
10900 &"
10901 {(ˇ)}
10902 [[ˇ]]
10903 {(ˇ)}
10904 "
10905 .unindent(),
10906 );
10907
10908 cx.update_editor(|editor, window, cx| {
10909 editor.backspace(&Default::default(), window, cx);
10910 editor.backspace(&Default::default(), window, cx);
10911 });
10912
10913 cx.assert_editor_state(
10914 &"
10915 ˇ
10916 ˇ]]
10917 ˇ
10918 "
10919 .unindent(),
10920 );
10921
10922 cx.update_editor(|editor, window, cx| {
10923 editor.handle_input("{", window, cx);
10924 editor.handle_input("{", window, cx);
10925 editor.move_right(&MoveRight, window, cx);
10926 editor.move_right(&MoveRight, window, cx);
10927 editor.move_left(&MoveLeft, window, cx);
10928 editor.move_left(&MoveLeft, window, cx);
10929 editor.backspace(&Default::default(), window, cx);
10930 });
10931
10932 cx.assert_editor_state(
10933 &"
10934 {ˇ}
10935 {ˇ}]]
10936 {ˇ}
10937 "
10938 .unindent(),
10939 );
10940
10941 cx.update_editor(|editor, window, cx| {
10942 editor.backspace(&Default::default(), window, cx);
10943 });
10944
10945 cx.assert_editor_state(
10946 &"
10947 ˇ
10948 ˇ]]
10949 ˇ
10950 "
10951 .unindent(),
10952 );
10953}
10954
10955#[gpui::test]
10956async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
10957 init_test(cx, |_| {});
10958
10959 let language = Arc::new(Language::new(
10960 LanguageConfig::default(),
10961 Some(tree_sitter_rust::LANGUAGE.into()),
10962 ));
10963
10964 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
10965 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10966 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10967 editor
10968 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10969 .await;
10970
10971 editor.update_in(cx, |editor, window, cx| {
10972 editor.set_auto_replace_emoji_shortcode(true);
10973
10974 editor.handle_input("Hello ", window, cx);
10975 editor.handle_input(":wave", window, cx);
10976 assert_eq!(editor.text(cx), "Hello :wave".unindent());
10977
10978 editor.handle_input(":", window, cx);
10979 assert_eq!(editor.text(cx), "Hello 👋".unindent());
10980
10981 editor.handle_input(" :smile", window, cx);
10982 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
10983
10984 editor.handle_input(":", window, cx);
10985 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
10986
10987 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
10988 editor.handle_input(":wave", window, cx);
10989 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
10990
10991 editor.handle_input(":", window, cx);
10992 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
10993
10994 editor.handle_input(":1", window, cx);
10995 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
10996
10997 editor.handle_input(":", window, cx);
10998 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
10999
11000 // Ensure shortcode does not get replaced when it is part of a word
11001 editor.handle_input(" Test:wave", window, cx);
11002 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
11003
11004 editor.handle_input(":", window, cx);
11005 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
11006
11007 editor.set_auto_replace_emoji_shortcode(false);
11008
11009 // Ensure shortcode does not get replaced when auto replace is off
11010 editor.handle_input(" :wave", window, cx);
11011 assert_eq!(
11012 editor.text(cx),
11013 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
11014 );
11015
11016 editor.handle_input(":", window, cx);
11017 assert_eq!(
11018 editor.text(cx),
11019 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
11020 );
11021 });
11022}
11023
11024#[gpui::test]
11025async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
11026 init_test(cx, |_| {});
11027
11028 let (text, insertion_ranges) = marked_text_ranges(
11029 indoc! {"
11030 ˇ
11031 "},
11032 false,
11033 );
11034
11035 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
11036 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11037
11038 _ = editor.update_in(cx, |editor, window, cx| {
11039 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
11040
11041 editor
11042 .insert_snippet(&insertion_ranges, snippet, window, cx)
11043 .unwrap();
11044
11045 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
11046 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
11047 assert_eq!(editor.text(cx), expected_text);
11048 assert_eq!(
11049 editor
11050 .selections
11051 .ranges::<usize>(&editor.display_snapshot(cx)),
11052 selection_ranges
11053 );
11054 }
11055
11056 assert(
11057 editor,
11058 cx,
11059 indoc! {"
11060 type «» =•
11061 "},
11062 );
11063
11064 assert!(editor.context_menu_visible(), "There should be a matches");
11065 });
11066}
11067
11068#[gpui::test]
11069async fn test_snippets(cx: &mut TestAppContext) {
11070 init_test(cx, |_| {});
11071
11072 let mut cx = EditorTestContext::new(cx).await;
11073
11074 cx.set_state(indoc! {"
11075 a.ˇ b
11076 a.ˇ b
11077 a.ˇ b
11078 "});
11079
11080 cx.update_editor(|editor, window, cx| {
11081 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
11082 let insertion_ranges = editor
11083 .selections
11084 .all(&editor.display_snapshot(cx))
11085 .iter()
11086 .map(|s| s.range())
11087 .collect::<Vec<_>>();
11088 editor
11089 .insert_snippet(&insertion_ranges, snippet, window, cx)
11090 .unwrap();
11091 });
11092
11093 cx.assert_editor_state(indoc! {"
11094 a.f(«oneˇ», two, «threeˇ») b
11095 a.f(«oneˇ», two, «threeˇ») b
11096 a.f(«oneˇ», two, «threeˇ») b
11097 "});
11098
11099 // Can't move earlier than the first tab stop
11100 cx.update_editor(|editor, window, cx| {
11101 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
11102 });
11103 cx.assert_editor_state(indoc! {"
11104 a.f(«oneˇ», two, «threeˇ») b
11105 a.f(«oneˇ», two, «threeˇ») b
11106 a.f(«oneˇ», two, «threeˇ») b
11107 "});
11108
11109 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11110 cx.assert_editor_state(indoc! {"
11111 a.f(one, «twoˇ», three) b
11112 a.f(one, «twoˇ», three) b
11113 a.f(one, «twoˇ», three) b
11114 "});
11115
11116 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
11117 cx.assert_editor_state(indoc! {"
11118 a.f(«oneˇ», two, «threeˇ») b
11119 a.f(«oneˇ», two, «threeˇ») b
11120 a.f(«oneˇ», two, «threeˇ») b
11121 "});
11122
11123 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11124 cx.assert_editor_state(indoc! {"
11125 a.f(one, «twoˇ», three) b
11126 a.f(one, «twoˇ», three) b
11127 a.f(one, «twoˇ», three) b
11128 "});
11129 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11130 cx.assert_editor_state(indoc! {"
11131 a.f(one, two, three)ˇ b
11132 a.f(one, two, three)ˇ b
11133 a.f(one, two, three)ˇ b
11134 "});
11135
11136 // As soon as the last tab stop is reached, snippet state is gone
11137 cx.update_editor(|editor, window, cx| {
11138 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
11139 });
11140 cx.assert_editor_state(indoc! {"
11141 a.f(one, two, three)ˇ b
11142 a.f(one, two, three)ˇ b
11143 a.f(one, two, three)ˇ b
11144 "});
11145}
11146
11147#[gpui::test]
11148async fn test_snippet_indentation(cx: &mut TestAppContext) {
11149 init_test(cx, |_| {});
11150
11151 let mut cx = EditorTestContext::new(cx).await;
11152
11153 cx.update_editor(|editor, window, cx| {
11154 let snippet = Snippet::parse(indoc! {"
11155 /*
11156 * Multiline comment with leading indentation
11157 *
11158 * $1
11159 */
11160 $0"})
11161 .unwrap();
11162 let insertion_ranges = editor
11163 .selections
11164 .all(&editor.display_snapshot(cx))
11165 .iter()
11166 .map(|s| s.range())
11167 .collect::<Vec<_>>();
11168 editor
11169 .insert_snippet(&insertion_ranges, snippet, window, cx)
11170 .unwrap();
11171 });
11172
11173 cx.assert_editor_state(indoc! {"
11174 /*
11175 * Multiline comment with leading indentation
11176 *
11177 * ˇ
11178 */
11179 "});
11180
11181 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11182 cx.assert_editor_state(indoc! {"
11183 /*
11184 * Multiline comment with leading indentation
11185 *
11186 *•
11187 */
11188 ˇ"});
11189}
11190
11191#[gpui::test]
11192async fn test_document_format_during_save(cx: &mut TestAppContext) {
11193 init_test(cx, |_| {});
11194
11195 let fs = FakeFs::new(cx.executor());
11196 fs.insert_file(path!("/file.rs"), Default::default()).await;
11197
11198 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
11199
11200 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11201 language_registry.add(rust_lang());
11202 let mut fake_servers = language_registry.register_fake_lsp(
11203 "Rust",
11204 FakeLspAdapter {
11205 capabilities: lsp::ServerCapabilities {
11206 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11207 ..Default::default()
11208 },
11209 ..Default::default()
11210 },
11211 );
11212
11213 let buffer = project
11214 .update(cx, |project, cx| {
11215 project.open_local_buffer(path!("/file.rs"), cx)
11216 })
11217 .await
11218 .unwrap();
11219
11220 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11221 let (editor, cx) = cx.add_window_view(|window, cx| {
11222 build_editor_with_project(project.clone(), buffer, window, cx)
11223 });
11224 editor.update_in(cx, |editor, window, cx| {
11225 editor.set_text("one\ntwo\nthree\n", window, cx)
11226 });
11227 assert!(cx.read(|cx| editor.is_dirty(cx)));
11228
11229 cx.executor().start_waiting();
11230 let fake_server = fake_servers.next().await.unwrap();
11231
11232 {
11233 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11234 move |params, _| async move {
11235 assert_eq!(
11236 params.text_document.uri,
11237 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11238 );
11239 assert_eq!(params.options.tab_size, 4);
11240 Ok(Some(vec![lsp::TextEdit::new(
11241 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11242 ", ".to_string(),
11243 )]))
11244 },
11245 );
11246 let save = editor
11247 .update_in(cx, |editor, window, cx| {
11248 editor.save(
11249 SaveOptions {
11250 format: true,
11251 autosave: false,
11252 },
11253 project.clone(),
11254 window,
11255 cx,
11256 )
11257 })
11258 .unwrap();
11259 cx.executor().start_waiting();
11260 save.await;
11261
11262 assert_eq!(
11263 editor.update(cx, |editor, cx| editor.text(cx)),
11264 "one, two\nthree\n"
11265 );
11266 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11267 }
11268
11269 {
11270 editor.update_in(cx, |editor, window, cx| {
11271 editor.set_text("one\ntwo\nthree\n", window, cx)
11272 });
11273 assert!(cx.read(|cx| editor.is_dirty(cx)));
11274
11275 // Ensure we can still save even if formatting hangs.
11276 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11277 move |params, _| async move {
11278 assert_eq!(
11279 params.text_document.uri,
11280 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11281 );
11282 futures::future::pending::<()>().await;
11283 unreachable!()
11284 },
11285 );
11286 let save = editor
11287 .update_in(cx, |editor, window, cx| {
11288 editor.save(
11289 SaveOptions {
11290 format: true,
11291 autosave: false,
11292 },
11293 project.clone(),
11294 window,
11295 cx,
11296 )
11297 })
11298 .unwrap();
11299 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11300 cx.executor().start_waiting();
11301 save.await;
11302 assert_eq!(
11303 editor.update(cx, |editor, cx| editor.text(cx)),
11304 "one\ntwo\nthree\n"
11305 );
11306 }
11307
11308 // Set rust language override and assert overridden tabsize is sent to language server
11309 update_test_language_settings(cx, |settings| {
11310 settings.languages.0.insert(
11311 "Rust".into(),
11312 LanguageSettingsContent {
11313 tab_size: NonZeroU32::new(8),
11314 ..Default::default()
11315 },
11316 );
11317 });
11318
11319 {
11320 editor.update_in(cx, |editor, window, cx| {
11321 editor.set_text("somehting_new\n", window, cx)
11322 });
11323 assert!(cx.read(|cx| editor.is_dirty(cx)));
11324 let _formatting_request_signal = fake_server
11325 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
11326 assert_eq!(
11327 params.text_document.uri,
11328 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11329 );
11330 assert_eq!(params.options.tab_size, 8);
11331 Ok(Some(vec![]))
11332 });
11333 let save = editor
11334 .update_in(cx, |editor, window, cx| {
11335 editor.save(
11336 SaveOptions {
11337 format: true,
11338 autosave: false,
11339 },
11340 project.clone(),
11341 window,
11342 cx,
11343 )
11344 })
11345 .unwrap();
11346 cx.executor().start_waiting();
11347 save.await;
11348 }
11349}
11350
11351#[gpui::test]
11352async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
11353 init_test(cx, |settings| {
11354 settings.defaults.ensure_final_newline_on_save = Some(false);
11355 });
11356
11357 let fs = FakeFs::new(cx.executor());
11358 fs.insert_file(path!("/file.txt"), "foo".into()).await;
11359
11360 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
11361
11362 let buffer = project
11363 .update(cx, |project, cx| {
11364 project.open_local_buffer(path!("/file.txt"), cx)
11365 })
11366 .await
11367 .unwrap();
11368
11369 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11370 let (editor, cx) = cx.add_window_view(|window, cx| {
11371 build_editor_with_project(project.clone(), buffer, window, cx)
11372 });
11373 editor.update_in(cx, |editor, window, cx| {
11374 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
11375 s.select_ranges([0..0])
11376 });
11377 });
11378 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11379
11380 editor.update_in(cx, |editor, window, cx| {
11381 editor.handle_input("\n", window, cx)
11382 });
11383 cx.run_until_parked();
11384 save(&editor, &project, cx).await;
11385 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11386
11387 editor.update_in(cx, |editor, window, cx| {
11388 editor.undo(&Default::default(), window, cx);
11389 });
11390 save(&editor, &project, cx).await;
11391 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11392
11393 editor.update_in(cx, |editor, window, cx| {
11394 editor.redo(&Default::default(), window, cx);
11395 });
11396 cx.run_until_parked();
11397 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11398
11399 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
11400 let save = editor
11401 .update_in(cx, |editor, window, cx| {
11402 editor.save(
11403 SaveOptions {
11404 format: true,
11405 autosave: false,
11406 },
11407 project.clone(),
11408 window,
11409 cx,
11410 )
11411 })
11412 .unwrap();
11413 cx.executor().start_waiting();
11414 save.await;
11415 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11416 }
11417}
11418
11419#[gpui::test]
11420async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
11421 init_test(cx, |_| {});
11422
11423 let cols = 4;
11424 let rows = 10;
11425 let sample_text_1 = sample_text(rows, cols, 'a');
11426 assert_eq!(
11427 sample_text_1,
11428 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
11429 );
11430 let sample_text_2 = sample_text(rows, cols, 'l');
11431 assert_eq!(
11432 sample_text_2,
11433 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
11434 );
11435 let sample_text_3 = sample_text(rows, cols, 'v');
11436 assert_eq!(
11437 sample_text_3,
11438 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
11439 );
11440
11441 let fs = FakeFs::new(cx.executor());
11442 fs.insert_tree(
11443 path!("/a"),
11444 json!({
11445 "main.rs": sample_text_1,
11446 "other.rs": sample_text_2,
11447 "lib.rs": sample_text_3,
11448 }),
11449 )
11450 .await;
11451
11452 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11453 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11454 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11455
11456 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11457 language_registry.add(rust_lang());
11458 let mut fake_servers = language_registry.register_fake_lsp(
11459 "Rust",
11460 FakeLspAdapter {
11461 capabilities: lsp::ServerCapabilities {
11462 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11463 ..Default::default()
11464 },
11465 ..Default::default()
11466 },
11467 );
11468
11469 let worktree = project.update(cx, |project, cx| {
11470 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
11471 assert_eq!(worktrees.len(), 1);
11472 worktrees.pop().unwrap()
11473 });
11474 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11475
11476 let buffer_1 = project
11477 .update(cx, |project, cx| {
11478 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
11479 })
11480 .await
11481 .unwrap();
11482 let buffer_2 = project
11483 .update(cx, |project, cx| {
11484 project.open_buffer((worktree_id, rel_path("other.rs")), cx)
11485 })
11486 .await
11487 .unwrap();
11488 let buffer_3 = project
11489 .update(cx, |project, cx| {
11490 project.open_buffer((worktree_id, rel_path("lib.rs")), cx)
11491 })
11492 .await
11493 .unwrap();
11494
11495 let multi_buffer = cx.new(|cx| {
11496 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11497 multi_buffer.push_excerpts(
11498 buffer_1.clone(),
11499 [
11500 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11501 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11502 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11503 ],
11504 cx,
11505 );
11506 multi_buffer.push_excerpts(
11507 buffer_2.clone(),
11508 [
11509 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11510 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11511 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11512 ],
11513 cx,
11514 );
11515 multi_buffer.push_excerpts(
11516 buffer_3.clone(),
11517 [
11518 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11519 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11520 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11521 ],
11522 cx,
11523 );
11524 multi_buffer
11525 });
11526 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
11527 Editor::new(
11528 EditorMode::full(),
11529 multi_buffer,
11530 Some(project.clone()),
11531 window,
11532 cx,
11533 )
11534 });
11535
11536 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11537 editor.change_selections(
11538 SelectionEffects::scroll(Autoscroll::Next),
11539 window,
11540 cx,
11541 |s| s.select_ranges(Some(1..2)),
11542 );
11543 editor.insert("|one|two|three|", window, cx);
11544 });
11545 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11546 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11547 editor.change_selections(
11548 SelectionEffects::scroll(Autoscroll::Next),
11549 window,
11550 cx,
11551 |s| s.select_ranges(Some(60..70)),
11552 );
11553 editor.insert("|four|five|six|", window, cx);
11554 });
11555 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11556
11557 // First two buffers should be edited, but not the third one.
11558 assert_eq!(
11559 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11560 "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}",
11561 );
11562 buffer_1.update(cx, |buffer, _| {
11563 assert!(buffer.is_dirty());
11564 assert_eq!(
11565 buffer.text(),
11566 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
11567 )
11568 });
11569 buffer_2.update(cx, |buffer, _| {
11570 assert!(buffer.is_dirty());
11571 assert_eq!(
11572 buffer.text(),
11573 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
11574 )
11575 });
11576 buffer_3.update(cx, |buffer, _| {
11577 assert!(!buffer.is_dirty());
11578 assert_eq!(buffer.text(), sample_text_3,)
11579 });
11580 cx.executor().run_until_parked();
11581
11582 cx.executor().start_waiting();
11583 let save = multi_buffer_editor
11584 .update_in(cx, |editor, window, cx| {
11585 editor.save(
11586 SaveOptions {
11587 format: true,
11588 autosave: false,
11589 },
11590 project.clone(),
11591 window,
11592 cx,
11593 )
11594 })
11595 .unwrap();
11596
11597 let fake_server = fake_servers.next().await.unwrap();
11598 fake_server
11599 .server
11600 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
11601 Ok(Some(vec![lsp::TextEdit::new(
11602 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11603 format!("[{} formatted]", params.text_document.uri),
11604 )]))
11605 })
11606 .detach();
11607 save.await;
11608
11609 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
11610 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
11611 assert_eq!(
11612 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11613 uri!(
11614 "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}"
11615 ),
11616 );
11617 buffer_1.update(cx, |buffer, _| {
11618 assert!(!buffer.is_dirty());
11619 assert_eq!(
11620 buffer.text(),
11621 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
11622 )
11623 });
11624 buffer_2.update(cx, |buffer, _| {
11625 assert!(!buffer.is_dirty());
11626 assert_eq!(
11627 buffer.text(),
11628 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
11629 )
11630 });
11631 buffer_3.update(cx, |buffer, _| {
11632 assert!(!buffer.is_dirty());
11633 assert_eq!(buffer.text(), sample_text_3,)
11634 });
11635}
11636
11637#[gpui::test]
11638async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
11639 init_test(cx, |_| {});
11640
11641 let fs = FakeFs::new(cx.executor());
11642 fs.insert_tree(
11643 path!("/dir"),
11644 json!({
11645 "file1.rs": "fn main() { println!(\"hello\"); }",
11646 "file2.rs": "fn test() { println!(\"test\"); }",
11647 "file3.rs": "fn other() { println!(\"other\"); }\n",
11648 }),
11649 )
11650 .await;
11651
11652 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
11653 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11654 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11655
11656 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11657 language_registry.add(rust_lang());
11658
11659 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
11660 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11661
11662 // Open three buffers
11663 let buffer_1 = project
11664 .update(cx, |project, cx| {
11665 project.open_buffer((worktree_id, rel_path("file1.rs")), cx)
11666 })
11667 .await
11668 .unwrap();
11669 let buffer_2 = project
11670 .update(cx, |project, cx| {
11671 project.open_buffer((worktree_id, rel_path("file2.rs")), cx)
11672 })
11673 .await
11674 .unwrap();
11675 let buffer_3 = project
11676 .update(cx, |project, cx| {
11677 project.open_buffer((worktree_id, rel_path("file3.rs")), cx)
11678 })
11679 .await
11680 .unwrap();
11681
11682 // Create a multi-buffer with all three buffers
11683 let multi_buffer = cx.new(|cx| {
11684 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11685 multi_buffer.push_excerpts(
11686 buffer_1.clone(),
11687 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11688 cx,
11689 );
11690 multi_buffer.push_excerpts(
11691 buffer_2.clone(),
11692 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11693 cx,
11694 );
11695 multi_buffer.push_excerpts(
11696 buffer_3.clone(),
11697 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11698 cx,
11699 );
11700 multi_buffer
11701 });
11702
11703 let editor = cx.new_window_entity(|window, cx| {
11704 Editor::new(
11705 EditorMode::full(),
11706 multi_buffer,
11707 Some(project.clone()),
11708 window,
11709 cx,
11710 )
11711 });
11712
11713 // Edit only the first buffer
11714 editor.update_in(cx, |editor, window, cx| {
11715 editor.change_selections(
11716 SelectionEffects::scroll(Autoscroll::Next),
11717 window,
11718 cx,
11719 |s| s.select_ranges(Some(10..10)),
11720 );
11721 editor.insert("// edited", window, cx);
11722 });
11723
11724 // Verify that only buffer 1 is dirty
11725 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
11726 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11727 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11728
11729 // Get write counts after file creation (files were created with initial content)
11730 // We expect each file to have been written once during creation
11731 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
11732 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
11733 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
11734
11735 // Perform autosave
11736 let save_task = editor.update_in(cx, |editor, window, cx| {
11737 editor.save(
11738 SaveOptions {
11739 format: true,
11740 autosave: true,
11741 },
11742 project.clone(),
11743 window,
11744 cx,
11745 )
11746 });
11747 save_task.await.unwrap();
11748
11749 // Only the dirty buffer should have been saved
11750 assert_eq!(
11751 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11752 1,
11753 "Buffer 1 was dirty, so it should have been written once during autosave"
11754 );
11755 assert_eq!(
11756 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11757 0,
11758 "Buffer 2 was clean, so it should not have been written during autosave"
11759 );
11760 assert_eq!(
11761 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11762 0,
11763 "Buffer 3 was clean, so it should not have been written during autosave"
11764 );
11765
11766 // Verify buffer states after autosave
11767 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11768 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11769 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11770
11771 // Now perform a manual save (format = true)
11772 let save_task = editor.update_in(cx, |editor, window, cx| {
11773 editor.save(
11774 SaveOptions {
11775 format: true,
11776 autosave: false,
11777 },
11778 project.clone(),
11779 window,
11780 cx,
11781 )
11782 });
11783 save_task.await.unwrap();
11784
11785 // During manual save, clean buffers don't get written to disk
11786 // They just get did_save called for language server notifications
11787 assert_eq!(
11788 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11789 1,
11790 "Buffer 1 should only have been written once total (during autosave, not manual save)"
11791 );
11792 assert_eq!(
11793 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11794 0,
11795 "Buffer 2 should not have been written at all"
11796 );
11797 assert_eq!(
11798 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11799 0,
11800 "Buffer 3 should not have been written at all"
11801 );
11802}
11803
11804async fn setup_range_format_test(
11805 cx: &mut TestAppContext,
11806) -> (
11807 Entity<Project>,
11808 Entity<Editor>,
11809 &mut gpui::VisualTestContext,
11810 lsp::FakeLanguageServer,
11811) {
11812 init_test(cx, |_| {});
11813
11814 let fs = FakeFs::new(cx.executor());
11815 fs.insert_file(path!("/file.rs"), Default::default()).await;
11816
11817 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11818
11819 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11820 language_registry.add(rust_lang());
11821 let mut fake_servers = language_registry.register_fake_lsp(
11822 "Rust",
11823 FakeLspAdapter {
11824 capabilities: lsp::ServerCapabilities {
11825 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
11826 ..lsp::ServerCapabilities::default()
11827 },
11828 ..FakeLspAdapter::default()
11829 },
11830 );
11831
11832 let buffer = project
11833 .update(cx, |project, cx| {
11834 project.open_local_buffer(path!("/file.rs"), cx)
11835 })
11836 .await
11837 .unwrap();
11838
11839 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11840 let (editor, cx) = cx.add_window_view(|window, cx| {
11841 build_editor_with_project(project.clone(), buffer, window, cx)
11842 });
11843
11844 cx.executor().start_waiting();
11845 let fake_server = fake_servers.next().await.unwrap();
11846
11847 (project, editor, cx, fake_server)
11848}
11849
11850#[gpui::test]
11851async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
11852 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11853
11854 editor.update_in(cx, |editor, window, cx| {
11855 editor.set_text("one\ntwo\nthree\n", window, cx)
11856 });
11857 assert!(cx.read(|cx| editor.is_dirty(cx)));
11858
11859 let save = editor
11860 .update_in(cx, |editor, window, cx| {
11861 editor.save(
11862 SaveOptions {
11863 format: true,
11864 autosave: false,
11865 },
11866 project.clone(),
11867 window,
11868 cx,
11869 )
11870 })
11871 .unwrap();
11872 fake_server
11873 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
11874 assert_eq!(
11875 params.text_document.uri,
11876 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11877 );
11878 assert_eq!(params.options.tab_size, 4);
11879 Ok(Some(vec![lsp::TextEdit::new(
11880 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11881 ", ".to_string(),
11882 )]))
11883 })
11884 .next()
11885 .await;
11886 cx.executor().start_waiting();
11887 save.await;
11888 assert_eq!(
11889 editor.update(cx, |editor, cx| editor.text(cx)),
11890 "one, two\nthree\n"
11891 );
11892 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11893}
11894
11895#[gpui::test]
11896async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
11897 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11898
11899 editor.update_in(cx, |editor, window, cx| {
11900 editor.set_text("one\ntwo\nthree\n", window, cx)
11901 });
11902 assert!(cx.read(|cx| editor.is_dirty(cx)));
11903
11904 // Test that save still works when formatting hangs
11905 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
11906 move |params, _| async move {
11907 assert_eq!(
11908 params.text_document.uri,
11909 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11910 );
11911 futures::future::pending::<()>().await;
11912 unreachable!()
11913 },
11914 );
11915 let save = editor
11916 .update_in(cx, |editor, window, cx| {
11917 editor.save(
11918 SaveOptions {
11919 format: true,
11920 autosave: false,
11921 },
11922 project.clone(),
11923 window,
11924 cx,
11925 )
11926 })
11927 .unwrap();
11928 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11929 cx.executor().start_waiting();
11930 save.await;
11931 assert_eq!(
11932 editor.update(cx, |editor, cx| editor.text(cx)),
11933 "one\ntwo\nthree\n"
11934 );
11935 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11936}
11937
11938#[gpui::test]
11939async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
11940 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11941
11942 // Buffer starts clean, no formatting should be requested
11943 let save = editor
11944 .update_in(cx, |editor, window, cx| {
11945 editor.save(
11946 SaveOptions {
11947 format: false,
11948 autosave: false,
11949 },
11950 project.clone(),
11951 window,
11952 cx,
11953 )
11954 })
11955 .unwrap();
11956 let _pending_format_request = fake_server
11957 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
11958 panic!("Should not be invoked");
11959 })
11960 .next();
11961 cx.executor().start_waiting();
11962 save.await;
11963 cx.run_until_parked();
11964}
11965
11966#[gpui::test]
11967async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
11968 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11969
11970 // Set Rust language override and assert overridden tabsize is sent to language server
11971 update_test_language_settings(cx, |settings| {
11972 settings.languages.0.insert(
11973 "Rust".into(),
11974 LanguageSettingsContent {
11975 tab_size: NonZeroU32::new(8),
11976 ..Default::default()
11977 },
11978 );
11979 });
11980
11981 editor.update_in(cx, |editor, window, cx| {
11982 editor.set_text("something_new\n", window, cx)
11983 });
11984 assert!(cx.read(|cx| editor.is_dirty(cx)));
11985 let save = editor
11986 .update_in(cx, |editor, window, cx| {
11987 editor.save(
11988 SaveOptions {
11989 format: true,
11990 autosave: false,
11991 },
11992 project.clone(),
11993 window,
11994 cx,
11995 )
11996 })
11997 .unwrap();
11998 fake_server
11999 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
12000 assert_eq!(
12001 params.text_document.uri,
12002 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12003 );
12004 assert_eq!(params.options.tab_size, 8);
12005 Ok(Some(Vec::new()))
12006 })
12007 .next()
12008 .await;
12009 save.await;
12010}
12011
12012#[gpui::test]
12013async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
12014 init_test(cx, |settings| {
12015 settings.defaults.formatter = Some(FormatterList::Single(Formatter::LanguageServer(
12016 settings::LanguageServerFormatterSpecifier::Current,
12017 )))
12018 });
12019
12020 let fs = FakeFs::new(cx.executor());
12021 fs.insert_file(path!("/file.rs"), Default::default()).await;
12022
12023 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12024
12025 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12026 language_registry.add(Arc::new(Language::new(
12027 LanguageConfig {
12028 name: "Rust".into(),
12029 matcher: LanguageMatcher {
12030 path_suffixes: vec!["rs".to_string()],
12031 ..Default::default()
12032 },
12033 ..LanguageConfig::default()
12034 },
12035 Some(tree_sitter_rust::LANGUAGE.into()),
12036 )));
12037 update_test_language_settings(cx, |settings| {
12038 // Enable Prettier formatting for the same buffer, and ensure
12039 // LSP is called instead of Prettier.
12040 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
12041 });
12042 let mut fake_servers = language_registry.register_fake_lsp(
12043 "Rust",
12044 FakeLspAdapter {
12045 capabilities: lsp::ServerCapabilities {
12046 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12047 ..Default::default()
12048 },
12049 ..Default::default()
12050 },
12051 );
12052
12053 let buffer = project
12054 .update(cx, |project, cx| {
12055 project.open_local_buffer(path!("/file.rs"), cx)
12056 })
12057 .await
12058 .unwrap();
12059
12060 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12061 let (editor, cx) = cx.add_window_view(|window, cx| {
12062 build_editor_with_project(project.clone(), buffer, window, cx)
12063 });
12064 editor.update_in(cx, |editor, window, cx| {
12065 editor.set_text("one\ntwo\nthree\n", window, cx)
12066 });
12067
12068 cx.executor().start_waiting();
12069 let fake_server = fake_servers.next().await.unwrap();
12070
12071 let format = editor
12072 .update_in(cx, |editor, window, cx| {
12073 editor.perform_format(
12074 project.clone(),
12075 FormatTrigger::Manual,
12076 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12077 window,
12078 cx,
12079 )
12080 })
12081 .unwrap();
12082 fake_server
12083 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
12084 assert_eq!(
12085 params.text_document.uri,
12086 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12087 );
12088 assert_eq!(params.options.tab_size, 4);
12089 Ok(Some(vec![lsp::TextEdit::new(
12090 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12091 ", ".to_string(),
12092 )]))
12093 })
12094 .next()
12095 .await;
12096 cx.executor().start_waiting();
12097 format.await;
12098 assert_eq!(
12099 editor.update(cx, |editor, cx| editor.text(cx)),
12100 "one, two\nthree\n"
12101 );
12102
12103 editor.update_in(cx, |editor, window, cx| {
12104 editor.set_text("one\ntwo\nthree\n", window, cx)
12105 });
12106 // Ensure we don't lock if formatting hangs.
12107 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12108 move |params, _| async move {
12109 assert_eq!(
12110 params.text_document.uri,
12111 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12112 );
12113 futures::future::pending::<()>().await;
12114 unreachable!()
12115 },
12116 );
12117 let format = editor
12118 .update_in(cx, |editor, window, cx| {
12119 editor.perform_format(
12120 project,
12121 FormatTrigger::Manual,
12122 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12123 window,
12124 cx,
12125 )
12126 })
12127 .unwrap();
12128 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
12129 cx.executor().start_waiting();
12130 format.await;
12131 assert_eq!(
12132 editor.update(cx, |editor, cx| editor.text(cx)),
12133 "one\ntwo\nthree\n"
12134 );
12135}
12136
12137#[gpui::test]
12138async fn test_multiple_formatters(cx: &mut TestAppContext) {
12139 init_test(cx, |settings| {
12140 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
12141 settings.defaults.formatter = Some(FormatterList::Vec(vec![
12142 Formatter::LanguageServer(settings::LanguageServerFormatterSpecifier::Current),
12143 Formatter::CodeAction("code-action-1".into()),
12144 Formatter::CodeAction("code-action-2".into()),
12145 ]))
12146 });
12147
12148 let fs = FakeFs::new(cx.executor());
12149 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
12150 .await;
12151
12152 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12153 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12154 language_registry.add(rust_lang());
12155
12156 let mut fake_servers = language_registry.register_fake_lsp(
12157 "Rust",
12158 FakeLspAdapter {
12159 capabilities: lsp::ServerCapabilities {
12160 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12161 execute_command_provider: Some(lsp::ExecuteCommandOptions {
12162 commands: vec!["the-command-for-code-action-1".into()],
12163 ..Default::default()
12164 }),
12165 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12166 ..Default::default()
12167 },
12168 ..Default::default()
12169 },
12170 );
12171
12172 let buffer = project
12173 .update(cx, |project, cx| {
12174 project.open_local_buffer(path!("/file.rs"), cx)
12175 })
12176 .await
12177 .unwrap();
12178
12179 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12180 let (editor, cx) = cx.add_window_view(|window, cx| {
12181 build_editor_with_project(project.clone(), buffer, window, cx)
12182 });
12183
12184 cx.executor().start_waiting();
12185
12186 let fake_server = fake_servers.next().await.unwrap();
12187 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12188 move |_params, _| async move {
12189 Ok(Some(vec![lsp::TextEdit::new(
12190 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12191 "applied-formatting\n".to_string(),
12192 )]))
12193 },
12194 );
12195 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12196 move |params, _| async move {
12197 let requested_code_actions = params.context.only.expect("Expected code action request");
12198 assert_eq!(requested_code_actions.len(), 1);
12199
12200 let uri = lsp::Uri::from_file_path(path!("/file.rs")).unwrap();
12201 let code_action = match requested_code_actions[0].as_str() {
12202 "code-action-1" => lsp::CodeAction {
12203 kind: Some("code-action-1".into()),
12204 edit: Some(lsp::WorkspaceEdit::new(
12205 [(
12206 uri,
12207 vec![lsp::TextEdit::new(
12208 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12209 "applied-code-action-1-edit\n".to_string(),
12210 )],
12211 )]
12212 .into_iter()
12213 .collect(),
12214 )),
12215 command: Some(lsp::Command {
12216 command: "the-command-for-code-action-1".into(),
12217 ..Default::default()
12218 }),
12219 ..Default::default()
12220 },
12221 "code-action-2" => lsp::CodeAction {
12222 kind: Some("code-action-2".into()),
12223 edit: Some(lsp::WorkspaceEdit::new(
12224 [(
12225 uri,
12226 vec![lsp::TextEdit::new(
12227 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12228 "applied-code-action-2-edit\n".to_string(),
12229 )],
12230 )]
12231 .into_iter()
12232 .collect(),
12233 )),
12234 ..Default::default()
12235 },
12236 req => panic!("Unexpected code action request: {:?}", req),
12237 };
12238 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12239 code_action,
12240 )]))
12241 },
12242 );
12243
12244 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
12245 move |params, _| async move { Ok(params) }
12246 });
12247
12248 let command_lock = Arc::new(futures::lock::Mutex::new(()));
12249 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
12250 let fake = fake_server.clone();
12251 let lock = command_lock.clone();
12252 move |params, _| {
12253 assert_eq!(params.command, "the-command-for-code-action-1");
12254 let fake = fake.clone();
12255 let lock = lock.clone();
12256 async move {
12257 lock.lock().await;
12258 fake.server
12259 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
12260 label: None,
12261 edit: lsp::WorkspaceEdit {
12262 changes: Some(
12263 [(
12264 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
12265 vec![lsp::TextEdit {
12266 range: lsp::Range::new(
12267 lsp::Position::new(0, 0),
12268 lsp::Position::new(0, 0),
12269 ),
12270 new_text: "applied-code-action-1-command\n".into(),
12271 }],
12272 )]
12273 .into_iter()
12274 .collect(),
12275 ),
12276 ..Default::default()
12277 },
12278 })
12279 .await
12280 .into_response()
12281 .unwrap();
12282 Ok(Some(json!(null)))
12283 }
12284 }
12285 });
12286
12287 cx.executor().start_waiting();
12288 editor
12289 .update_in(cx, |editor, window, cx| {
12290 editor.perform_format(
12291 project.clone(),
12292 FormatTrigger::Manual,
12293 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12294 window,
12295 cx,
12296 )
12297 })
12298 .unwrap()
12299 .await;
12300 editor.update(cx, |editor, cx| {
12301 assert_eq!(
12302 editor.text(cx),
12303 r#"
12304 applied-code-action-2-edit
12305 applied-code-action-1-command
12306 applied-code-action-1-edit
12307 applied-formatting
12308 one
12309 two
12310 three
12311 "#
12312 .unindent()
12313 );
12314 });
12315
12316 editor.update_in(cx, |editor, window, cx| {
12317 editor.undo(&Default::default(), window, cx);
12318 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12319 });
12320
12321 // Perform a manual edit while waiting for an LSP command
12322 // that's being run as part of a formatting code action.
12323 let lock_guard = command_lock.lock().await;
12324 let format = editor
12325 .update_in(cx, |editor, window, cx| {
12326 editor.perform_format(
12327 project.clone(),
12328 FormatTrigger::Manual,
12329 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12330 window,
12331 cx,
12332 )
12333 })
12334 .unwrap();
12335 cx.run_until_parked();
12336 editor.update(cx, |editor, cx| {
12337 assert_eq!(
12338 editor.text(cx),
12339 r#"
12340 applied-code-action-1-edit
12341 applied-formatting
12342 one
12343 two
12344 three
12345 "#
12346 .unindent()
12347 );
12348
12349 editor.buffer.update(cx, |buffer, cx| {
12350 let ix = buffer.len(cx);
12351 buffer.edit([(ix..ix, "edited\n")], None, cx);
12352 });
12353 });
12354
12355 // Allow the LSP command to proceed. Because the buffer was edited,
12356 // the second code action will not be run.
12357 drop(lock_guard);
12358 format.await;
12359 editor.update_in(cx, |editor, window, cx| {
12360 assert_eq!(
12361 editor.text(cx),
12362 r#"
12363 applied-code-action-1-command
12364 applied-code-action-1-edit
12365 applied-formatting
12366 one
12367 two
12368 three
12369 edited
12370 "#
12371 .unindent()
12372 );
12373
12374 // The manual edit is undone first, because it is the last thing the user did
12375 // (even though the command completed afterwards).
12376 editor.undo(&Default::default(), window, cx);
12377 assert_eq!(
12378 editor.text(cx),
12379 r#"
12380 applied-code-action-1-command
12381 applied-code-action-1-edit
12382 applied-formatting
12383 one
12384 two
12385 three
12386 "#
12387 .unindent()
12388 );
12389
12390 // All the formatting (including the command, which completed after the manual edit)
12391 // is undone together.
12392 editor.undo(&Default::default(), window, cx);
12393 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12394 });
12395}
12396
12397#[gpui::test]
12398async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
12399 init_test(cx, |settings| {
12400 settings.defaults.formatter = Some(FormatterList::Vec(vec![Formatter::LanguageServer(
12401 settings::LanguageServerFormatterSpecifier::Current,
12402 )]))
12403 });
12404
12405 let fs = FakeFs::new(cx.executor());
12406 fs.insert_file(path!("/file.ts"), Default::default()).await;
12407
12408 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12409
12410 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12411 language_registry.add(Arc::new(Language::new(
12412 LanguageConfig {
12413 name: "TypeScript".into(),
12414 matcher: LanguageMatcher {
12415 path_suffixes: vec!["ts".to_string()],
12416 ..Default::default()
12417 },
12418 ..LanguageConfig::default()
12419 },
12420 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12421 )));
12422 update_test_language_settings(cx, |settings| {
12423 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
12424 });
12425 let mut fake_servers = language_registry.register_fake_lsp(
12426 "TypeScript",
12427 FakeLspAdapter {
12428 capabilities: lsp::ServerCapabilities {
12429 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12430 ..Default::default()
12431 },
12432 ..Default::default()
12433 },
12434 );
12435
12436 let buffer = project
12437 .update(cx, |project, cx| {
12438 project.open_local_buffer(path!("/file.ts"), cx)
12439 })
12440 .await
12441 .unwrap();
12442
12443 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12444 let (editor, cx) = cx.add_window_view(|window, cx| {
12445 build_editor_with_project(project.clone(), buffer, window, cx)
12446 });
12447 editor.update_in(cx, |editor, window, cx| {
12448 editor.set_text(
12449 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12450 window,
12451 cx,
12452 )
12453 });
12454
12455 cx.executor().start_waiting();
12456 let fake_server = fake_servers.next().await.unwrap();
12457
12458 let format = editor
12459 .update_in(cx, |editor, window, cx| {
12460 editor.perform_code_action_kind(
12461 project.clone(),
12462 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12463 window,
12464 cx,
12465 )
12466 })
12467 .unwrap();
12468 fake_server
12469 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
12470 assert_eq!(
12471 params.text_document.uri,
12472 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12473 );
12474 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12475 lsp::CodeAction {
12476 title: "Organize Imports".to_string(),
12477 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
12478 edit: Some(lsp::WorkspaceEdit {
12479 changes: Some(
12480 [(
12481 params.text_document.uri.clone(),
12482 vec![lsp::TextEdit::new(
12483 lsp::Range::new(
12484 lsp::Position::new(1, 0),
12485 lsp::Position::new(2, 0),
12486 ),
12487 "".to_string(),
12488 )],
12489 )]
12490 .into_iter()
12491 .collect(),
12492 ),
12493 ..Default::default()
12494 }),
12495 ..Default::default()
12496 },
12497 )]))
12498 })
12499 .next()
12500 .await;
12501 cx.executor().start_waiting();
12502 format.await;
12503 assert_eq!(
12504 editor.update(cx, |editor, cx| editor.text(cx)),
12505 "import { a } from 'module';\n\nconst x = a;\n"
12506 );
12507
12508 editor.update_in(cx, |editor, window, cx| {
12509 editor.set_text(
12510 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12511 window,
12512 cx,
12513 )
12514 });
12515 // Ensure we don't lock if code action hangs.
12516 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12517 move |params, _| async move {
12518 assert_eq!(
12519 params.text_document.uri,
12520 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12521 );
12522 futures::future::pending::<()>().await;
12523 unreachable!()
12524 },
12525 );
12526 let format = editor
12527 .update_in(cx, |editor, window, cx| {
12528 editor.perform_code_action_kind(
12529 project,
12530 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12531 window,
12532 cx,
12533 )
12534 })
12535 .unwrap();
12536 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
12537 cx.executor().start_waiting();
12538 format.await;
12539 assert_eq!(
12540 editor.update(cx, |editor, cx| editor.text(cx)),
12541 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
12542 );
12543}
12544
12545#[gpui::test]
12546async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
12547 init_test(cx, |_| {});
12548
12549 let mut cx = EditorLspTestContext::new_rust(
12550 lsp::ServerCapabilities {
12551 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12552 ..Default::default()
12553 },
12554 cx,
12555 )
12556 .await;
12557
12558 cx.set_state(indoc! {"
12559 one.twoˇ
12560 "});
12561
12562 // The format request takes a long time. When it completes, it inserts
12563 // a newline and an indent before the `.`
12564 cx.lsp
12565 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
12566 let executor = cx.background_executor().clone();
12567 async move {
12568 executor.timer(Duration::from_millis(100)).await;
12569 Ok(Some(vec![lsp::TextEdit {
12570 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
12571 new_text: "\n ".into(),
12572 }]))
12573 }
12574 });
12575
12576 // Submit a format request.
12577 let format_1 = cx
12578 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12579 .unwrap();
12580 cx.executor().run_until_parked();
12581
12582 // Submit a second format request.
12583 let format_2 = cx
12584 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12585 .unwrap();
12586 cx.executor().run_until_parked();
12587
12588 // Wait for both format requests to complete
12589 cx.executor().advance_clock(Duration::from_millis(200));
12590 cx.executor().start_waiting();
12591 format_1.await.unwrap();
12592 cx.executor().start_waiting();
12593 format_2.await.unwrap();
12594
12595 // The formatting edits only happens once.
12596 cx.assert_editor_state(indoc! {"
12597 one
12598 .twoˇ
12599 "});
12600}
12601
12602#[gpui::test]
12603async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
12604 init_test(cx, |settings| {
12605 settings.defaults.formatter = Some(FormatterList::default())
12606 });
12607
12608 let mut cx = EditorLspTestContext::new_rust(
12609 lsp::ServerCapabilities {
12610 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12611 ..Default::default()
12612 },
12613 cx,
12614 )
12615 .await;
12616
12617 cx.run_until_parked();
12618 // Set up a buffer white some trailing whitespace and no trailing newline.
12619 cx.set_state(
12620 &[
12621 "one ", //
12622 "twoˇ", //
12623 "three ", //
12624 "four", //
12625 ]
12626 .join("\n"),
12627 );
12628
12629 // Record which buffer changes have been sent to the language server
12630 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
12631 cx.lsp
12632 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
12633 let buffer_changes = buffer_changes.clone();
12634 move |params, _| {
12635 buffer_changes.lock().extend(
12636 params
12637 .content_changes
12638 .into_iter()
12639 .map(|e| (e.range.unwrap(), e.text)),
12640 );
12641 }
12642 });
12643
12644 // Handle formatting requests to the language server.
12645 cx.lsp
12646 .set_request_handler::<lsp::request::Formatting, _, _>({
12647 let buffer_changes = buffer_changes.clone();
12648 move |_, _| {
12649 let buffer_changes = buffer_changes.clone();
12650 // Insert blank lines between each line of the buffer.
12651 async move {
12652 // When formatting is requested, trailing whitespace has already been stripped,
12653 // and the trailing newline has already been added.
12654 assert_eq!(
12655 &buffer_changes.lock()[1..],
12656 &[
12657 (
12658 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
12659 "".into()
12660 ),
12661 (
12662 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
12663 "".into()
12664 ),
12665 (
12666 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
12667 "\n".into()
12668 ),
12669 ]
12670 );
12671
12672 Ok(Some(vec![
12673 lsp::TextEdit {
12674 range: lsp::Range::new(
12675 lsp::Position::new(1, 0),
12676 lsp::Position::new(1, 0),
12677 ),
12678 new_text: "\n".into(),
12679 },
12680 lsp::TextEdit {
12681 range: lsp::Range::new(
12682 lsp::Position::new(2, 0),
12683 lsp::Position::new(2, 0),
12684 ),
12685 new_text: "\n".into(),
12686 },
12687 ]))
12688 }
12689 }
12690 });
12691
12692 // Submit a format request.
12693 let format = cx
12694 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12695 .unwrap();
12696
12697 cx.run_until_parked();
12698 // After formatting the buffer, the trailing whitespace is stripped,
12699 // a newline is appended, and the edits provided by the language server
12700 // have been applied.
12701 format.await.unwrap();
12702
12703 cx.assert_editor_state(
12704 &[
12705 "one", //
12706 "", //
12707 "twoˇ", //
12708 "", //
12709 "three", //
12710 "four", //
12711 "", //
12712 ]
12713 .join("\n"),
12714 );
12715
12716 // Undoing the formatting undoes the trailing whitespace removal, the
12717 // trailing newline, and the LSP edits.
12718 cx.update_buffer(|buffer, cx| buffer.undo(cx));
12719 cx.assert_editor_state(
12720 &[
12721 "one ", //
12722 "twoˇ", //
12723 "three ", //
12724 "four", //
12725 ]
12726 .join("\n"),
12727 );
12728}
12729
12730#[gpui::test]
12731async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
12732 cx: &mut TestAppContext,
12733) {
12734 init_test(cx, |_| {});
12735
12736 cx.update(|cx| {
12737 cx.update_global::<SettingsStore, _>(|settings, cx| {
12738 settings.update_user_settings(cx, |settings| {
12739 settings.editor.auto_signature_help = Some(true);
12740 });
12741 });
12742 });
12743
12744 let mut cx = EditorLspTestContext::new_rust(
12745 lsp::ServerCapabilities {
12746 signature_help_provider: Some(lsp::SignatureHelpOptions {
12747 ..Default::default()
12748 }),
12749 ..Default::default()
12750 },
12751 cx,
12752 )
12753 .await;
12754
12755 let language = Language::new(
12756 LanguageConfig {
12757 name: "Rust".into(),
12758 brackets: BracketPairConfig {
12759 pairs: vec![
12760 BracketPair {
12761 start: "{".to_string(),
12762 end: "}".to_string(),
12763 close: true,
12764 surround: true,
12765 newline: true,
12766 },
12767 BracketPair {
12768 start: "(".to_string(),
12769 end: ")".to_string(),
12770 close: true,
12771 surround: true,
12772 newline: true,
12773 },
12774 BracketPair {
12775 start: "/*".to_string(),
12776 end: " */".to_string(),
12777 close: true,
12778 surround: true,
12779 newline: true,
12780 },
12781 BracketPair {
12782 start: "[".to_string(),
12783 end: "]".to_string(),
12784 close: false,
12785 surround: false,
12786 newline: true,
12787 },
12788 BracketPair {
12789 start: "\"".to_string(),
12790 end: "\"".to_string(),
12791 close: true,
12792 surround: true,
12793 newline: false,
12794 },
12795 BracketPair {
12796 start: "<".to_string(),
12797 end: ">".to_string(),
12798 close: false,
12799 surround: true,
12800 newline: true,
12801 },
12802 ],
12803 ..Default::default()
12804 },
12805 autoclose_before: "})]".to_string(),
12806 ..Default::default()
12807 },
12808 Some(tree_sitter_rust::LANGUAGE.into()),
12809 );
12810 let language = Arc::new(language);
12811
12812 cx.language_registry().add(language.clone());
12813 cx.update_buffer(|buffer, cx| {
12814 buffer.set_language(Some(language), cx);
12815 });
12816
12817 cx.set_state(
12818 &r#"
12819 fn main() {
12820 sampleˇ
12821 }
12822 "#
12823 .unindent(),
12824 );
12825
12826 cx.update_editor(|editor, window, cx| {
12827 editor.handle_input("(", window, cx);
12828 });
12829 cx.assert_editor_state(
12830 &"
12831 fn main() {
12832 sample(ˇ)
12833 }
12834 "
12835 .unindent(),
12836 );
12837
12838 let mocked_response = lsp::SignatureHelp {
12839 signatures: vec![lsp::SignatureInformation {
12840 label: "fn sample(param1: u8, param2: u8)".to_string(),
12841 documentation: None,
12842 parameters: Some(vec![
12843 lsp::ParameterInformation {
12844 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12845 documentation: None,
12846 },
12847 lsp::ParameterInformation {
12848 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12849 documentation: None,
12850 },
12851 ]),
12852 active_parameter: None,
12853 }],
12854 active_signature: Some(0),
12855 active_parameter: Some(0),
12856 };
12857 handle_signature_help_request(&mut cx, mocked_response).await;
12858
12859 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12860 .await;
12861
12862 cx.editor(|editor, _, _| {
12863 let signature_help_state = editor.signature_help_state.popover().cloned();
12864 let signature = signature_help_state.unwrap();
12865 assert_eq!(
12866 signature.signatures[signature.current_signature].label,
12867 "fn sample(param1: u8, param2: u8)"
12868 );
12869 });
12870}
12871
12872#[gpui::test]
12873async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
12874 init_test(cx, |_| {});
12875
12876 cx.update(|cx| {
12877 cx.update_global::<SettingsStore, _>(|settings, cx| {
12878 settings.update_user_settings(cx, |settings| {
12879 settings.editor.auto_signature_help = Some(false);
12880 settings.editor.show_signature_help_after_edits = Some(false);
12881 });
12882 });
12883 });
12884
12885 let mut cx = EditorLspTestContext::new_rust(
12886 lsp::ServerCapabilities {
12887 signature_help_provider: Some(lsp::SignatureHelpOptions {
12888 ..Default::default()
12889 }),
12890 ..Default::default()
12891 },
12892 cx,
12893 )
12894 .await;
12895
12896 let language = Language::new(
12897 LanguageConfig {
12898 name: "Rust".into(),
12899 brackets: BracketPairConfig {
12900 pairs: vec![
12901 BracketPair {
12902 start: "{".to_string(),
12903 end: "}".to_string(),
12904 close: true,
12905 surround: true,
12906 newline: true,
12907 },
12908 BracketPair {
12909 start: "(".to_string(),
12910 end: ")".to_string(),
12911 close: true,
12912 surround: true,
12913 newline: true,
12914 },
12915 BracketPair {
12916 start: "/*".to_string(),
12917 end: " */".to_string(),
12918 close: true,
12919 surround: true,
12920 newline: true,
12921 },
12922 BracketPair {
12923 start: "[".to_string(),
12924 end: "]".to_string(),
12925 close: false,
12926 surround: false,
12927 newline: true,
12928 },
12929 BracketPair {
12930 start: "\"".to_string(),
12931 end: "\"".to_string(),
12932 close: true,
12933 surround: true,
12934 newline: false,
12935 },
12936 BracketPair {
12937 start: "<".to_string(),
12938 end: ">".to_string(),
12939 close: false,
12940 surround: true,
12941 newline: true,
12942 },
12943 ],
12944 ..Default::default()
12945 },
12946 autoclose_before: "})]".to_string(),
12947 ..Default::default()
12948 },
12949 Some(tree_sitter_rust::LANGUAGE.into()),
12950 );
12951 let language = Arc::new(language);
12952
12953 cx.language_registry().add(language.clone());
12954 cx.update_buffer(|buffer, cx| {
12955 buffer.set_language(Some(language), cx);
12956 });
12957
12958 // Ensure that signature_help is not called when no signature help is enabled.
12959 cx.set_state(
12960 &r#"
12961 fn main() {
12962 sampleˇ
12963 }
12964 "#
12965 .unindent(),
12966 );
12967 cx.update_editor(|editor, window, cx| {
12968 editor.handle_input("(", window, cx);
12969 });
12970 cx.assert_editor_state(
12971 &"
12972 fn main() {
12973 sample(ˇ)
12974 }
12975 "
12976 .unindent(),
12977 );
12978 cx.editor(|editor, _, _| {
12979 assert!(editor.signature_help_state.task().is_none());
12980 });
12981
12982 let mocked_response = lsp::SignatureHelp {
12983 signatures: vec![lsp::SignatureInformation {
12984 label: "fn sample(param1: u8, param2: u8)".to_string(),
12985 documentation: None,
12986 parameters: Some(vec![
12987 lsp::ParameterInformation {
12988 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12989 documentation: None,
12990 },
12991 lsp::ParameterInformation {
12992 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12993 documentation: None,
12994 },
12995 ]),
12996 active_parameter: None,
12997 }],
12998 active_signature: Some(0),
12999 active_parameter: Some(0),
13000 };
13001
13002 // Ensure that signature_help is called when enabled afte edits
13003 cx.update(|_, cx| {
13004 cx.update_global::<SettingsStore, _>(|settings, cx| {
13005 settings.update_user_settings(cx, |settings| {
13006 settings.editor.auto_signature_help = Some(false);
13007 settings.editor.show_signature_help_after_edits = Some(true);
13008 });
13009 });
13010 });
13011 cx.set_state(
13012 &r#"
13013 fn main() {
13014 sampleˇ
13015 }
13016 "#
13017 .unindent(),
13018 );
13019 cx.update_editor(|editor, window, cx| {
13020 editor.handle_input("(", window, cx);
13021 });
13022 cx.assert_editor_state(
13023 &"
13024 fn main() {
13025 sample(ˇ)
13026 }
13027 "
13028 .unindent(),
13029 );
13030 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13031 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13032 .await;
13033 cx.update_editor(|editor, _, _| {
13034 let signature_help_state = editor.signature_help_state.popover().cloned();
13035 assert!(signature_help_state.is_some());
13036 let signature = signature_help_state.unwrap();
13037 assert_eq!(
13038 signature.signatures[signature.current_signature].label,
13039 "fn sample(param1: u8, param2: u8)"
13040 );
13041 editor.signature_help_state = SignatureHelpState::default();
13042 });
13043
13044 // Ensure that signature_help is called when auto signature help override is enabled
13045 cx.update(|_, cx| {
13046 cx.update_global::<SettingsStore, _>(|settings, cx| {
13047 settings.update_user_settings(cx, |settings| {
13048 settings.editor.auto_signature_help = Some(true);
13049 settings.editor.show_signature_help_after_edits = Some(false);
13050 });
13051 });
13052 });
13053 cx.set_state(
13054 &r#"
13055 fn main() {
13056 sampleˇ
13057 }
13058 "#
13059 .unindent(),
13060 );
13061 cx.update_editor(|editor, window, cx| {
13062 editor.handle_input("(", window, cx);
13063 });
13064 cx.assert_editor_state(
13065 &"
13066 fn main() {
13067 sample(ˇ)
13068 }
13069 "
13070 .unindent(),
13071 );
13072 handle_signature_help_request(&mut cx, mocked_response).await;
13073 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13074 .await;
13075 cx.editor(|editor, _, _| {
13076 let signature_help_state = editor.signature_help_state.popover().cloned();
13077 assert!(signature_help_state.is_some());
13078 let signature = signature_help_state.unwrap();
13079 assert_eq!(
13080 signature.signatures[signature.current_signature].label,
13081 "fn sample(param1: u8, param2: u8)"
13082 );
13083 });
13084}
13085
13086#[gpui::test]
13087async fn test_signature_help(cx: &mut TestAppContext) {
13088 init_test(cx, |_| {});
13089 cx.update(|cx| {
13090 cx.update_global::<SettingsStore, _>(|settings, cx| {
13091 settings.update_user_settings(cx, |settings| {
13092 settings.editor.auto_signature_help = Some(true);
13093 });
13094 });
13095 });
13096
13097 let mut cx = EditorLspTestContext::new_rust(
13098 lsp::ServerCapabilities {
13099 signature_help_provider: Some(lsp::SignatureHelpOptions {
13100 ..Default::default()
13101 }),
13102 ..Default::default()
13103 },
13104 cx,
13105 )
13106 .await;
13107
13108 // A test that directly calls `show_signature_help`
13109 cx.update_editor(|editor, window, cx| {
13110 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13111 });
13112
13113 let mocked_response = lsp::SignatureHelp {
13114 signatures: vec![lsp::SignatureInformation {
13115 label: "fn sample(param1: u8, param2: u8)".to_string(),
13116 documentation: None,
13117 parameters: Some(vec![
13118 lsp::ParameterInformation {
13119 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13120 documentation: None,
13121 },
13122 lsp::ParameterInformation {
13123 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13124 documentation: None,
13125 },
13126 ]),
13127 active_parameter: None,
13128 }],
13129 active_signature: Some(0),
13130 active_parameter: Some(0),
13131 };
13132 handle_signature_help_request(&mut cx, mocked_response).await;
13133
13134 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13135 .await;
13136
13137 cx.editor(|editor, _, _| {
13138 let signature_help_state = editor.signature_help_state.popover().cloned();
13139 assert!(signature_help_state.is_some());
13140 let signature = signature_help_state.unwrap();
13141 assert_eq!(
13142 signature.signatures[signature.current_signature].label,
13143 "fn sample(param1: u8, param2: u8)"
13144 );
13145 });
13146
13147 // When exiting outside from inside the brackets, `signature_help` is closed.
13148 cx.set_state(indoc! {"
13149 fn main() {
13150 sample(ˇ);
13151 }
13152
13153 fn sample(param1: u8, param2: u8) {}
13154 "});
13155
13156 cx.update_editor(|editor, window, cx| {
13157 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13158 s.select_ranges([0..0])
13159 });
13160 });
13161
13162 let mocked_response = lsp::SignatureHelp {
13163 signatures: Vec::new(),
13164 active_signature: None,
13165 active_parameter: None,
13166 };
13167 handle_signature_help_request(&mut cx, mocked_response).await;
13168
13169 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13170 .await;
13171
13172 cx.editor(|editor, _, _| {
13173 assert!(!editor.signature_help_state.is_shown());
13174 });
13175
13176 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
13177 cx.set_state(indoc! {"
13178 fn main() {
13179 sample(ˇ);
13180 }
13181
13182 fn sample(param1: u8, param2: u8) {}
13183 "});
13184
13185 let mocked_response = lsp::SignatureHelp {
13186 signatures: vec![lsp::SignatureInformation {
13187 label: "fn sample(param1: u8, param2: u8)".to_string(),
13188 documentation: None,
13189 parameters: Some(vec![
13190 lsp::ParameterInformation {
13191 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13192 documentation: None,
13193 },
13194 lsp::ParameterInformation {
13195 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13196 documentation: None,
13197 },
13198 ]),
13199 active_parameter: None,
13200 }],
13201 active_signature: Some(0),
13202 active_parameter: Some(0),
13203 };
13204 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13205 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13206 .await;
13207 cx.editor(|editor, _, _| {
13208 assert!(editor.signature_help_state.is_shown());
13209 });
13210
13211 // Restore the popover with more parameter input
13212 cx.set_state(indoc! {"
13213 fn main() {
13214 sample(param1, param2ˇ);
13215 }
13216
13217 fn sample(param1: u8, param2: u8) {}
13218 "});
13219
13220 let mocked_response = lsp::SignatureHelp {
13221 signatures: vec![lsp::SignatureInformation {
13222 label: "fn sample(param1: u8, param2: u8)".to_string(),
13223 documentation: None,
13224 parameters: Some(vec![
13225 lsp::ParameterInformation {
13226 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13227 documentation: None,
13228 },
13229 lsp::ParameterInformation {
13230 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13231 documentation: None,
13232 },
13233 ]),
13234 active_parameter: None,
13235 }],
13236 active_signature: Some(0),
13237 active_parameter: Some(1),
13238 };
13239 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13240 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13241 .await;
13242
13243 // When selecting a range, the popover is gone.
13244 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
13245 cx.update_editor(|editor, window, cx| {
13246 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13247 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13248 })
13249 });
13250 cx.assert_editor_state(indoc! {"
13251 fn main() {
13252 sample(param1, «ˇparam2»);
13253 }
13254
13255 fn sample(param1: u8, param2: u8) {}
13256 "});
13257 cx.editor(|editor, _, _| {
13258 assert!(!editor.signature_help_state.is_shown());
13259 });
13260
13261 // When unselecting again, the popover is back if within the brackets.
13262 cx.update_editor(|editor, window, cx| {
13263 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13264 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13265 })
13266 });
13267 cx.assert_editor_state(indoc! {"
13268 fn main() {
13269 sample(param1, ˇparam2);
13270 }
13271
13272 fn sample(param1: u8, param2: u8) {}
13273 "});
13274 handle_signature_help_request(&mut cx, mocked_response).await;
13275 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13276 .await;
13277 cx.editor(|editor, _, _| {
13278 assert!(editor.signature_help_state.is_shown());
13279 });
13280
13281 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
13282 cx.update_editor(|editor, window, cx| {
13283 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13284 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
13285 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13286 })
13287 });
13288 cx.assert_editor_state(indoc! {"
13289 fn main() {
13290 sample(param1, ˇparam2);
13291 }
13292
13293 fn sample(param1: u8, param2: u8) {}
13294 "});
13295
13296 let mocked_response = lsp::SignatureHelp {
13297 signatures: vec![lsp::SignatureInformation {
13298 label: "fn sample(param1: u8, param2: u8)".to_string(),
13299 documentation: None,
13300 parameters: Some(vec![
13301 lsp::ParameterInformation {
13302 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13303 documentation: None,
13304 },
13305 lsp::ParameterInformation {
13306 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13307 documentation: None,
13308 },
13309 ]),
13310 active_parameter: None,
13311 }],
13312 active_signature: Some(0),
13313 active_parameter: Some(1),
13314 };
13315 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13316 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13317 .await;
13318 cx.update_editor(|editor, _, cx| {
13319 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
13320 });
13321 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13322 .await;
13323 cx.update_editor(|editor, window, cx| {
13324 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13325 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13326 })
13327 });
13328 cx.assert_editor_state(indoc! {"
13329 fn main() {
13330 sample(param1, «ˇparam2»);
13331 }
13332
13333 fn sample(param1: u8, param2: u8) {}
13334 "});
13335 cx.update_editor(|editor, window, cx| {
13336 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13337 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13338 })
13339 });
13340 cx.assert_editor_state(indoc! {"
13341 fn main() {
13342 sample(param1, ˇparam2);
13343 }
13344
13345 fn sample(param1: u8, param2: u8) {}
13346 "});
13347 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
13348 .await;
13349}
13350
13351#[gpui::test]
13352async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
13353 init_test(cx, |_| {});
13354
13355 let mut cx = EditorLspTestContext::new_rust(
13356 lsp::ServerCapabilities {
13357 signature_help_provider: Some(lsp::SignatureHelpOptions {
13358 ..Default::default()
13359 }),
13360 ..Default::default()
13361 },
13362 cx,
13363 )
13364 .await;
13365
13366 cx.set_state(indoc! {"
13367 fn main() {
13368 overloadedˇ
13369 }
13370 "});
13371
13372 cx.update_editor(|editor, window, cx| {
13373 editor.handle_input("(", window, cx);
13374 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13375 });
13376
13377 // Mock response with 3 signatures
13378 let mocked_response = lsp::SignatureHelp {
13379 signatures: vec![
13380 lsp::SignatureInformation {
13381 label: "fn overloaded(x: i32)".to_string(),
13382 documentation: None,
13383 parameters: Some(vec![lsp::ParameterInformation {
13384 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13385 documentation: None,
13386 }]),
13387 active_parameter: None,
13388 },
13389 lsp::SignatureInformation {
13390 label: "fn overloaded(x: i32, y: i32)".to_string(),
13391 documentation: None,
13392 parameters: Some(vec![
13393 lsp::ParameterInformation {
13394 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13395 documentation: None,
13396 },
13397 lsp::ParameterInformation {
13398 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13399 documentation: None,
13400 },
13401 ]),
13402 active_parameter: None,
13403 },
13404 lsp::SignatureInformation {
13405 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
13406 documentation: None,
13407 parameters: Some(vec![
13408 lsp::ParameterInformation {
13409 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13410 documentation: None,
13411 },
13412 lsp::ParameterInformation {
13413 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13414 documentation: None,
13415 },
13416 lsp::ParameterInformation {
13417 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
13418 documentation: None,
13419 },
13420 ]),
13421 active_parameter: None,
13422 },
13423 ],
13424 active_signature: Some(1),
13425 active_parameter: Some(0),
13426 };
13427 handle_signature_help_request(&mut cx, mocked_response).await;
13428
13429 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13430 .await;
13431
13432 // Verify we have multiple signatures and the right one is selected
13433 cx.editor(|editor, _, _| {
13434 let popover = editor.signature_help_state.popover().cloned().unwrap();
13435 assert_eq!(popover.signatures.len(), 3);
13436 // active_signature was 1, so that should be the current
13437 assert_eq!(popover.current_signature, 1);
13438 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
13439 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
13440 assert_eq!(
13441 popover.signatures[2].label,
13442 "fn overloaded(x: i32, y: i32, z: i32)"
13443 );
13444 });
13445
13446 // Test navigation functionality
13447 cx.update_editor(|editor, window, cx| {
13448 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13449 });
13450
13451 cx.editor(|editor, _, _| {
13452 let popover = editor.signature_help_state.popover().cloned().unwrap();
13453 assert_eq!(popover.current_signature, 2);
13454 });
13455
13456 // Test wrap around
13457 cx.update_editor(|editor, window, cx| {
13458 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13459 });
13460
13461 cx.editor(|editor, _, _| {
13462 let popover = editor.signature_help_state.popover().cloned().unwrap();
13463 assert_eq!(popover.current_signature, 0);
13464 });
13465
13466 // Test previous navigation
13467 cx.update_editor(|editor, window, cx| {
13468 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
13469 });
13470
13471 cx.editor(|editor, _, _| {
13472 let popover = editor.signature_help_state.popover().cloned().unwrap();
13473 assert_eq!(popover.current_signature, 2);
13474 });
13475}
13476
13477#[gpui::test]
13478async fn test_completion_mode(cx: &mut TestAppContext) {
13479 init_test(cx, |_| {});
13480 let mut cx = EditorLspTestContext::new_rust(
13481 lsp::ServerCapabilities {
13482 completion_provider: Some(lsp::CompletionOptions {
13483 resolve_provider: Some(true),
13484 ..Default::default()
13485 }),
13486 ..Default::default()
13487 },
13488 cx,
13489 )
13490 .await;
13491
13492 struct Run {
13493 run_description: &'static str,
13494 initial_state: String,
13495 buffer_marked_text: String,
13496 completion_label: &'static str,
13497 completion_text: &'static str,
13498 expected_with_insert_mode: String,
13499 expected_with_replace_mode: String,
13500 expected_with_replace_subsequence_mode: String,
13501 expected_with_replace_suffix_mode: String,
13502 }
13503
13504 let runs = [
13505 Run {
13506 run_description: "Start of word matches completion text",
13507 initial_state: "before ediˇ after".into(),
13508 buffer_marked_text: "before <edi|> after".into(),
13509 completion_label: "editor",
13510 completion_text: "editor",
13511 expected_with_insert_mode: "before editorˇ after".into(),
13512 expected_with_replace_mode: "before editorˇ after".into(),
13513 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13514 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13515 },
13516 Run {
13517 run_description: "Accept same text at the middle of the word",
13518 initial_state: "before ediˇtor after".into(),
13519 buffer_marked_text: "before <edi|tor> after".into(),
13520 completion_label: "editor",
13521 completion_text: "editor",
13522 expected_with_insert_mode: "before editorˇtor after".into(),
13523 expected_with_replace_mode: "before editorˇ after".into(),
13524 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13525 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13526 },
13527 Run {
13528 run_description: "End of word matches completion text -- cursor at end",
13529 initial_state: "before torˇ after".into(),
13530 buffer_marked_text: "before <tor|> after".into(),
13531 completion_label: "editor",
13532 completion_text: "editor",
13533 expected_with_insert_mode: "before editorˇ after".into(),
13534 expected_with_replace_mode: "before editorˇ after".into(),
13535 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13536 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13537 },
13538 Run {
13539 run_description: "End of word matches completion text -- cursor at start",
13540 initial_state: "before ˇtor after".into(),
13541 buffer_marked_text: "before <|tor> after".into(),
13542 completion_label: "editor",
13543 completion_text: "editor",
13544 expected_with_insert_mode: "before editorˇtor after".into(),
13545 expected_with_replace_mode: "before editorˇ after".into(),
13546 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13547 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13548 },
13549 Run {
13550 run_description: "Prepend text containing whitespace",
13551 initial_state: "pˇfield: bool".into(),
13552 buffer_marked_text: "<p|field>: bool".into(),
13553 completion_label: "pub ",
13554 completion_text: "pub ",
13555 expected_with_insert_mode: "pub ˇfield: bool".into(),
13556 expected_with_replace_mode: "pub ˇ: bool".into(),
13557 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
13558 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
13559 },
13560 Run {
13561 run_description: "Add element to start of list",
13562 initial_state: "[element_ˇelement_2]".into(),
13563 buffer_marked_text: "[<element_|element_2>]".into(),
13564 completion_label: "element_1",
13565 completion_text: "element_1",
13566 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
13567 expected_with_replace_mode: "[element_1ˇ]".into(),
13568 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
13569 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
13570 },
13571 Run {
13572 run_description: "Add element to start of list -- first and second elements are equal",
13573 initial_state: "[elˇelement]".into(),
13574 buffer_marked_text: "[<el|element>]".into(),
13575 completion_label: "element",
13576 completion_text: "element",
13577 expected_with_insert_mode: "[elementˇelement]".into(),
13578 expected_with_replace_mode: "[elementˇ]".into(),
13579 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
13580 expected_with_replace_suffix_mode: "[elementˇ]".into(),
13581 },
13582 Run {
13583 run_description: "Ends with matching suffix",
13584 initial_state: "SubˇError".into(),
13585 buffer_marked_text: "<Sub|Error>".into(),
13586 completion_label: "SubscriptionError",
13587 completion_text: "SubscriptionError",
13588 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
13589 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13590 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13591 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
13592 },
13593 Run {
13594 run_description: "Suffix is a subsequence -- contiguous",
13595 initial_state: "SubˇErr".into(),
13596 buffer_marked_text: "<Sub|Err>".into(),
13597 completion_label: "SubscriptionError",
13598 completion_text: "SubscriptionError",
13599 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
13600 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13601 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13602 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
13603 },
13604 Run {
13605 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
13606 initial_state: "Suˇscrirr".into(),
13607 buffer_marked_text: "<Su|scrirr>".into(),
13608 completion_label: "SubscriptionError",
13609 completion_text: "SubscriptionError",
13610 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
13611 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13612 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13613 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
13614 },
13615 Run {
13616 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
13617 initial_state: "foo(indˇix)".into(),
13618 buffer_marked_text: "foo(<ind|ix>)".into(),
13619 completion_label: "node_index",
13620 completion_text: "node_index",
13621 expected_with_insert_mode: "foo(node_indexˇix)".into(),
13622 expected_with_replace_mode: "foo(node_indexˇ)".into(),
13623 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
13624 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
13625 },
13626 Run {
13627 run_description: "Replace range ends before cursor - should extend to cursor",
13628 initial_state: "before editˇo after".into(),
13629 buffer_marked_text: "before <{ed}>it|o after".into(),
13630 completion_label: "editor",
13631 completion_text: "editor",
13632 expected_with_insert_mode: "before editorˇo after".into(),
13633 expected_with_replace_mode: "before editorˇo after".into(),
13634 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
13635 expected_with_replace_suffix_mode: "before editorˇo after".into(),
13636 },
13637 Run {
13638 run_description: "Uses label for suffix matching",
13639 initial_state: "before ediˇtor after".into(),
13640 buffer_marked_text: "before <edi|tor> after".into(),
13641 completion_label: "editor",
13642 completion_text: "editor()",
13643 expected_with_insert_mode: "before editor()ˇtor after".into(),
13644 expected_with_replace_mode: "before editor()ˇ after".into(),
13645 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
13646 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
13647 },
13648 Run {
13649 run_description: "Case insensitive subsequence and suffix matching",
13650 initial_state: "before EDiˇtoR after".into(),
13651 buffer_marked_text: "before <EDi|toR> after".into(),
13652 completion_label: "editor",
13653 completion_text: "editor",
13654 expected_with_insert_mode: "before editorˇtoR after".into(),
13655 expected_with_replace_mode: "before editorˇ after".into(),
13656 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13657 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13658 },
13659 ];
13660
13661 for run in runs {
13662 let run_variations = [
13663 (LspInsertMode::Insert, run.expected_with_insert_mode),
13664 (LspInsertMode::Replace, run.expected_with_replace_mode),
13665 (
13666 LspInsertMode::ReplaceSubsequence,
13667 run.expected_with_replace_subsequence_mode,
13668 ),
13669 (
13670 LspInsertMode::ReplaceSuffix,
13671 run.expected_with_replace_suffix_mode,
13672 ),
13673 ];
13674
13675 for (lsp_insert_mode, expected_text) in run_variations {
13676 eprintln!(
13677 "run = {:?}, mode = {lsp_insert_mode:.?}",
13678 run.run_description,
13679 );
13680
13681 update_test_language_settings(&mut cx, |settings| {
13682 settings.defaults.completions = Some(CompletionSettingsContent {
13683 lsp_insert_mode: Some(lsp_insert_mode),
13684 words: Some(WordsCompletionMode::Disabled),
13685 words_min_length: Some(0),
13686 ..Default::default()
13687 });
13688 });
13689
13690 cx.set_state(&run.initial_state);
13691 cx.update_editor(|editor, window, cx| {
13692 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13693 });
13694
13695 let counter = Arc::new(AtomicUsize::new(0));
13696 handle_completion_request_with_insert_and_replace(
13697 &mut cx,
13698 &run.buffer_marked_text,
13699 vec![(run.completion_label, run.completion_text)],
13700 counter.clone(),
13701 )
13702 .await;
13703 cx.condition(|editor, _| editor.context_menu_visible())
13704 .await;
13705 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13706
13707 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13708 editor
13709 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13710 .unwrap()
13711 });
13712 cx.assert_editor_state(&expected_text);
13713 handle_resolve_completion_request(&mut cx, None).await;
13714 apply_additional_edits.await.unwrap();
13715 }
13716 }
13717}
13718
13719#[gpui::test]
13720async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
13721 init_test(cx, |_| {});
13722 let mut cx = EditorLspTestContext::new_rust(
13723 lsp::ServerCapabilities {
13724 completion_provider: Some(lsp::CompletionOptions {
13725 resolve_provider: Some(true),
13726 ..Default::default()
13727 }),
13728 ..Default::default()
13729 },
13730 cx,
13731 )
13732 .await;
13733
13734 let initial_state = "SubˇError";
13735 let buffer_marked_text = "<Sub|Error>";
13736 let completion_text = "SubscriptionError";
13737 let expected_with_insert_mode = "SubscriptionErrorˇError";
13738 let expected_with_replace_mode = "SubscriptionErrorˇ";
13739
13740 update_test_language_settings(&mut cx, |settings| {
13741 settings.defaults.completions = Some(CompletionSettingsContent {
13742 words: Some(WordsCompletionMode::Disabled),
13743 words_min_length: Some(0),
13744 // set the opposite here to ensure that the action is overriding the default behavior
13745 lsp_insert_mode: Some(LspInsertMode::Insert),
13746 ..Default::default()
13747 });
13748 });
13749
13750 cx.set_state(initial_state);
13751 cx.update_editor(|editor, window, cx| {
13752 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13753 });
13754
13755 let counter = Arc::new(AtomicUsize::new(0));
13756 handle_completion_request_with_insert_and_replace(
13757 &mut cx,
13758 buffer_marked_text,
13759 vec![(completion_text, completion_text)],
13760 counter.clone(),
13761 )
13762 .await;
13763 cx.condition(|editor, _| editor.context_menu_visible())
13764 .await;
13765 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13766
13767 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13768 editor
13769 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13770 .unwrap()
13771 });
13772 cx.assert_editor_state(expected_with_replace_mode);
13773 handle_resolve_completion_request(&mut cx, None).await;
13774 apply_additional_edits.await.unwrap();
13775
13776 update_test_language_settings(&mut cx, |settings| {
13777 settings.defaults.completions = Some(CompletionSettingsContent {
13778 words: Some(WordsCompletionMode::Disabled),
13779 words_min_length: Some(0),
13780 // set the opposite here to ensure that the action is overriding the default behavior
13781 lsp_insert_mode: Some(LspInsertMode::Replace),
13782 ..Default::default()
13783 });
13784 });
13785
13786 cx.set_state(initial_state);
13787 cx.update_editor(|editor, window, cx| {
13788 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13789 });
13790 handle_completion_request_with_insert_and_replace(
13791 &mut cx,
13792 buffer_marked_text,
13793 vec![(completion_text, completion_text)],
13794 counter.clone(),
13795 )
13796 .await;
13797 cx.condition(|editor, _| editor.context_menu_visible())
13798 .await;
13799 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
13800
13801 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13802 editor
13803 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
13804 .unwrap()
13805 });
13806 cx.assert_editor_state(expected_with_insert_mode);
13807 handle_resolve_completion_request(&mut cx, None).await;
13808 apply_additional_edits.await.unwrap();
13809}
13810
13811#[gpui::test]
13812async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
13813 init_test(cx, |_| {});
13814 let mut cx = EditorLspTestContext::new_rust(
13815 lsp::ServerCapabilities {
13816 completion_provider: Some(lsp::CompletionOptions {
13817 resolve_provider: Some(true),
13818 ..Default::default()
13819 }),
13820 ..Default::default()
13821 },
13822 cx,
13823 )
13824 .await;
13825
13826 // scenario: surrounding text matches completion text
13827 let completion_text = "to_offset";
13828 let initial_state = indoc! {"
13829 1. buf.to_offˇsuffix
13830 2. buf.to_offˇsuf
13831 3. buf.to_offˇfix
13832 4. buf.to_offˇ
13833 5. into_offˇensive
13834 6. ˇsuffix
13835 7. let ˇ //
13836 8. aaˇzz
13837 9. buf.to_off«zzzzzˇ»suffix
13838 10. buf.«ˇzzzzz»suffix
13839 11. to_off«ˇzzzzz»
13840
13841 buf.to_offˇsuffix // newest cursor
13842 "};
13843 let completion_marked_buffer = indoc! {"
13844 1. buf.to_offsuffix
13845 2. buf.to_offsuf
13846 3. buf.to_offfix
13847 4. buf.to_off
13848 5. into_offensive
13849 6. suffix
13850 7. let //
13851 8. aazz
13852 9. buf.to_offzzzzzsuffix
13853 10. buf.zzzzzsuffix
13854 11. to_offzzzzz
13855
13856 buf.<to_off|suffix> // newest cursor
13857 "};
13858 let expected = indoc! {"
13859 1. buf.to_offsetˇ
13860 2. buf.to_offsetˇsuf
13861 3. buf.to_offsetˇfix
13862 4. buf.to_offsetˇ
13863 5. into_offsetˇensive
13864 6. to_offsetˇsuffix
13865 7. let to_offsetˇ //
13866 8. aato_offsetˇzz
13867 9. buf.to_offsetˇ
13868 10. buf.to_offsetˇsuffix
13869 11. to_offsetˇ
13870
13871 buf.to_offsetˇ // newest cursor
13872 "};
13873 cx.set_state(initial_state);
13874 cx.update_editor(|editor, window, cx| {
13875 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13876 });
13877 handle_completion_request_with_insert_and_replace(
13878 &mut cx,
13879 completion_marked_buffer,
13880 vec![(completion_text, completion_text)],
13881 Arc::new(AtomicUsize::new(0)),
13882 )
13883 .await;
13884 cx.condition(|editor, _| editor.context_menu_visible())
13885 .await;
13886 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13887 editor
13888 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13889 .unwrap()
13890 });
13891 cx.assert_editor_state(expected);
13892 handle_resolve_completion_request(&mut cx, None).await;
13893 apply_additional_edits.await.unwrap();
13894
13895 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
13896 let completion_text = "foo_and_bar";
13897 let initial_state = indoc! {"
13898 1. ooanbˇ
13899 2. zooanbˇ
13900 3. ooanbˇz
13901 4. zooanbˇz
13902 5. ooanˇ
13903 6. oanbˇ
13904
13905 ooanbˇ
13906 "};
13907 let completion_marked_buffer = indoc! {"
13908 1. ooanb
13909 2. zooanb
13910 3. ooanbz
13911 4. zooanbz
13912 5. ooan
13913 6. oanb
13914
13915 <ooanb|>
13916 "};
13917 let expected = indoc! {"
13918 1. foo_and_barˇ
13919 2. zfoo_and_barˇ
13920 3. foo_and_barˇz
13921 4. zfoo_and_barˇz
13922 5. ooanfoo_and_barˇ
13923 6. oanbfoo_and_barˇ
13924
13925 foo_and_barˇ
13926 "};
13927 cx.set_state(initial_state);
13928 cx.update_editor(|editor, window, cx| {
13929 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13930 });
13931 handle_completion_request_with_insert_and_replace(
13932 &mut cx,
13933 completion_marked_buffer,
13934 vec![(completion_text, completion_text)],
13935 Arc::new(AtomicUsize::new(0)),
13936 )
13937 .await;
13938 cx.condition(|editor, _| editor.context_menu_visible())
13939 .await;
13940 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13941 editor
13942 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13943 .unwrap()
13944 });
13945 cx.assert_editor_state(expected);
13946 handle_resolve_completion_request(&mut cx, None).await;
13947 apply_additional_edits.await.unwrap();
13948
13949 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
13950 // (expects the same as if it was inserted at the end)
13951 let completion_text = "foo_and_bar";
13952 let initial_state = indoc! {"
13953 1. ooˇanb
13954 2. zooˇanb
13955 3. ooˇanbz
13956 4. zooˇanbz
13957
13958 ooˇanb
13959 "};
13960 let completion_marked_buffer = indoc! {"
13961 1. ooanb
13962 2. zooanb
13963 3. ooanbz
13964 4. zooanbz
13965
13966 <oo|anb>
13967 "};
13968 let expected = indoc! {"
13969 1. foo_and_barˇ
13970 2. zfoo_and_barˇ
13971 3. foo_and_barˇz
13972 4. zfoo_and_barˇz
13973
13974 foo_and_barˇ
13975 "};
13976 cx.set_state(initial_state);
13977 cx.update_editor(|editor, window, cx| {
13978 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13979 });
13980 handle_completion_request_with_insert_and_replace(
13981 &mut cx,
13982 completion_marked_buffer,
13983 vec![(completion_text, completion_text)],
13984 Arc::new(AtomicUsize::new(0)),
13985 )
13986 .await;
13987 cx.condition(|editor, _| editor.context_menu_visible())
13988 .await;
13989 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13990 editor
13991 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13992 .unwrap()
13993 });
13994 cx.assert_editor_state(expected);
13995 handle_resolve_completion_request(&mut cx, None).await;
13996 apply_additional_edits.await.unwrap();
13997}
13998
13999// This used to crash
14000#[gpui::test]
14001async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
14002 init_test(cx, |_| {});
14003
14004 let buffer_text = indoc! {"
14005 fn main() {
14006 10.satu;
14007
14008 //
14009 // separate cursors so they open in different excerpts (manually reproducible)
14010 //
14011
14012 10.satu20;
14013 }
14014 "};
14015 let multibuffer_text_with_selections = indoc! {"
14016 fn main() {
14017 10.satuˇ;
14018
14019 //
14020
14021 //
14022
14023 10.satuˇ20;
14024 }
14025 "};
14026 let expected_multibuffer = indoc! {"
14027 fn main() {
14028 10.saturating_sub()ˇ;
14029
14030 //
14031
14032 //
14033
14034 10.saturating_sub()ˇ;
14035 }
14036 "};
14037
14038 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
14039 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
14040
14041 let fs = FakeFs::new(cx.executor());
14042 fs.insert_tree(
14043 path!("/a"),
14044 json!({
14045 "main.rs": buffer_text,
14046 }),
14047 )
14048 .await;
14049
14050 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14051 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14052 language_registry.add(rust_lang());
14053 let mut fake_servers = language_registry.register_fake_lsp(
14054 "Rust",
14055 FakeLspAdapter {
14056 capabilities: lsp::ServerCapabilities {
14057 completion_provider: Some(lsp::CompletionOptions {
14058 resolve_provider: None,
14059 ..lsp::CompletionOptions::default()
14060 }),
14061 ..lsp::ServerCapabilities::default()
14062 },
14063 ..FakeLspAdapter::default()
14064 },
14065 );
14066 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14067 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14068 let buffer = project
14069 .update(cx, |project, cx| {
14070 project.open_local_buffer(path!("/a/main.rs"), cx)
14071 })
14072 .await
14073 .unwrap();
14074
14075 let multi_buffer = cx.new(|cx| {
14076 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
14077 multi_buffer.push_excerpts(
14078 buffer.clone(),
14079 [ExcerptRange::new(0..first_excerpt_end)],
14080 cx,
14081 );
14082 multi_buffer.push_excerpts(
14083 buffer.clone(),
14084 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
14085 cx,
14086 );
14087 multi_buffer
14088 });
14089
14090 let editor = workspace
14091 .update(cx, |_, window, cx| {
14092 cx.new(|cx| {
14093 Editor::new(
14094 EditorMode::Full {
14095 scale_ui_elements_with_buffer_font_size: false,
14096 show_active_line_background: false,
14097 sized_by_content: false,
14098 },
14099 multi_buffer.clone(),
14100 Some(project.clone()),
14101 window,
14102 cx,
14103 )
14104 })
14105 })
14106 .unwrap();
14107
14108 let pane = workspace
14109 .update(cx, |workspace, _, _| workspace.active_pane().clone())
14110 .unwrap();
14111 pane.update_in(cx, |pane, window, cx| {
14112 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
14113 });
14114
14115 let fake_server = fake_servers.next().await.unwrap();
14116
14117 editor.update_in(cx, |editor, window, cx| {
14118 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14119 s.select_ranges([
14120 Point::new(1, 11)..Point::new(1, 11),
14121 Point::new(7, 11)..Point::new(7, 11),
14122 ])
14123 });
14124
14125 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
14126 });
14127
14128 editor.update_in(cx, |editor, window, cx| {
14129 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14130 });
14131
14132 fake_server
14133 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14134 let completion_item = lsp::CompletionItem {
14135 label: "saturating_sub()".into(),
14136 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
14137 lsp::InsertReplaceEdit {
14138 new_text: "saturating_sub()".to_owned(),
14139 insert: lsp::Range::new(
14140 lsp::Position::new(7, 7),
14141 lsp::Position::new(7, 11),
14142 ),
14143 replace: lsp::Range::new(
14144 lsp::Position::new(7, 7),
14145 lsp::Position::new(7, 13),
14146 ),
14147 },
14148 )),
14149 ..lsp::CompletionItem::default()
14150 };
14151
14152 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
14153 })
14154 .next()
14155 .await
14156 .unwrap();
14157
14158 cx.condition(&editor, |editor, _| editor.context_menu_visible())
14159 .await;
14160
14161 editor
14162 .update_in(cx, |editor, window, cx| {
14163 editor
14164 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14165 .unwrap()
14166 })
14167 .await
14168 .unwrap();
14169
14170 editor.update(cx, |editor, cx| {
14171 assert_text_with_selections(editor, expected_multibuffer, cx);
14172 })
14173}
14174
14175#[gpui::test]
14176async fn test_completion(cx: &mut TestAppContext) {
14177 init_test(cx, |_| {});
14178
14179 let mut cx = EditorLspTestContext::new_rust(
14180 lsp::ServerCapabilities {
14181 completion_provider: Some(lsp::CompletionOptions {
14182 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14183 resolve_provider: Some(true),
14184 ..Default::default()
14185 }),
14186 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14187 ..Default::default()
14188 },
14189 cx,
14190 )
14191 .await;
14192 let counter = Arc::new(AtomicUsize::new(0));
14193
14194 cx.set_state(indoc! {"
14195 oneˇ
14196 two
14197 three
14198 "});
14199 cx.simulate_keystroke(".");
14200 handle_completion_request(
14201 indoc! {"
14202 one.|<>
14203 two
14204 three
14205 "},
14206 vec!["first_completion", "second_completion"],
14207 true,
14208 counter.clone(),
14209 &mut cx,
14210 )
14211 .await;
14212 cx.condition(|editor, _| editor.context_menu_visible())
14213 .await;
14214 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14215
14216 let _handler = handle_signature_help_request(
14217 &mut cx,
14218 lsp::SignatureHelp {
14219 signatures: vec![lsp::SignatureInformation {
14220 label: "test signature".to_string(),
14221 documentation: None,
14222 parameters: Some(vec![lsp::ParameterInformation {
14223 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
14224 documentation: None,
14225 }]),
14226 active_parameter: None,
14227 }],
14228 active_signature: None,
14229 active_parameter: None,
14230 },
14231 );
14232 cx.update_editor(|editor, window, cx| {
14233 assert!(
14234 !editor.signature_help_state.is_shown(),
14235 "No signature help was called for"
14236 );
14237 editor.show_signature_help(&ShowSignatureHelp, window, cx);
14238 });
14239 cx.run_until_parked();
14240 cx.update_editor(|editor, _, _| {
14241 assert!(
14242 !editor.signature_help_state.is_shown(),
14243 "No signature help should be shown when completions menu is open"
14244 );
14245 });
14246
14247 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14248 editor.context_menu_next(&Default::default(), window, cx);
14249 editor
14250 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14251 .unwrap()
14252 });
14253 cx.assert_editor_state(indoc! {"
14254 one.second_completionˇ
14255 two
14256 three
14257 "});
14258
14259 handle_resolve_completion_request(
14260 &mut cx,
14261 Some(vec![
14262 (
14263 //This overlaps with the primary completion edit which is
14264 //misbehavior from the LSP spec, test that we filter it out
14265 indoc! {"
14266 one.second_ˇcompletion
14267 two
14268 threeˇ
14269 "},
14270 "overlapping additional edit",
14271 ),
14272 (
14273 indoc! {"
14274 one.second_completion
14275 two
14276 threeˇ
14277 "},
14278 "\nadditional edit",
14279 ),
14280 ]),
14281 )
14282 .await;
14283 apply_additional_edits.await.unwrap();
14284 cx.assert_editor_state(indoc! {"
14285 one.second_completionˇ
14286 two
14287 three
14288 additional edit
14289 "});
14290
14291 cx.set_state(indoc! {"
14292 one.second_completion
14293 twoˇ
14294 threeˇ
14295 additional edit
14296 "});
14297 cx.simulate_keystroke(" ");
14298 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14299 cx.simulate_keystroke("s");
14300 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14301
14302 cx.assert_editor_state(indoc! {"
14303 one.second_completion
14304 two sˇ
14305 three sˇ
14306 additional edit
14307 "});
14308 handle_completion_request(
14309 indoc! {"
14310 one.second_completion
14311 two s
14312 three <s|>
14313 additional edit
14314 "},
14315 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14316 true,
14317 counter.clone(),
14318 &mut cx,
14319 )
14320 .await;
14321 cx.condition(|editor, _| editor.context_menu_visible())
14322 .await;
14323 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14324
14325 cx.simulate_keystroke("i");
14326
14327 handle_completion_request(
14328 indoc! {"
14329 one.second_completion
14330 two si
14331 three <si|>
14332 additional edit
14333 "},
14334 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14335 true,
14336 counter.clone(),
14337 &mut cx,
14338 )
14339 .await;
14340 cx.condition(|editor, _| editor.context_menu_visible())
14341 .await;
14342 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14343
14344 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14345 editor
14346 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14347 .unwrap()
14348 });
14349 cx.assert_editor_state(indoc! {"
14350 one.second_completion
14351 two sixth_completionˇ
14352 three sixth_completionˇ
14353 additional edit
14354 "});
14355
14356 apply_additional_edits.await.unwrap();
14357
14358 update_test_language_settings(&mut cx, |settings| {
14359 settings.defaults.show_completions_on_input = Some(false);
14360 });
14361 cx.set_state("editorˇ");
14362 cx.simulate_keystroke(".");
14363 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14364 cx.simulate_keystrokes("c l o");
14365 cx.assert_editor_state("editor.cloˇ");
14366 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14367 cx.update_editor(|editor, window, cx| {
14368 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14369 });
14370 handle_completion_request(
14371 "editor.<clo|>",
14372 vec!["close", "clobber"],
14373 true,
14374 counter.clone(),
14375 &mut cx,
14376 )
14377 .await;
14378 cx.condition(|editor, _| editor.context_menu_visible())
14379 .await;
14380 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
14381
14382 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14383 editor
14384 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14385 .unwrap()
14386 });
14387 cx.assert_editor_state("editor.clobberˇ");
14388 handle_resolve_completion_request(&mut cx, None).await;
14389 apply_additional_edits.await.unwrap();
14390}
14391
14392#[gpui::test]
14393async fn test_completion_reuse(cx: &mut TestAppContext) {
14394 init_test(cx, |_| {});
14395
14396 let mut cx = EditorLspTestContext::new_rust(
14397 lsp::ServerCapabilities {
14398 completion_provider: Some(lsp::CompletionOptions {
14399 trigger_characters: Some(vec![".".to_string()]),
14400 ..Default::default()
14401 }),
14402 ..Default::default()
14403 },
14404 cx,
14405 )
14406 .await;
14407
14408 let counter = Arc::new(AtomicUsize::new(0));
14409 cx.set_state("objˇ");
14410 cx.simulate_keystroke(".");
14411
14412 // Initial completion request returns complete results
14413 let is_incomplete = false;
14414 handle_completion_request(
14415 "obj.|<>",
14416 vec!["a", "ab", "abc"],
14417 is_incomplete,
14418 counter.clone(),
14419 &mut cx,
14420 )
14421 .await;
14422 cx.run_until_parked();
14423 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14424 cx.assert_editor_state("obj.ˇ");
14425 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14426
14427 // Type "a" - filters existing completions
14428 cx.simulate_keystroke("a");
14429 cx.run_until_parked();
14430 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14431 cx.assert_editor_state("obj.aˇ");
14432 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14433
14434 // Type "b" - filters existing completions
14435 cx.simulate_keystroke("b");
14436 cx.run_until_parked();
14437 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14438 cx.assert_editor_state("obj.abˇ");
14439 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14440
14441 // Type "c" - filters existing completions
14442 cx.simulate_keystroke("c");
14443 cx.run_until_parked();
14444 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14445 cx.assert_editor_state("obj.abcˇ");
14446 check_displayed_completions(vec!["abc"], &mut cx);
14447
14448 // Backspace to delete "c" - filters existing completions
14449 cx.update_editor(|editor, window, cx| {
14450 editor.backspace(&Backspace, window, cx);
14451 });
14452 cx.run_until_parked();
14453 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14454 cx.assert_editor_state("obj.abˇ");
14455 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14456
14457 // Moving cursor to the left dismisses menu.
14458 cx.update_editor(|editor, window, cx| {
14459 editor.move_left(&MoveLeft, window, cx);
14460 });
14461 cx.run_until_parked();
14462 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14463 cx.assert_editor_state("obj.aˇb");
14464 cx.update_editor(|editor, _, _| {
14465 assert_eq!(editor.context_menu_visible(), false);
14466 });
14467
14468 // Type "b" - new request
14469 cx.simulate_keystroke("b");
14470 let is_incomplete = false;
14471 handle_completion_request(
14472 "obj.<ab|>a",
14473 vec!["ab", "abc"],
14474 is_incomplete,
14475 counter.clone(),
14476 &mut cx,
14477 )
14478 .await;
14479 cx.run_until_parked();
14480 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14481 cx.assert_editor_state("obj.abˇb");
14482 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14483
14484 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
14485 cx.update_editor(|editor, window, cx| {
14486 editor.backspace(&Backspace, window, cx);
14487 });
14488 let is_incomplete = false;
14489 handle_completion_request(
14490 "obj.<a|>b",
14491 vec!["a", "ab", "abc"],
14492 is_incomplete,
14493 counter.clone(),
14494 &mut cx,
14495 )
14496 .await;
14497 cx.run_until_parked();
14498 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14499 cx.assert_editor_state("obj.aˇb");
14500 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14501
14502 // Backspace to delete "a" - dismisses menu.
14503 cx.update_editor(|editor, window, cx| {
14504 editor.backspace(&Backspace, window, cx);
14505 });
14506 cx.run_until_parked();
14507 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14508 cx.assert_editor_state("obj.ˇb");
14509 cx.update_editor(|editor, _, _| {
14510 assert_eq!(editor.context_menu_visible(), false);
14511 });
14512}
14513
14514#[gpui::test]
14515async fn test_word_completion(cx: &mut TestAppContext) {
14516 let lsp_fetch_timeout_ms = 10;
14517 init_test(cx, |language_settings| {
14518 language_settings.defaults.completions = Some(CompletionSettingsContent {
14519 words_min_length: Some(0),
14520 lsp_fetch_timeout_ms: Some(10),
14521 lsp_insert_mode: Some(LspInsertMode::Insert),
14522 ..Default::default()
14523 });
14524 });
14525
14526 let mut cx = EditorLspTestContext::new_rust(
14527 lsp::ServerCapabilities {
14528 completion_provider: Some(lsp::CompletionOptions {
14529 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14530 ..lsp::CompletionOptions::default()
14531 }),
14532 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14533 ..lsp::ServerCapabilities::default()
14534 },
14535 cx,
14536 )
14537 .await;
14538
14539 let throttle_completions = Arc::new(AtomicBool::new(false));
14540
14541 let lsp_throttle_completions = throttle_completions.clone();
14542 let _completion_requests_handler =
14543 cx.lsp
14544 .server
14545 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
14546 let lsp_throttle_completions = lsp_throttle_completions.clone();
14547 let cx = cx.clone();
14548 async move {
14549 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
14550 cx.background_executor()
14551 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
14552 .await;
14553 }
14554 Ok(Some(lsp::CompletionResponse::Array(vec![
14555 lsp::CompletionItem {
14556 label: "first".into(),
14557 ..lsp::CompletionItem::default()
14558 },
14559 lsp::CompletionItem {
14560 label: "last".into(),
14561 ..lsp::CompletionItem::default()
14562 },
14563 ])))
14564 }
14565 });
14566
14567 cx.set_state(indoc! {"
14568 oneˇ
14569 two
14570 three
14571 "});
14572 cx.simulate_keystroke(".");
14573 cx.executor().run_until_parked();
14574 cx.condition(|editor, _| editor.context_menu_visible())
14575 .await;
14576 cx.update_editor(|editor, window, cx| {
14577 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14578 {
14579 assert_eq!(
14580 completion_menu_entries(menu),
14581 &["first", "last"],
14582 "When LSP server is fast to reply, no fallback word completions are used"
14583 );
14584 } else {
14585 panic!("expected completion menu to be open");
14586 }
14587 editor.cancel(&Cancel, window, cx);
14588 });
14589 cx.executor().run_until_parked();
14590 cx.condition(|editor, _| !editor.context_menu_visible())
14591 .await;
14592
14593 throttle_completions.store(true, atomic::Ordering::Release);
14594 cx.simulate_keystroke(".");
14595 cx.executor()
14596 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
14597 cx.executor().run_until_parked();
14598 cx.condition(|editor, _| editor.context_menu_visible())
14599 .await;
14600 cx.update_editor(|editor, _, _| {
14601 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14602 {
14603 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
14604 "When LSP server is slow, document words can be shown instead, if configured accordingly");
14605 } else {
14606 panic!("expected completion menu to be open");
14607 }
14608 });
14609}
14610
14611#[gpui::test]
14612async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
14613 init_test(cx, |language_settings| {
14614 language_settings.defaults.completions = Some(CompletionSettingsContent {
14615 words: Some(WordsCompletionMode::Enabled),
14616 words_min_length: Some(0),
14617 lsp_insert_mode: Some(LspInsertMode::Insert),
14618 ..Default::default()
14619 });
14620 });
14621
14622 let mut cx = EditorLspTestContext::new_rust(
14623 lsp::ServerCapabilities {
14624 completion_provider: Some(lsp::CompletionOptions {
14625 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14626 ..lsp::CompletionOptions::default()
14627 }),
14628 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14629 ..lsp::ServerCapabilities::default()
14630 },
14631 cx,
14632 )
14633 .await;
14634
14635 let _completion_requests_handler =
14636 cx.lsp
14637 .server
14638 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14639 Ok(Some(lsp::CompletionResponse::Array(vec![
14640 lsp::CompletionItem {
14641 label: "first".into(),
14642 ..lsp::CompletionItem::default()
14643 },
14644 lsp::CompletionItem {
14645 label: "last".into(),
14646 ..lsp::CompletionItem::default()
14647 },
14648 ])))
14649 });
14650
14651 cx.set_state(indoc! {"ˇ
14652 first
14653 last
14654 second
14655 "});
14656 cx.simulate_keystroke(".");
14657 cx.executor().run_until_parked();
14658 cx.condition(|editor, _| editor.context_menu_visible())
14659 .await;
14660 cx.update_editor(|editor, _, _| {
14661 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14662 {
14663 assert_eq!(
14664 completion_menu_entries(menu),
14665 &["first", "last", "second"],
14666 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
14667 );
14668 } else {
14669 panic!("expected completion menu to be open");
14670 }
14671 });
14672}
14673
14674#[gpui::test]
14675async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
14676 init_test(cx, |language_settings| {
14677 language_settings.defaults.completions = Some(CompletionSettingsContent {
14678 words: Some(WordsCompletionMode::Disabled),
14679 words_min_length: Some(0),
14680 lsp_insert_mode: Some(LspInsertMode::Insert),
14681 ..Default::default()
14682 });
14683 });
14684
14685 let mut cx = EditorLspTestContext::new_rust(
14686 lsp::ServerCapabilities {
14687 completion_provider: Some(lsp::CompletionOptions {
14688 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14689 ..lsp::CompletionOptions::default()
14690 }),
14691 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14692 ..lsp::ServerCapabilities::default()
14693 },
14694 cx,
14695 )
14696 .await;
14697
14698 let _completion_requests_handler =
14699 cx.lsp
14700 .server
14701 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14702 panic!("LSP completions should not be queried when dealing with word completions")
14703 });
14704
14705 cx.set_state(indoc! {"ˇ
14706 first
14707 last
14708 second
14709 "});
14710 cx.update_editor(|editor, window, cx| {
14711 editor.show_word_completions(&ShowWordCompletions, window, cx);
14712 });
14713 cx.executor().run_until_parked();
14714 cx.condition(|editor, _| editor.context_menu_visible())
14715 .await;
14716 cx.update_editor(|editor, _, _| {
14717 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14718 {
14719 assert_eq!(
14720 completion_menu_entries(menu),
14721 &["first", "last", "second"],
14722 "`ShowWordCompletions` action should show word completions"
14723 );
14724 } else {
14725 panic!("expected completion menu to be open");
14726 }
14727 });
14728
14729 cx.simulate_keystroke("l");
14730 cx.executor().run_until_parked();
14731 cx.condition(|editor, _| editor.context_menu_visible())
14732 .await;
14733 cx.update_editor(|editor, _, _| {
14734 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14735 {
14736 assert_eq!(
14737 completion_menu_entries(menu),
14738 &["last"],
14739 "After showing word completions, further editing should filter them and not query the LSP"
14740 );
14741 } else {
14742 panic!("expected completion menu to be open");
14743 }
14744 });
14745}
14746
14747#[gpui::test]
14748async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
14749 init_test(cx, |language_settings| {
14750 language_settings.defaults.completions = Some(CompletionSettingsContent {
14751 words_min_length: Some(0),
14752 lsp: Some(false),
14753 lsp_insert_mode: Some(LspInsertMode::Insert),
14754 ..Default::default()
14755 });
14756 });
14757
14758 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14759
14760 cx.set_state(indoc! {"ˇ
14761 0_usize
14762 let
14763 33
14764 4.5f32
14765 "});
14766 cx.update_editor(|editor, window, cx| {
14767 editor.show_completions(&ShowCompletions::default(), window, cx);
14768 });
14769 cx.executor().run_until_parked();
14770 cx.condition(|editor, _| editor.context_menu_visible())
14771 .await;
14772 cx.update_editor(|editor, window, cx| {
14773 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14774 {
14775 assert_eq!(
14776 completion_menu_entries(menu),
14777 &["let"],
14778 "With no digits in the completion query, no digits should be in the word completions"
14779 );
14780 } else {
14781 panic!("expected completion menu to be open");
14782 }
14783 editor.cancel(&Cancel, window, cx);
14784 });
14785
14786 cx.set_state(indoc! {"3ˇ
14787 0_usize
14788 let
14789 3
14790 33.35f32
14791 "});
14792 cx.update_editor(|editor, window, cx| {
14793 editor.show_completions(&ShowCompletions::default(), window, cx);
14794 });
14795 cx.executor().run_until_parked();
14796 cx.condition(|editor, _| editor.context_menu_visible())
14797 .await;
14798 cx.update_editor(|editor, _, _| {
14799 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14800 {
14801 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
14802 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
14803 } else {
14804 panic!("expected completion menu to be open");
14805 }
14806 });
14807}
14808
14809#[gpui::test]
14810async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
14811 init_test(cx, |language_settings| {
14812 language_settings.defaults.completions = Some(CompletionSettingsContent {
14813 words: Some(WordsCompletionMode::Enabled),
14814 words_min_length: Some(3),
14815 lsp_insert_mode: Some(LspInsertMode::Insert),
14816 ..Default::default()
14817 });
14818 });
14819
14820 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14821 cx.set_state(indoc! {"ˇ
14822 wow
14823 wowen
14824 wowser
14825 "});
14826 cx.simulate_keystroke("w");
14827 cx.executor().run_until_parked();
14828 cx.update_editor(|editor, _, _| {
14829 if editor.context_menu.borrow_mut().is_some() {
14830 panic!(
14831 "expected completion menu to be hidden, as words completion threshold is not met"
14832 );
14833 }
14834 });
14835
14836 cx.update_editor(|editor, window, cx| {
14837 editor.show_word_completions(&ShowWordCompletions, window, cx);
14838 });
14839 cx.executor().run_until_parked();
14840 cx.update_editor(|editor, window, cx| {
14841 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14842 {
14843 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");
14844 } else {
14845 panic!("expected completion menu to be open after the word completions are called with an action");
14846 }
14847
14848 editor.cancel(&Cancel, window, cx);
14849 });
14850 cx.update_editor(|editor, _, _| {
14851 if editor.context_menu.borrow_mut().is_some() {
14852 panic!("expected completion menu to be hidden after canceling");
14853 }
14854 });
14855
14856 cx.simulate_keystroke("o");
14857 cx.executor().run_until_parked();
14858 cx.update_editor(|editor, _, _| {
14859 if editor.context_menu.borrow_mut().is_some() {
14860 panic!(
14861 "expected completion menu to be hidden, as words completion threshold is not met still"
14862 );
14863 }
14864 });
14865
14866 cx.simulate_keystroke("w");
14867 cx.executor().run_until_parked();
14868 cx.update_editor(|editor, _, _| {
14869 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14870 {
14871 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
14872 } else {
14873 panic!("expected completion menu to be open after the word completions threshold is met");
14874 }
14875 });
14876}
14877
14878#[gpui::test]
14879async fn test_word_completions_disabled(cx: &mut TestAppContext) {
14880 init_test(cx, |language_settings| {
14881 language_settings.defaults.completions = Some(CompletionSettingsContent {
14882 words: Some(WordsCompletionMode::Enabled),
14883 words_min_length: Some(0),
14884 lsp_insert_mode: Some(LspInsertMode::Insert),
14885 ..Default::default()
14886 });
14887 });
14888
14889 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14890 cx.update_editor(|editor, _, _| {
14891 editor.disable_word_completions();
14892 });
14893 cx.set_state(indoc! {"ˇ
14894 wow
14895 wowen
14896 wowser
14897 "});
14898 cx.simulate_keystroke("w");
14899 cx.executor().run_until_parked();
14900 cx.update_editor(|editor, _, _| {
14901 if editor.context_menu.borrow_mut().is_some() {
14902 panic!(
14903 "expected completion menu to be hidden, as words completion are disabled for this editor"
14904 );
14905 }
14906 });
14907
14908 cx.update_editor(|editor, window, cx| {
14909 editor.show_word_completions(&ShowWordCompletions, window, cx);
14910 });
14911 cx.executor().run_until_parked();
14912 cx.update_editor(|editor, _, _| {
14913 if editor.context_menu.borrow_mut().is_some() {
14914 panic!(
14915 "expected completion menu to be hidden even if called for explicitly, as words completion are disabled for this editor"
14916 );
14917 }
14918 });
14919}
14920
14921fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
14922 let position = || lsp::Position {
14923 line: params.text_document_position.position.line,
14924 character: params.text_document_position.position.character,
14925 };
14926 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14927 range: lsp::Range {
14928 start: position(),
14929 end: position(),
14930 },
14931 new_text: text.to_string(),
14932 }))
14933}
14934
14935#[gpui::test]
14936async fn test_multiline_completion(cx: &mut TestAppContext) {
14937 init_test(cx, |_| {});
14938
14939 let fs = FakeFs::new(cx.executor());
14940 fs.insert_tree(
14941 path!("/a"),
14942 json!({
14943 "main.ts": "a",
14944 }),
14945 )
14946 .await;
14947
14948 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14949 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14950 let typescript_language = Arc::new(Language::new(
14951 LanguageConfig {
14952 name: "TypeScript".into(),
14953 matcher: LanguageMatcher {
14954 path_suffixes: vec!["ts".to_string()],
14955 ..LanguageMatcher::default()
14956 },
14957 line_comments: vec!["// ".into()],
14958 ..LanguageConfig::default()
14959 },
14960 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
14961 ));
14962 language_registry.add(typescript_language.clone());
14963 let mut fake_servers = language_registry.register_fake_lsp(
14964 "TypeScript",
14965 FakeLspAdapter {
14966 capabilities: lsp::ServerCapabilities {
14967 completion_provider: Some(lsp::CompletionOptions {
14968 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14969 ..lsp::CompletionOptions::default()
14970 }),
14971 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14972 ..lsp::ServerCapabilities::default()
14973 },
14974 // Emulate vtsls label generation
14975 label_for_completion: Some(Box::new(|item, _| {
14976 let text = if let Some(description) = item
14977 .label_details
14978 .as_ref()
14979 .and_then(|label_details| label_details.description.as_ref())
14980 {
14981 format!("{} {}", item.label, description)
14982 } else if let Some(detail) = &item.detail {
14983 format!("{} {}", item.label, detail)
14984 } else {
14985 item.label.clone()
14986 };
14987 Some(language::CodeLabel::plain(text, None))
14988 })),
14989 ..FakeLspAdapter::default()
14990 },
14991 );
14992 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14993 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14994 let worktree_id = workspace
14995 .update(cx, |workspace, _window, cx| {
14996 workspace.project().update(cx, |project, cx| {
14997 project.worktrees(cx).next().unwrap().read(cx).id()
14998 })
14999 })
15000 .unwrap();
15001 let _buffer = project
15002 .update(cx, |project, cx| {
15003 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
15004 })
15005 .await
15006 .unwrap();
15007 let editor = workspace
15008 .update(cx, |workspace, window, cx| {
15009 workspace.open_path((worktree_id, rel_path("main.ts")), None, true, window, cx)
15010 })
15011 .unwrap()
15012 .await
15013 .unwrap()
15014 .downcast::<Editor>()
15015 .unwrap();
15016 let fake_server = fake_servers.next().await.unwrap();
15017
15018 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
15019 let multiline_label_2 = "a\nb\nc\n";
15020 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
15021 let multiline_description = "d\ne\nf\n";
15022 let multiline_detail_2 = "g\nh\ni\n";
15023
15024 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
15025 move |params, _| async move {
15026 Ok(Some(lsp::CompletionResponse::Array(vec![
15027 lsp::CompletionItem {
15028 label: multiline_label.to_string(),
15029 text_edit: gen_text_edit(¶ms, "new_text_1"),
15030 ..lsp::CompletionItem::default()
15031 },
15032 lsp::CompletionItem {
15033 label: "single line label 1".to_string(),
15034 detail: Some(multiline_detail.to_string()),
15035 text_edit: gen_text_edit(¶ms, "new_text_2"),
15036 ..lsp::CompletionItem::default()
15037 },
15038 lsp::CompletionItem {
15039 label: "single line label 2".to_string(),
15040 label_details: Some(lsp::CompletionItemLabelDetails {
15041 description: Some(multiline_description.to_string()),
15042 detail: None,
15043 }),
15044 text_edit: gen_text_edit(¶ms, "new_text_2"),
15045 ..lsp::CompletionItem::default()
15046 },
15047 lsp::CompletionItem {
15048 label: multiline_label_2.to_string(),
15049 detail: Some(multiline_detail_2.to_string()),
15050 text_edit: gen_text_edit(¶ms, "new_text_3"),
15051 ..lsp::CompletionItem::default()
15052 },
15053 lsp::CompletionItem {
15054 label: "Label with many spaces and \t but without newlines".to_string(),
15055 detail: Some(
15056 "Details with many spaces and \t but without newlines".to_string(),
15057 ),
15058 text_edit: gen_text_edit(¶ms, "new_text_4"),
15059 ..lsp::CompletionItem::default()
15060 },
15061 ])))
15062 },
15063 );
15064
15065 editor.update_in(cx, |editor, window, cx| {
15066 cx.focus_self(window);
15067 editor.move_to_end(&MoveToEnd, window, cx);
15068 editor.handle_input(".", window, cx);
15069 });
15070 cx.run_until_parked();
15071 completion_handle.next().await.unwrap();
15072
15073 editor.update(cx, |editor, _| {
15074 assert!(editor.context_menu_visible());
15075 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15076 {
15077 let completion_labels = menu
15078 .completions
15079 .borrow()
15080 .iter()
15081 .map(|c| c.label.text.clone())
15082 .collect::<Vec<_>>();
15083 assert_eq!(
15084 completion_labels,
15085 &[
15086 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
15087 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
15088 "single line label 2 d e f ",
15089 "a b c g h i ",
15090 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
15091 ],
15092 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
15093 );
15094
15095 for completion in menu
15096 .completions
15097 .borrow()
15098 .iter() {
15099 assert_eq!(
15100 completion.label.filter_range,
15101 0..completion.label.text.len(),
15102 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
15103 );
15104 }
15105 } else {
15106 panic!("expected completion menu to be open");
15107 }
15108 });
15109}
15110
15111#[gpui::test]
15112async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
15113 init_test(cx, |_| {});
15114 let mut cx = EditorLspTestContext::new_rust(
15115 lsp::ServerCapabilities {
15116 completion_provider: Some(lsp::CompletionOptions {
15117 trigger_characters: Some(vec![".".to_string()]),
15118 ..Default::default()
15119 }),
15120 ..Default::default()
15121 },
15122 cx,
15123 )
15124 .await;
15125 cx.lsp
15126 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15127 Ok(Some(lsp::CompletionResponse::Array(vec![
15128 lsp::CompletionItem {
15129 label: "first".into(),
15130 ..Default::default()
15131 },
15132 lsp::CompletionItem {
15133 label: "last".into(),
15134 ..Default::default()
15135 },
15136 ])))
15137 });
15138 cx.set_state("variableˇ");
15139 cx.simulate_keystroke(".");
15140 cx.executor().run_until_parked();
15141
15142 cx.update_editor(|editor, _, _| {
15143 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15144 {
15145 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
15146 } else {
15147 panic!("expected completion menu to be open");
15148 }
15149 });
15150
15151 cx.update_editor(|editor, window, cx| {
15152 editor.move_page_down(&MovePageDown::default(), window, cx);
15153 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15154 {
15155 assert!(
15156 menu.selected_item == 1,
15157 "expected PageDown to select the last item from the context menu"
15158 );
15159 } else {
15160 panic!("expected completion menu to stay open after PageDown");
15161 }
15162 });
15163
15164 cx.update_editor(|editor, window, cx| {
15165 editor.move_page_up(&MovePageUp::default(), window, cx);
15166 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15167 {
15168 assert!(
15169 menu.selected_item == 0,
15170 "expected PageUp to select the first item from the context menu"
15171 );
15172 } else {
15173 panic!("expected completion menu to stay open after PageUp");
15174 }
15175 });
15176}
15177
15178#[gpui::test]
15179async fn test_as_is_completions(cx: &mut TestAppContext) {
15180 init_test(cx, |_| {});
15181 let mut cx = EditorLspTestContext::new_rust(
15182 lsp::ServerCapabilities {
15183 completion_provider: Some(lsp::CompletionOptions {
15184 ..Default::default()
15185 }),
15186 ..Default::default()
15187 },
15188 cx,
15189 )
15190 .await;
15191 cx.lsp
15192 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15193 Ok(Some(lsp::CompletionResponse::Array(vec![
15194 lsp::CompletionItem {
15195 label: "unsafe".into(),
15196 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15197 range: lsp::Range {
15198 start: lsp::Position {
15199 line: 1,
15200 character: 2,
15201 },
15202 end: lsp::Position {
15203 line: 1,
15204 character: 3,
15205 },
15206 },
15207 new_text: "unsafe".to_string(),
15208 })),
15209 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
15210 ..Default::default()
15211 },
15212 ])))
15213 });
15214 cx.set_state("fn a() {}\n nˇ");
15215 cx.executor().run_until_parked();
15216 cx.update_editor(|editor, window, cx| {
15217 editor.show_completions(
15218 &ShowCompletions {
15219 trigger: Some("\n".into()),
15220 },
15221 window,
15222 cx,
15223 );
15224 });
15225 cx.executor().run_until_parked();
15226
15227 cx.update_editor(|editor, window, cx| {
15228 editor.confirm_completion(&Default::default(), window, cx)
15229 });
15230 cx.executor().run_until_parked();
15231 cx.assert_editor_state("fn a() {}\n unsafeˇ");
15232}
15233
15234#[gpui::test]
15235async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
15236 init_test(cx, |_| {});
15237 let language =
15238 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
15239 let mut cx = EditorLspTestContext::new(
15240 language,
15241 lsp::ServerCapabilities {
15242 completion_provider: Some(lsp::CompletionOptions {
15243 ..lsp::CompletionOptions::default()
15244 }),
15245 ..lsp::ServerCapabilities::default()
15246 },
15247 cx,
15248 )
15249 .await;
15250
15251 cx.set_state(
15252 "#ifndef BAR_H
15253#define BAR_H
15254
15255#include <stdbool.h>
15256
15257int fn_branch(bool do_branch1, bool do_branch2);
15258
15259#endif // BAR_H
15260ˇ",
15261 );
15262 cx.executor().run_until_parked();
15263 cx.update_editor(|editor, window, cx| {
15264 editor.handle_input("#", window, cx);
15265 });
15266 cx.executor().run_until_parked();
15267 cx.update_editor(|editor, window, cx| {
15268 editor.handle_input("i", window, cx);
15269 });
15270 cx.executor().run_until_parked();
15271 cx.update_editor(|editor, window, cx| {
15272 editor.handle_input("n", window, cx);
15273 });
15274 cx.executor().run_until_parked();
15275 cx.assert_editor_state(
15276 "#ifndef BAR_H
15277#define BAR_H
15278
15279#include <stdbool.h>
15280
15281int fn_branch(bool do_branch1, bool do_branch2);
15282
15283#endif // BAR_H
15284#inˇ",
15285 );
15286
15287 cx.lsp
15288 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15289 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15290 is_incomplete: false,
15291 item_defaults: None,
15292 items: vec![lsp::CompletionItem {
15293 kind: Some(lsp::CompletionItemKind::SNIPPET),
15294 label_details: Some(lsp::CompletionItemLabelDetails {
15295 detail: Some("header".to_string()),
15296 description: None,
15297 }),
15298 label: " include".to_string(),
15299 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15300 range: lsp::Range {
15301 start: lsp::Position {
15302 line: 8,
15303 character: 1,
15304 },
15305 end: lsp::Position {
15306 line: 8,
15307 character: 1,
15308 },
15309 },
15310 new_text: "include \"$0\"".to_string(),
15311 })),
15312 sort_text: Some("40b67681include".to_string()),
15313 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15314 filter_text: Some("include".to_string()),
15315 insert_text: Some("include \"$0\"".to_string()),
15316 ..lsp::CompletionItem::default()
15317 }],
15318 })))
15319 });
15320 cx.update_editor(|editor, window, cx| {
15321 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15322 });
15323 cx.executor().run_until_parked();
15324 cx.update_editor(|editor, window, cx| {
15325 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15326 });
15327 cx.executor().run_until_parked();
15328 cx.assert_editor_state(
15329 "#ifndef BAR_H
15330#define BAR_H
15331
15332#include <stdbool.h>
15333
15334int fn_branch(bool do_branch1, bool do_branch2);
15335
15336#endif // BAR_H
15337#include \"ˇ\"",
15338 );
15339
15340 cx.lsp
15341 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15342 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15343 is_incomplete: true,
15344 item_defaults: None,
15345 items: vec![lsp::CompletionItem {
15346 kind: Some(lsp::CompletionItemKind::FILE),
15347 label: "AGL/".to_string(),
15348 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15349 range: lsp::Range {
15350 start: lsp::Position {
15351 line: 8,
15352 character: 10,
15353 },
15354 end: lsp::Position {
15355 line: 8,
15356 character: 11,
15357 },
15358 },
15359 new_text: "AGL/".to_string(),
15360 })),
15361 sort_text: Some("40b67681AGL/".to_string()),
15362 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15363 filter_text: Some("AGL/".to_string()),
15364 insert_text: Some("AGL/".to_string()),
15365 ..lsp::CompletionItem::default()
15366 }],
15367 })))
15368 });
15369 cx.update_editor(|editor, window, cx| {
15370 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15371 });
15372 cx.executor().run_until_parked();
15373 cx.update_editor(|editor, window, cx| {
15374 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15375 });
15376 cx.executor().run_until_parked();
15377 cx.assert_editor_state(
15378 r##"#ifndef BAR_H
15379#define BAR_H
15380
15381#include <stdbool.h>
15382
15383int fn_branch(bool do_branch1, bool do_branch2);
15384
15385#endif // BAR_H
15386#include "AGL/ˇ"##,
15387 );
15388
15389 cx.update_editor(|editor, window, cx| {
15390 editor.handle_input("\"", window, cx);
15391 });
15392 cx.executor().run_until_parked();
15393 cx.assert_editor_state(
15394 r##"#ifndef BAR_H
15395#define BAR_H
15396
15397#include <stdbool.h>
15398
15399int fn_branch(bool do_branch1, bool do_branch2);
15400
15401#endif // BAR_H
15402#include "AGL/"ˇ"##,
15403 );
15404}
15405
15406#[gpui::test]
15407async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
15408 init_test(cx, |_| {});
15409
15410 let mut cx = EditorLspTestContext::new_rust(
15411 lsp::ServerCapabilities {
15412 completion_provider: Some(lsp::CompletionOptions {
15413 trigger_characters: Some(vec![".".to_string()]),
15414 resolve_provider: Some(true),
15415 ..Default::default()
15416 }),
15417 ..Default::default()
15418 },
15419 cx,
15420 )
15421 .await;
15422
15423 cx.set_state("fn main() { let a = 2ˇ; }");
15424 cx.simulate_keystroke(".");
15425 let completion_item = lsp::CompletionItem {
15426 label: "Some".into(),
15427 kind: Some(lsp::CompletionItemKind::SNIPPET),
15428 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15429 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15430 kind: lsp::MarkupKind::Markdown,
15431 value: "```rust\nSome(2)\n```".to_string(),
15432 })),
15433 deprecated: Some(false),
15434 sort_text: Some("Some".to_string()),
15435 filter_text: Some("Some".to_string()),
15436 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15437 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15438 range: lsp::Range {
15439 start: lsp::Position {
15440 line: 0,
15441 character: 22,
15442 },
15443 end: lsp::Position {
15444 line: 0,
15445 character: 22,
15446 },
15447 },
15448 new_text: "Some(2)".to_string(),
15449 })),
15450 additional_text_edits: Some(vec![lsp::TextEdit {
15451 range: lsp::Range {
15452 start: lsp::Position {
15453 line: 0,
15454 character: 20,
15455 },
15456 end: lsp::Position {
15457 line: 0,
15458 character: 22,
15459 },
15460 },
15461 new_text: "".to_string(),
15462 }]),
15463 ..Default::default()
15464 };
15465
15466 let closure_completion_item = completion_item.clone();
15467 let counter = Arc::new(AtomicUsize::new(0));
15468 let counter_clone = counter.clone();
15469 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15470 let task_completion_item = closure_completion_item.clone();
15471 counter_clone.fetch_add(1, atomic::Ordering::Release);
15472 async move {
15473 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15474 is_incomplete: true,
15475 item_defaults: None,
15476 items: vec![task_completion_item],
15477 })))
15478 }
15479 });
15480
15481 cx.condition(|editor, _| editor.context_menu_visible())
15482 .await;
15483 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
15484 assert!(request.next().await.is_some());
15485 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15486
15487 cx.simulate_keystrokes("S o m");
15488 cx.condition(|editor, _| editor.context_menu_visible())
15489 .await;
15490 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
15491 assert!(request.next().await.is_some());
15492 assert!(request.next().await.is_some());
15493 assert!(request.next().await.is_some());
15494 request.close();
15495 assert!(request.next().await.is_none());
15496 assert_eq!(
15497 counter.load(atomic::Ordering::Acquire),
15498 4,
15499 "With the completions menu open, only one LSP request should happen per input"
15500 );
15501}
15502
15503#[gpui::test]
15504async fn test_toggle_comment(cx: &mut TestAppContext) {
15505 init_test(cx, |_| {});
15506 let mut cx = EditorTestContext::new(cx).await;
15507 let language = Arc::new(Language::new(
15508 LanguageConfig {
15509 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15510 ..Default::default()
15511 },
15512 Some(tree_sitter_rust::LANGUAGE.into()),
15513 ));
15514 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15515
15516 // If multiple selections intersect a line, the line is only toggled once.
15517 cx.set_state(indoc! {"
15518 fn a() {
15519 «//b();
15520 ˇ»// «c();
15521 //ˇ» d();
15522 }
15523 "});
15524
15525 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15526
15527 cx.assert_editor_state(indoc! {"
15528 fn a() {
15529 «b();
15530 c();
15531 ˇ» d();
15532 }
15533 "});
15534
15535 // The comment prefix is inserted at the same column for every line in a
15536 // selection.
15537 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15538
15539 cx.assert_editor_state(indoc! {"
15540 fn a() {
15541 // «b();
15542 // c();
15543 ˇ»// d();
15544 }
15545 "});
15546
15547 // If a selection ends at the beginning of a line, that line is not toggled.
15548 cx.set_selections_state(indoc! {"
15549 fn a() {
15550 // b();
15551 «// c();
15552 ˇ» // d();
15553 }
15554 "});
15555
15556 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15557
15558 cx.assert_editor_state(indoc! {"
15559 fn a() {
15560 // b();
15561 «c();
15562 ˇ» // d();
15563 }
15564 "});
15565
15566 // If a selection span a single line and is empty, the line is toggled.
15567 cx.set_state(indoc! {"
15568 fn a() {
15569 a();
15570 b();
15571 ˇ
15572 }
15573 "});
15574
15575 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15576
15577 cx.assert_editor_state(indoc! {"
15578 fn a() {
15579 a();
15580 b();
15581 //•ˇ
15582 }
15583 "});
15584
15585 // If a selection span multiple lines, empty lines are not toggled.
15586 cx.set_state(indoc! {"
15587 fn a() {
15588 «a();
15589
15590 c();ˇ»
15591 }
15592 "});
15593
15594 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15595
15596 cx.assert_editor_state(indoc! {"
15597 fn a() {
15598 // «a();
15599
15600 // c();ˇ»
15601 }
15602 "});
15603
15604 // If a selection includes multiple comment prefixes, all lines are uncommented.
15605 cx.set_state(indoc! {"
15606 fn a() {
15607 «// a();
15608 /// b();
15609 //! c();ˇ»
15610 }
15611 "});
15612
15613 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15614
15615 cx.assert_editor_state(indoc! {"
15616 fn a() {
15617 «a();
15618 b();
15619 c();ˇ»
15620 }
15621 "});
15622}
15623
15624#[gpui::test]
15625async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
15626 init_test(cx, |_| {});
15627 let mut cx = EditorTestContext::new(cx).await;
15628 let language = Arc::new(Language::new(
15629 LanguageConfig {
15630 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15631 ..Default::default()
15632 },
15633 Some(tree_sitter_rust::LANGUAGE.into()),
15634 ));
15635 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15636
15637 let toggle_comments = &ToggleComments {
15638 advance_downwards: false,
15639 ignore_indent: true,
15640 };
15641
15642 // If multiple selections intersect a line, the line is only toggled once.
15643 cx.set_state(indoc! {"
15644 fn a() {
15645 // «b();
15646 // c();
15647 // ˇ» d();
15648 }
15649 "});
15650
15651 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15652
15653 cx.assert_editor_state(indoc! {"
15654 fn a() {
15655 «b();
15656 c();
15657 ˇ» d();
15658 }
15659 "});
15660
15661 // The comment prefix is inserted at the beginning of each line
15662 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15663
15664 cx.assert_editor_state(indoc! {"
15665 fn a() {
15666 // «b();
15667 // c();
15668 // ˇ» d();
15669 }
15670 "});
15671
15672 // If a selection ends at the beginning of a line, that line is not toggled.
15673 cx.set_selections_state(indoc! {"
15674 fn a() {
15675 // b();
15676 // «c();
15677 ˇ»// d();
15678 }
15679 "});
15680
15681 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15682
15683 cx.assert_editor_state(indoc! {"
15684 fn a() {
15685 // b();
15686 «c();
15687 ˇ»// d();
15688 }
15689 "});
15690
15691 // If a selection span a single line and is empty, the line is toggled.
15692 cx.set_state(indoc! {"
15693 fn a() {
15694 a();
15695 b();
15696 ˇ
15697 }
15698 "});
15699
15700 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15701
15702 cx.assert_editor_state(indoc! {"
15703 fn a() {
15704 a();
15705 b();
15706 //ˇ
15707 }
15708 "});
15709
15710 // If a selection span multiple lines, empty lines are not toggled.
15711 cx.set_state(indoc! {"
15712 fn a() {
15713 «a();
15714
15715 c();ˇ»
15716 }
15717 "});
15718
15719 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15720
15721 cx.assert_editor_state(indoc! {"
15722 fn a() {
15723 // «a();
15724
15725 // c();ˇ»
15726 }
15727 "});
15728
15729 // If a selection includes multiple comment prefixes, all lines are uncommented.
15730 cx.set_state(indoc! {"
15731 fn a() {
15732 // «a();
15733 /// b();
15734 //! c();ˇ»
15735 }
15736 "});
15737
15738 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15739
15740 cx.assert_editor_state(indoc! {"
15741 fn a() {
15742 «a();
15743 b();
15744 c();ˇ»
15745 }
15746 "});
15747}
15748
15749#[gpui::test]
15750async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
15751 init_test(cx, |_| {});
15752
15753 let language = Arc::new(Language::new(
15754 LanguageConfig {
15755 line_comments: vec!["// ".into()],
15756 ..Default::default()
15757 },
15758 Some(tree_sitter_rust::LANGUAGE.into()),
15759 ));
15760
15761 let mut cx = EditorTestContext::new(cx).await;
15762
15763 cx.language_registry().add(language.clone());
15764 cx.update_buffer(|buffer, cx| {
15765 buffer.set_language(Some(language), cx);
15766 });
15767
15768 let toggle_comments = &ToggleComments {
15769 advance_downwards: true,
15770 ignore_indent: false,
15771 };
15772
15773 // Single cursor on one line -> advance
15774 // Cursor moves horizontally 3 characters as well on non-blank line
15775 cx.set_state(indoc!(
15776 "fn a() {
15777 ˇdog();
15778 cat();
15779 }"
15780 ));
15781 cx.update_editor(|editor, window, cx| {
15782 editor.toggle_comments(toggle_comments, window, cx);
15783 });
15784 cx.assert_editor_state(indoc!(
15785 "fn a() {
15786 // dog();
15787 catˇ();
15788 }"
15789 ));
15790
15791 // Single selection on one line -> don't advance
15792 cx.set_state(indoc!(
15793 "fn a() {
15794 «dog()ˇ»;
15795 cat();
15796 }"
15797 ));
15798 cx.update_editor(|editor, window, cx| {
15799 editor.toggle_comments(toggle_comments, window, cx);
15800 });
15801 cx.assert_editor_state(indoc!(
15802 "fn a() {
15803 // «dog()ˇ»;
15804 cat();
15805 }"
15806 ));
15807
15808 // Multiple cursors on one line -> advance
15809 cx.set_state(indoc!(
15810 "fn a() {
15811 ˇdˇog();
15812 cat();
15813 }"
15814 ));
15815 cx.update_editor(|editor, window, cx| {
15816 editor.toggle_comments(toggle_comments, window, cx);
15817 });
15818 cx.assert_editor_state(indoc!(
15819 "fn a() {
15820 // dog();
15821 catˇ(ˇ);
15822 }"
15823 ));
15824
15825 // Multiple cursors on one line, with selection -> don't advance
15826 cx.set_state(indoc!(
15827 "fn a() {
15828 ˇdˇog«()ˇ»;
15829 cat();
15830 }"
15831 ));
15832 cx.update_editor(|editor, window, cx| {
15833 editor.toggle_comments(toggle_comments, window, cx);
15834 });
15835 cx.assert_editor_state(indoc!(
15836 "fn a() {
15837 // ˇdˇog«()ˇ»;
15838 cat();
15839 }"
15840 ));
15841
15842 // Single cursor on one line -> advance
15843 // Cursor moves to column 0 on blank line
15844 cx.set_state(indoc!(
15845 "fn a() {
15846 ˇdog();
15847
15848 cat();
15849 }"
15850 ));
15851 cx.update_editor(|editor, window, cx| {
15852 editor.toggle_comments(toggle_comments, window, cx);
15853 });
15854 cx.assert_editor_state(indoc!(
15855 "fn a() {
15856 // dog();
15857 ˇ
15858 cat();
15859 }"
15860 ));
15861
15862 // Single cursor on one line -> advance
15863 // Cursor starts and ends at column 0
15864 cx.set_state(indoc!(
15865 "fn a() {
15866 ˇ dog();
15867 cat();
15868 }"
15869 ));
15870 cx.update_editor(|editor, window, cx| {
15871 editor.toggle_comments(toggle_comments, window, cx);
15872 });
15873 cx.assert_editor_state(indoc!(
15874 "fn a() {
15875 // dog();
15876 ˇ cat();
15877 }"
15878 ));
15879}
15880
15881#[gpui::test]
15882async fn test_toggle_block_comment(cx: &mut TestAppContext) {
15883 init_test(cx, |_| {});
15884
15885 let mut cx = EditorTestContext::new(cx).await;
15886
15887 let html_language = Arc::new(
15888 Language::new(
15889 LanguageConfig {
15890 name: "HTML".into(),
15891 block_comment: Some(BlockCommentConfig {
15892 start: "<!-- ".into(),
15893 prefix: "".into(),
15894 end: " -->".into(),
15895 tab_size: 0,
15896 }),
15897 ..Default::default()
15898 },
15899 Some(tree_sitter_html::LANGUAGE.into()),
15900 )
15901 .with_injection_query(
15902 r#"
15903 (script_element
15904 (raw_text) @injection.content
15905 (#set! injection.language "javascript"))
15906 "#,
15907 )
15908 .unwrap(),
15909 );
15910
15911 let javascript_language = Arc::new(Language::new(
15912 LanguageConfig {
15913 name: "JavaScript".into(),
15914 line_comments: vec!["// ".into()],
15915 ..Default::default()
15916 },
15917 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
15918 ));
15919
15920 cx.language_registry().add(html_language.clone());
15921 cx.language_registry().add(javascript_language);
15922 cx.update_buffer(|buffer, cx| {
15923 buffer.set_language(Some(html_language), cx);
15924 });
15925
15926 // Toggle comments for empty selections
15927 cx.set_state(
15928 &r#"
15929 <p>A</p>ˇ
15930 <p>B</p>ˇ
15931 <p>C</p>ˇ
15932 "#
15933 .unindent(),
15934 );
15935 cx.update_editor(|editor, window, cx| {
15936 editor.toggle_comments(&ToggleComments::default(), window, cx)
15937 });
15938 cx.assert_editor_state(
15939 &r#"
15940 <!-- <p>A</p>ˇ -->
15941 <!-- <p>B</p>ˇ -->
15942 <!-- <p>C</p>ˇ -->
15943 "#
15944 .unindent(),
15945 );
15946 cx.update_editor(|editor, window, cx| {
15947 editor.toggle_comments(&ToggleComments::default(), window, cx)
15948 });
15949 cx.assert_editor_state(
15950 &r#"
15951 <p>A</p>ˇ
15952 <p>B</p>ˇ
15953 <p>C</p>ˇ
15954 "#
15955 .unindent(),
15956 );
15957
15958 // Toggle comments for mixture of empty and non-empty selections, where
15959 // multiple selections occupy a given line.
15960 cx.set_state(
15961 &r#"
15962 <p>A«</p>
15963 <p>ˇ»B</p>ˇ
15964 <p>C«</p>
15965 <p>ˇ»D</p>ˇ
15966 "#
15967 .unindent(),
15968 );
15969
15970 cx.update_editor(|editor, window, cx| {
15971 editor.toggle_comments(&ToggleComments::default(), window, cx)
15972 });
15973 cx.assert_editor_state(
15974 &r#"
15975 <!-- <p>A«</p>
15976 <p>ˇ»B</p>ˇ -->
15977 <!-- <p>C«</p>
15978 <p>ˇ»D</p>ˇ -->
15979 "#
15980 .unindent(),
15981 );
15982 cx.update_editor(|editor, window, cx| {
15983 editor.toggle_comments(&ToggleComments::default(), window, cx)
15984 });
15985 cx.assert_editor_state(
15986 &r#"
15987 <p>A«</p>
15988 <p>ˇ»B</p>ˇ
15989 <p>C«</p>
15990 <p>ˇ»D</p>ˇ
15991 "#
15992 .unindent(),
15993 );
15994
15995 // Toggle comments when different languages are active for different
15996 // selections.
15997 cx.set_state(
15998 &r#"
15999 ˇ<script>
16000 ˇvar x = new Y();
16001 ˇ</script>
16002 "#
16003 .unindent(),
16004 );
16005 cx.executor().run_until_parked();
16006 cx.update_editor(|editor, window, cx| {
16007 editor.toggle_comments(&ToggleComments::default(), window, cx)
16008 });
16009 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
16010 // Uncommenting and commenting from this position brings in even more wrong artifacts.
16011 cx.assert_editor_state(
16012 &r#"
16013 <!-- ˇ<script> -->
16014 // ˇvar x = new Y();
16015 <!-- ˇ</script> -->
16016 "#
16017 .unindent(),
16018 );
16019}
16020
16021#[gpui::test]
16022fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
16023 init_test(cx, |_| {});
16024
16025 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16026 let multibuffer = cx.new(|cx| {
16027 let mut multibuffer = MultiBuffer::new(ReadWrite);
16028 multibuffer.push_excerpts(
16029 buffer.clone(),
16030 [
16031 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
16032 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
16033 ],
16034 cx,
16035 );
16036 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
16037 multibuffer
16038 });
16039
16040 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
16041 editor.update_in(cx, |editor, window, cx| {
16042 assert_eq!(editor.text(cx), "aaaa\nbbbb");
16043 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16044 s.select_ranges([
16045 Point::new(0, 0)..Point::new(0, 0),
16046 Point::new(1, 0)..Point::new(1, 0),
16047 ])
16048 });
16049
16050 editor.handle_input("X", window, cx);
16051 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
16052 assert_eq!(
16053 editor.selections.ranges(&editor.display_snapshot(cx)),
16054 [
16055 Point::new(0, 1)..Point::new(0, 1),
16056 Point::new(1, 1)..Point::new(1, 1),
16057 ]
16058 );
16059
16060 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
16061 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16062 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
16063 });
16064 editor.backspace(&Default::default(), window, cx);
16065 assert_eq!(editor.text(cx), "Xa\nbbb");
16066 assert_eq!(
16067 editor.selections.ranges(&editor.display_snapshot(cx)),
16068 [Point::new(1, 0)..Point::new(1, 0)]
16069 );
16070
16071 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16072 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
16073 });
16074 editor.backspace(&Default::default(), window, cx);
16075 assert_eq!(editor.text(cx), "X\nbb");
16076 assert_eq!(
16077 editor.selections.ranges(&editor.display_snapshot(cx)),
16078 [Point::new(0, 1)..Point::new(0, 1)]
16079 );
16080 });
16081}
16082
16083#[gpui::test]
16084fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
16085 init_test(cx, |_| {});
16086
16087 let markers = vec![('[', ']').into(), ('(', ')').into()];
16088 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
16089 indoc! {"
16090 [aaaa
16091 (bbbb]
16092 cccc)",
16093 },
16094 markers.clone(),
16095 );
16096 let excerpt_ranges = markers.into_iter().map(|marker| {
16097 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
16098 ExcerptRange::new(context)
16099 });
16100 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
16101 let multibuffer = cx.new(|cx| {
16102 let mut multibuffer = MultiBuffer::new(ReadWrite);
16103 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
16104 multibuffer
16105 });
16106
16107 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
16108 editor.update_in(cx, |editor, window, cx| {
16109 let (expected_text, selection_ranges) = marked_text_ranges(
16110 indoc! {"
16111 aaaa
16112 bˇbbb
16113 bˇbbˇb
16114 cccc"
16115 },
16116 true,
16117 );
16118 assert_eq!(editor.text(cx), expected_text);
16119 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16120 s.select_ranges(selection_ranges)
16121 });
16122
16123 editor.handle_input("X", window, cx);
16124
16125 let (expected_text, expected_selections) = marked_text_ranges(
16126 indoc! {"
16127 aaaa
16128 bXˇbbXb
16129 bXˇbbXˇb
16130 cccc"
16131 },
16132 false,
16133 );
16134 assert_eq!(editor.text(cx), expected_text);
16135 assert_eq!(
16136 editor.selections.ranges(&editor.display_snapshot(cx)),
16137 expected_selections
16138 );
16139
16140 editor.newline(&Newline, window, cx);
16141 let (expected_text, expected_selections) = marked_text_ranges(
16142 indoc! {"
16143 aaaa
16144 bX
16145 ˇbbX
16146 b
16147 bX
16148 ˇbbX
16149 ˇb
16150 cccc"
16151 },
16152 false,
16153 );
16154 assert_eq!(editor.text(cx), expected_text);
16155 assert_eq!(
16156 editor.selections.ranges(&editor.display_snapshot(cx)),
16157 expected_selections
16158 );
16159 });
16160}
16161
16162#[gpui::test]
16163fn test_refresh_selections(cx: &mut TestAppContext) {
16164 init_test(cx, |_| {});
16165
16166 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16167 let mut excerpt1_id = None;
16168 let multibuffer = cx.new(|cx| {
16169 let mut multibuffer = MultiBuffer::new(ReadWrite);
16170 excerpt1_id = multibuffer
16171 .push_excerpts(
16172 buffer.clone(),
16173 [
16174 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
16175 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
16176 ],
16177 cx,
16178 )
16179 .into_iter()
16180 .next();
16181 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16182 multibuffer
16183 });
16184
16185 let editor = cx.add_window(|window, cx| {
16186 let mut editor = build_editor(multibuffer.clone(), window, cx);
16187 let snapshot = editor.snapshot(window, cx);
16188 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16189 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
16190 });
16191 editor.begin_selection(
16192 Point::new(2, 1).to_display_point(&snapshot),
16193 true,
16194 1,
16195 window,
16196 cx,
16197 );
16198 assert_eq!(
16199 editor.selections.ranges(&editor.display_snapshot(cx)),
16200 [
16201 Point::new(1, 3)..Point::new(1, 3),
16202 Point::new(2, 1)..Point::new(2, 1),
16203 ]
16204 );
16205 editor
16206 });
16207
16208 // Refreshing selections is a no-op when excerpts haven't changed.
16209 _ = editor.update(cx, |editor, window, cx| {
16210 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16211 assert_eq!(
16212 editor.selections.ranges(&editor.display_snapshot(cx)),
16213 [
16214 Point::new(1, 3)..Point::new(1, 3),
16215 Point::new(2, 1)..Point::new(2, 1),
16216 ]
16217 );
16218 });
16219
16220 multibuffer.update(cx, |multibuffer, cx| {
16221 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16222 });
16223 _ = editor.update(cx, |editor, window, cx| {
16224 // Removing an excerpt causes the first selection to become degenerate.
16225 assert_eq!(
16226 editor.selections.ranges(&editor.display_snapshot(cx)),
16227 [
16228 Point::new(0, 0)..Point::new(0, 0),
16229 Point::new(0, 1)..Point::new(0, 1)
16230 ]
16231 );
16232
16233 // Refreshing selections will relocate the first selection to the original buffer
16234 // location.
16235 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16236 assert_eq!(
16237 editor.selections.ranges(&editor.display_snapshot(cx)),
16238 [
16239 Point::new(0, 1)..Point::new(0, 1),
16240 Point::new(0, 3)..Point::new(0, 3)
16241 ]
16242 );
16243 assert!(editor.selections.pending_anchor().is_some());
16244 });
16245}
16246
16247#[gpui::test]
16248fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
16249 init_test(cx, |_| {});
16250
16251 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16252 let mut excerpt1_id = None;
16253 let multibuffer = cx.new(|cx| {
16254 let mut multibuffer = MultiBuffer::new(ReadWrite);
16255 excerpt1_id = multibuffer
16256 .push_excerpts(
16257 buffer.clone(),
16258 [
16259 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
16260 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
16261 ],
16262 cx,
16263 )
16264 .into_iter()
16265 .next();
16266 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16267 multibuffer
16268 });
16269
16270 let editor = cx.add_window(|window, cx| {
16271 let mut editor = build_editor(multibuffer.clone(), window, cx);
16272 let snapshot = editor.snapshot(window, cx);
16273 editor.begin_selection(
16274 Point::new(1, 3).to_display_point(&snapshot),
16275 false,
16276 1,
16277 window,
16278 cx,
16279 );
16280 assert_eq!(
16281 editor.selections.ranges(&editor.display_snapshot(cx)),
16282 [Point::new(1, 3)..Point::new(1, 3)]
16283 );
16284 editor
16285 });
16286
16287 multibuffer.update(cx, |multibuffer, cx| {
16288 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16289 });
16290 _ = editor.update(cx, |editor, window, cx| {
16291 assert_eq!(
16292 editor.selections.ranges(&editor.display_snapshot(cx)),
16293 [Point::new(0, 0)..Point::new(0, 0)]
16294 );
16295
16296 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
16297 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16298 assert_eq!(
16299 editor.selections.ranges(&editor.display_snapshot(cx)),
16300 [Point::new(0, 3)..Point::new(0, 3)]
16301 );
16302 assert!(editor.selections.pending_anchor().is_some());
16303 });
16304}
16305
16306#[gpui::test]
16307async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
16308 init_test(cx, |_| {});
16309
16310 let language = Arc::new(
16311 Language::new(
16312 LanguageConfig {
16313 brackets: BracketPairConfig {
16314 pairs: vec![
16315 BracketPair {
16316 start: "{".to_string(),
16317 end: "}".to_string(),
16318 close: true,
16319 surround: true,
16320 newline: true,
16321 },
16322 BracketPair {
16323 start: "/* ".to_string(),
16324 end: " */".to_string(),
16325 close: true,
16326 surround: true,
16327 newline: true,
16328 },
16329 ],
16330 ..Default::default()
16331 },
16332 ..Default::default()
16333 },
16334 Some(tree_sitter_rust::LANGUAGE.into()),
16335 )
16336 .with_indents_query("")
16337 .unwrap(),
16338 );
16339
16340 let text = concat!(
16341 "{ }\n", //
16342 " x\n", //
16343 " /* */\n", //
16344 "x\n", //
16345 "{{} }\n", //
16346 );
16347
16348 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
16349 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16350 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16351 editor
16352 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
16353 .await;
16354
16355 editor.update_in(cx, |editor, window, cx| {
16356 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16357 s.select_display_ranges([
16358 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
16359 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
16360 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
16361 ])
16362 });
16363 editor.newline(&Newline, window, cx);
16364
16365 assert_eq!(
16366 editor.buffer().read(cx).read(cx).text(),
16367 concat!(
16368 "{ \n", // Suppress rustfmt
16369 "\n", //
16370 "}\n", //
16371 " x\n", //
16372 " /* \n", //
16373 " \n", //
16374 " */\n", //
16375 "x\n", //
16376 "{{} \n", //
16377 "}\n", //
16378 )
16379 );
16380 });
16381}
16382
16383#[gpui::test]
16384fn test_highlighted_ranges(cx: &mut TestAppContext) {
16385 init_test(cx, |_| {});
16386
16387 let editor = cx.add_window(|window, cx| {
16388 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
16389 build_editor(buffer, window, cx)
16390 });
16391
16392 _ = editor.update(cx, |editor, window, cx| {
16393 struct Type1;
16394 struct Type2;
16395
16396 let buffer = editor.buffer.read(cx).snapshot(cx);
16397
16398 let anchor_range =
16399 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
16400
16401 editor.highlight_background::<Type1>(
16402 &[
16403 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
16404 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
16405 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
16406 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
16407 ],
16408 |_| Hsla::red(),
16409 cx,
16410 );
16411 editor.highlight_background::<Type2>(
16412 &[
16413 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
16414 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
16415 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
16416 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
16417 ],
16418 |_| Hsla::green(),
16419 cx,
16420 );
16421
16422 let snapshot = editor.snapshot(window, cx);
16423 let highlighted_ranges = editor.sorted_background_highlights_in_range(
16424 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
16425 &snapshot,
16426 cx.theme(),
16427 );
16428 assert_eq!(
16429 highlighted_ranges,
16430 &[
16431 (
16432 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
16433 Hsla::green(),
16434 ),
16435 (
16436 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
16437 Hsla::red(),
16438 ),
16439 (
16440 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
16441 Hsla::green(),
16442 ),
16443 (
16444 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16445 Hsla::red(),
16446 ),
16447 ]
16448 );
16449 assert_eq!(
16450 editor.sorted_background_highlights_in_range(
16451 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
16452 &snapshot,
16453 cx.theme(),
16454 ),
16455 &[(
16456 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16457 Hsla::red(),
16458 )]
16459 );
16460 });
16461}
16462
16463#[gpui::test]
16464async fn test_following(cx: &mut TestAppContext) {
16465 init_test(cx, |_| {});
16466
16467 let fs = FakeFs::new(cx.executor());
16468 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16469
16470 let buffer = project.update(cx, |project, cx| {
16471 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, false, cx);
16472 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
16473 });
16474 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
16475 let follower = cx.update(|cx| {
16476 cx.open_window(
16477 WindowOptions {
16478 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
16479 gpui::Point::new(px(0.), px(0.)),
16480 gpui::Point::new(px(10.), px(80.)),
16481 ))),
16482 ..Default::default()
16483 },
16484 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
16485 )
16486 .unwrap()
16487 });
16488
16489 let is_still_following = Rc::new(RefCell::new(true));
16490 let follower_edit_event_count = Rc::new(RefCell::new(0));
16491 let pending_update = Rc::new(RefCell::new(None));
16492 let leader_entity = leader.root(cx).unwrap();
16493 let follower_entity = follower.root(cx).unwrap();
16494 _ = follower.update(cx, {
16495 let update = pending_update.clone();
16496 let is_still_following = is_still_following.clone();
16497 let follower_edit_event_count = follower_edit_event_count.clone();
16498 |_, window, cx| {
16499 cx.subscribe_in(
16500 &leader_entity,
16501 window,
16502 move |_, leader, event, window, cx| {
16503 leader.read(cx).add_event_to_update_proto(
16504 event,
16505 &mut update.borrow_mut(),
16506 window,
16507 cx,
16508 );
16509 },
16510 )
16511 .detach();
16512
16513 cx.subscribe_in(
16514 &follower_entity,
16515 window,
16516 move |_, _, event: &EditorEvent, _window, _cx| {
16517 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
16518 *is_still_following.borrow_mut() = false;
16519 }
16520
16521 if let EditorEvent::BufferEdited = event {
16522 *follower_edit_event_count.borrow_mut() += 1;
16523 }
16524 },
16525 )
16526 .detach();
16527 }
16528 });
16529
16530 // Update the selections only
16531 _ = leader.update(cx, |leader, window, cx| {
16532 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16533 s.select_ranges([1..1])
16534 });
16535 });
16536 follower
16537 .update(cx, |follower, window, cx| {
16538 follower.apply_update_proto(
16539 &project,
16540 pending_update.borrow_mut().take().unwrap(),
16541 window,
16542 cx,
16543 )
16544 })
16545 .unwrap()
16546 .await
16547 .unwrap();
16548 _ = follower.update(cx, |follower, _, cx| {
16549 assert_eq!(
16550 follower.selections.ranges(&follower.display_snapshot(cx)),
16551 vec![1..1]
16552 );
16553 });
16554 assert!(*is_still_following.borrow());
16555 assert_eq!(*follower_edit_event_count.borrow(), 0);
16556
16557 // Update the scroll position only
16558 _ = leader.update(cx, |leader, window, cx| {
16559 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16560 });
16561 follower
16562 .update(cx, |follower, window, cx| {
16563 follower.apply_update_proto(
16564 &project,
16565 pending_update.borrow_mut().take().unwrap(),
16566 window,
16567 cx,
16568 )
16569 })
16570 .unwrap()
16571 .await
16572 .unwrap();
16573 assert_eq!(
16574 follower
16575 .update(cx, |follower, _, cx| follower.scroll_position(cx))
16576 .unwrap(),
16577 gpui::Point::new(1.5, 3.5)
16578 );
16579 assert!(*is_still_following.borrow());
16580 assert_eq!(*follower_edit_event_count.borrow(), 0);
16581
16582 // Update the selections and scroll position. The follower's scroll position is updated
16583 // via autoscroll, not via the leader's exact scroll position.
16584 _ = leader.update(cx, |leader, window, cx| {
16585 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16586 s.select_ranges([0..0])
16587 });
16588 leader.request_autoscroll(Autoscroll::newest(), cx);
16589 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16590 });
16591 follower
16592 .update(cx, |follower, window, cx| {
16593 follower.apply_update_proto(
16594 &project,
16595 pending_update.borrow_mut().take().unwrap(),
16596 window,
16597 cx,
16598 )
16599 })
16600 .unwrap()
16601 .await
16602 .unwrap();
16603 _ = follower.update(cx, |follower, _, cx| {
16604 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
16605 assert_eq!(
16606 follower.selections.ranges(&follower.display_snapshot(cx)),
16607 vec![0..0]
16608 );
16609 });
16610 assert!(*is_still_following.borrow());
16611
16612 // Creating a pending selection that precedes another selection
16613 _ = leader.update(cx, |leader, window, cx| {
16614 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16615 s.select_ranges([1..1])
16616 });
16617 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
16618 });
16619 follower
16620 .update(cx, |follower, window, cx| {
16621 follower.apply_update_proto(
16622 &project,
16623 pending_update.borrow_mut().take().unwrap(),
16624 window,
16625 cx,
16626 )
16627 })
16628 .unwrap()
16629 .await
16630 .unwrap();
16631 _ = follower.update(cx, |follower, _, cx| {
16632 assert_eq!(
16633 follower.selections.ranges(&follower.display_snapshot(cx)),
16634 vec![0..0, 1..1]
16635 );
16636 });
16637 assert!(*is_still_following.borrow());
16638
16639 // Extend the pending selection so that it surrounds another selection
16640 _ = leader.update(cx, |leader, window, cx| {
16641 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
16642 });
16643 follower
16644 .update(cx, |follower, window, cx| {
16645 follower.apply_update_proto(
16646 &project,
16647 pending_update.borrow_mut().take().unwrap(),
16648 window,
16649 cx,
16650 )
16651 })
16652 .unwrap()
16653 .await
16654 .unwrap();
16655 _ = follower.update(cx, |follower, _, cx| {
16656 assert_eq!(
16657 follower.selections.ranges(&follower.display_snapshot(cx)),
16658 vec![0..2]
16659 );
16660 });
16661
16662 // Scrolling locally breaks the follow
16663 _ = follower.update(cx, |follower, window, cx| {
16664 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
16665 follower.set_scroll_anchor(
16666 ScrollAnchor {
16667 anchor: top_anchor,
16668 offset: gpui::Point::new(0.0, 0.5),
16669 },
16670 window,
16671 cx,
16672 );
16673 });
16674 assert!(!(*is_still_following.borrow()));
16675}
16676
16677#[gpui::test]
16678async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
16679 init_test(cx, |_| {});
16680
16681 let fs = FakeFs::new(cx.executor());
16682 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16683 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16684 let pane = workspace
16685 .update(cx, |workspace, _, _| workspace.active_pane().clone())
16686 .unwrap();
16687
16688 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16689
16690 let leader = pane.update_in(cx, |_, window, cx| {
16691 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
16692 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
16693 });
16694
16695 // Start following the editor when it has no excerpts.
16696 let mut state_message =
16697 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16698 let workspace_entity = workspace.root(cx).unwrap();
16699 let follower_1 = cx
16700 .update_window(*workspace.deref(), |_, window, cx| {
16701 Editor::from_state_proto(
16702 workspace_entity,
16703 ViewId {
16704 creator: CollaboratorId::PeerId(PeerId::default()),
16705 id: 0,
16706 },
16707 &mut state_message,
16708 window,
16709 cx,
16710 )
16711 })
16712 .unwrap()
16713 .unwrap()
16714 .await
16715 .unwrap();
16716
16717 let update_message = Rc::new(RefCell::new(None));
16718 follower_1.update_in(cx, {
16719 let update = update_message.clone();
16720 |_, window, cx| {
16721 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
16722 leader.read(cx).add_event_to_update_proto(
16723 event,
16724 &mut update.borrow_mut(),
16725 window,
16726 cx,
16727 );
16728 })
16729 .detach();
16730 }
16731 });
16732
16733 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
16734 (
16735 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, false, cx),
16736 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, false, cx),
16737 )
16738 });
16739
16740 // Insert some excerpts.
16741 leader.update(cx, |leader, cx| {
16742 leader.buffer.update(cx, |multibuffer, cx| {
16743 multibuffer.set_excerpts_for_path(
16744 PathKey::with_sort_prefix(1, rel_path("b.txt").into_arc()),
16745 buffer_1.clone(),
16746 vec![
16747 Point::row_range(0..3),
16748 Point::row_range(1..6),
16749 Point::row_range(12..15),
16750 ],
16751 0,
16752 cx,
16753 );
16754 multibuffer.set_excerpts_for_path(
16755 PathKey::with_sort_prefix(1, rel_path("a.txt").into_arc()),
16756 buffer_2.clone(),
16757 vec![Point::row_range(0..6), Point::row_range(8..12)],
16758 0,
16759 cx,
16760 );
16761 });
16762 });
16763
16764 // Apply the update of adding the excerpts.
16765 follower_1
16766 .update_in(cx, |follower, window, cx| {
16767 follower.apply_update_proto(
16768 &project,
16769 update_message.borrow().clone().unwrap(),
16770 window,
16771 cx,
16772 )
16773 })
16774 .await
16775 .unwrap();
16776 assert_eq!(
16777 follower_1.update(cx, |editor, cx| editor.text(cx)),
16778 leader.update(cx, |editor, cx| editor.text(cx))
16779 );
16780 update_message.borrow_mut().take();
16781
16782 // Start following separately after it already has excerpts.
16783 let mut state_message =
16784 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16785 let workspace_entity = workspace.root(cx).unwrap();
16786 let follower_2 = cx
16787 .update_window(*workspace.deref(), |_, window, cx| {
16788 Editor::from_state_proto(
16789 workspace_entity,
16790 ViewId {
16791 creator: CollaboratorId::PeerId(PeerId::default()),
16792 id: 0,
16793 },
16794 &mut state_message,
16795 window,
16796 cx,
16797 )
16798 })
16799 .unwrap()
16800 .unwrap()
16801 .await
16802 .unwrap();
16803 assert_eq!(
16804 follower_2.update(cx, |editor, cx| editor.text(cx)),
16805 leader.update(cx, |editor, cx| editor.text(cx))
16806 );
16807
16808 // Remove some excerpts.
16809 leader.update(cx, |leader, cx| {
16810 leader.buffer.update(cx, |multibuffer, cx| {
16811 let excerpt_ids = multibuffer.excerpt_ids();
16812 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
16813 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
16814 });
16815 });
16816
16817 // Apply the update of removing the excerpts.
16818 follower_1
16819 .update_in(cx, |follower, window, cx| {
16820 follower.apply_update_proto(
16821 &project,
16822 update_message.borrow().clone().unwrap(),
16823 window,
16824 cx,
16825 )
16826 })
16827 .await
16828 .unwrap();
16829 follower_2
16830 .update_in(cx, |follower, window, cx| {
16831 follower.apply_update_proto(
16832 &project,
16833 update_message.borrow().clone().unwrap(),
16834 window,
16835 cx,
16836 )
16837 })
16838 .await
16839 .unwrap();
16840 update_message.borrow_mut().take();
16841 assert_eq!(
16842 follower_1.update(cx, |editor, cx| editor.text(cx)),
16843 leader.update(cx, |editor, cx| editor.text(cx))
16844 );
16845}
16846
16847#[gpui::test]
16848async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16849 init_test(cx, |_| {});
16850
16851 let mut cx = EditorTestContext::new(cx).await;
16852 let lsp_store =
16853 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
16854
16855 cx.set_state(indoc! {"
16856 ˇfn func(abc def: i32) -> u32 {
16857 }
16858 "});
16859
16860 cx.update(|_, cx| {
16861 lsp_store.update(cx, |lsp_store, cx| {
16862 lsp_store
16863 .update_diagnostics(
16864 LanguageServerId(0),
16865 lsp::PublishDiagnosticsParams {
16866 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
16867 version: None,
16868 diagnostics: vec![
16869 lsp::Diagnostic {
16870 range: lsp::Range::new(
16871 lsp::Position::new(0, 11),
16872 lsp::Position::new(0, 12),
16873 ),
16874 severity: Some(lsp::DiagnosticSeverity::ERROR),
16875 ..Default::default()
16876 },
16877 lsp::Diagnostic {
16878 range: lsp::Range::new(
16879 lsp::Position::new(0, 12),
16880 lsp::Position::new(0, 15),
16881 ),
16882 severity: Some(lsp::DiagnosticSeverity::ERROR),
16883 ..Default::default()
16884 },
16885 lsp::Diagnostic {
16886 range: lsp::Range::new(
16887 lsp::Position::new(0, 25),
16888 lsp::Position::new(0, 28),
16889 ),
16890 severity: Some(lsp::DiagnosticSeverity::ERROR),
16891 ..Default::default()
16892 },
16893 ],
16894 },
16895 None,
16896 DiagnosticSourceKind::Pushed,
16897 &[],
16898 cx,
16899 )
16900 .unwrap()
16901 });
16902 });
16903
16904 executor.run_until_parked();
16905
16906 cx.update_editor(|editor, window, cx| {
16907 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16908 });
16909
16910 cx.assert_editor_state(indoc! {"
16911 fn func(abc def: i32) -> ˇu32 {
16912 }
16913 "});
16914
16915 cx.update_editor(|editor, window, cx| {
16916 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16917 });
16918
16919 cx.assert_editor_state(indoc! {"
16920 fn func(abc ˇdef: i32) -> u32 {
16921 }
16922 "});
16923
16924 cx.update_editor(|editor, window, cx| {
16925 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16926 });
16927
16928 cx.assert_editor_state(indoc! {"
16929 fn func(abcˇ def: i32) -> u32 {
16930 }
16931 "});
16932
16933 cx.update_editor(|editor, window, cx| {
16934 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16935 });
16936
16937 cx.assert_editor_state(indoc! {"
16938 fn func(abc def: i32) -> ˇu32 {
16939 }
16940 "});
16941}
16942
16943#[gpui::test]
16944async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16945 init_test(cx, |_| {});
16946
16947 let mut cx = EditorTestContext::new(cx).await;
16948
16949 let diff_base = r#"
16950 use some::mod;
16951
16952 const A: u32 = 42;
16953
16954 fn main() {
16955 println!("hello");
16956
16957 println!("world");
16958 }
16959 "#
16960 .unindent();
16961
16962 // Edits are modified, removed, modified, added
16963 cx.set_state(
16964 &r#"
16965 use some::modified;
16966
16967 ˇ
16968 fn main() {
16969 println!("hello there");
16970
16971 println!("around the");
16972 println!("world");
16973 }
16974 "#
16975 .unindent(),
16976 );
16977
16978 cx.set_head_text(&diff_base);
16979 executor.run_until_parked();
16980
16981 cx.update_editor(|editor, window, cx| {
16982 //Wrap around the bottom of the buffer
16983 for _ in 0..3 {
16984 editor.go_to_next_hunk(&GoToHunk, window, cx);
16985 }
16986 });
16987
16988 cx.assert_editor_state(
16989 &r#"
16990 ˇuse some::modified;
16991
16992
16993 fn main() {
16994 println!("hello there");
16995
16996 println!("around the");
16997 println!("world");
16998 }
16999 "#
17000 .unindent(),
17001 );
17002
17003 cx.update_editor(|editor, window, cx| {
17004 //Wrap around the top of the buffer
17005 for _ in 0..2 {
17006 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17007 }
17008 });
17009
17010 cx.assert_editor_state(
17011 &r#"
17012 use some::modified;
17013
17014
17015 fn main() {
17016 ˇ println!("hello there");
17017
17018 println!("around the");
17019 println!("world");
17020 }
17021 "#
17022 .unindent(),
17023 );
17024
17025 cx.update_editor(|editor, window, cx| {
17026 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17027 });
17028
17029 cx.assert_editor_state(
17030 &r#"
17031 use some::modified;
17032
17033 ˇ
17034 fn main() {
17035 println!("hello there");
17036
17037 println!("around the");
17038 println!("world");
17039 }
17040 "#
17041 .unindent(),
17042 );
17043
17044 cx.update_editor(|editor, window, cx| {
17045 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17046 });
17047
17048 cx.assert_editor_state(
17049 &r#"
17050 ˇuse some::modified;
17051
17052
17053 fn main() {
17054 println!("hello there");
17055
17056 println!("around the");
17057 println!("world");
17058 }
17059 "#
17060 .unindent(),
17061 );
17062
17063 cx.update_editor(|editor, window, cx| {
17064 for _ in 0..2 {
17065 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17066 }
17067 });
17068
17069 cx.assert_editor_state(
17070 &r#"
17071 use some::modified;
17072
17073
17074 fn main() {
17075 ˇ println!("hello there");
17076
17077 println!("around the");
17078 println!("world");
17079 }
17080 "#
17081 .unindent(),
17082 );
17083
17084 cx.update_editor(|editor, window, cx| {
17085 editor.fold(&Fold, window, cx);
17086 });
17087
17088 cx.update_editor(|editor, window, cx| {
17089 editor.go_to_next_hunk(&GoToHunk, window, cx);
17090 });
17091
17092 cx.assert_editor_state(
17093 &r#"
17094 ˇuse some::modified;
17095
17096
17097 fn main() {
17098 println!("hello there");
17099
17100 println!("around the");
17101 println!("world");
17102 }
17103 "#
17104 .unindent(),
17105 );
17106}
17107
17108#[test]
17109fn test_split_words() {
17110 fn split(text: &str) -> Vec<&str> {
17111 split_words(text).collect()
17112 }
17113
17114 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
17115 assert_eq!(split("hello_world"), &["hello_", "world"]);
17116 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
17117 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
17118 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
17119 assert_eq!(split("helloworld"), &["helloworld"]);
17120
17121 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
17122}
17123
17124#[gpui::test]
17125async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
17126 init_test(cx, |_| {});
17127
17128 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
17129 let mut assert = |before, after| {
17130 let _state_context = cx.set_state(before);
17131 cx.run_until_parked();
17132 cx.update_editor(|editor, window, cx| {
17133 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
17134 });
17135 cx.run_until_parked();
17136 cx.assert_editor_state(after);
17137 };
17138
17139 // Outside bracket jumps to outside of matching bracket
17140 assert("console.logˇ(var);", "console.log(var)ˇ;");
17141 assert("console.log(var)ˇ;", "console.logˇ(var);");
17142
17143 // Inside bracket jumps to inside of matching bracket
17144 assert("console.log(ˇvar);", "console.log(varˇ);");
17145 assert("console.log(varˇ);", "console.log(ˇvar);");
17146
17147 // When outside a bracket and inside, favor jumping to the inside bracket
17148 assert(
17149 "console.log('foo', [1, 2, 3]ˇ);",
17150 "console.log(ˇ'foo', [1, 2, 3]);",
17151 );
17152 assert(
17153 "console.log(ˇ'foo', [1, 2, 3]);",
17154 "console.log('foo', [1, 2, 3]ˇ);",
17155 );
17156
17157 // Bias forward if two options are equally likely
17158 assert(
17159 "let result = curried_fun()ˇ();",
17160 "let result = curried_fun()()ˇ;",
17161 );
17162
17163 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
17164 assert(
17165 indoc! {"
17166 function test() {
17167 console.log('test')ˇ
17168 }"},
17169 indoc! {"
17170 function test() {
17171 console.logˇ('test')
17172 }"},
17173 );
17174}
17175
17176#[gpui::test]
17177async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
17178 init_test(cx, |_| {});
17179
17180 let fs = FakeFs::new(cx.executor());
17181 fs.insert_tree(
17182 path!("/a"),
17183 json!({
17184 "main.rs": "fn main() { let a = 5; }",
17185 "other.rs": "// Test file",
17186 }),
17187 )
17188 .await;
17189 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17190
17191 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17192 language_registry.add(Arc::new(Language::new(
17193 LanguageConfig {
17194 name: "Rust".into(),
17195 matcher: LanguageMatcher {
17196 path_suffixes: vec!["rs".to_string()],
17197 ..Default::default()
17198 },
17199 brackets: BracketPairConfig {
17200 pairs: vec![BracketPair {
17201 start: "{".to_string(),
17202 end: "}".to_string(),
17203 close: true,
17204 surround: true,
17205 newline: true,
17206 }],
17207 disabled_scopes_by_bracket_ix: Vec::new(),
17208 },
17209 ..Default::default()
17210 },
17211 Some(tree_sitter_rust::LANGUAGE.into()),
17212 )));
17213 let mut fake_servers = language_registry.register_fake_lsp(
17214 "Rust",
17215 FakeLspAdapter {
17216 capabilities: lsp::ServerCapabilities {
17217 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
17218 first_trigger_character: "{".to_string(),
17219 more_trigger_character: None,
17220 }),
17221 ..Default::default()
17222 },
17223 ..Default::default()
17224 },
17225 );
17226
17227 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17228
17229 let cx = &mut VisualTestContext::from_window(*workspace, cx);
17230
17231 let worktree_id = workspace
17232 .update(cx, |workspace, _, cx| {
17233 workspace.project().update(cx, |project, cx| {
17234 project.worktrees(cx).next().unwrap().read(cx).id()
17235 })
17236 })
17237 .unwrap();
17238
17239 let buffer = project
17240 .update(cx, |project, cx| {
17241 project.open_local_buffer(path!("/a/main.rs"), cx)
17242 })
17243 .await
17244 .unwrap();
17245 let editor_handle = workspace
17246 .update(cx, |workspace, window, cx| {
17247 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
17248 })
17249 .unwrap()
17250 .await
17251 .unwrap()
17252 .downcast::<Editor>()
17253 .unwrap();
17254
17255 cx.executor().start_waiting();
17256 let fake_server = fake_servers.next().await.unwrap();
17257
17258 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
17259 |params, _| async move {
17260 assert_eq!(
17261 params.text_document_position.text_document.uri,
17262 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
17263 );
17264 assert_eq!(
17265 params.text_document_position.position,
17266 lsp::Position::new(0, 21),
17267 );
17268
17269 Ok(Some(vec![lsp::TextEdit {
17270 new_text: "]".to_string(),
17271 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17272 }]))
17273 },
17274 );
17275
17276 editor_handle.update_in(cx, |editor, window, cx| {
17277 window.focus(&editor.focus_handle(cx));
17278 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17279 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
17280 });
17281 editor.handle_input("{", window, cx);
17282 });
17283
17284 cx.executor().run_until_parked();
17285
17286 buffer.update(cx, |buffer, _| {
17287 assert_eq!(
17288 buffer.text(),
17289 "fn main() { let a = {5}; }",
17290 "No extra braces from on type formatting should appear in the buffer"
17291 )
17292 });
17293}
17294
17295#[gpui::test(iterations = 20, seeds(31))]
17296async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
17297 init_test(cx, |_| {});
17298
17299 let mut cx = EditorLspTestContext::new_rust(
17300 lsp::ServerCapabilities {
17301 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
17302 first_trigger_character: ".".to_string(),
17303 more_trigger_character: None,
17304 }),
17305 ..Default::default()
17306 },
17307 cx,
17308 )
17309 .await;
17310
17311 cx.update_buffer(|buffer, _| {
17312 // This causes autoindent to be async.
17313 buffer.set_sync_parse_timeout(Duration::ZERO)
17314 });
17315
17316 cx.set_state("fn c() {\n d()ˇ\n}\n");
17317 cx.simulate_keystroke("\n");
17318 cx.run_until_parked();
17319
17320 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
17321 let mut request =
17322 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
17323 let buffer_cloned = buffer_cloned.clone();
17324 async move {
17325 buffer_cloned.update(&mut cx, |buffer, _| {
17326 assert_eq!(
17327 buffer.text(),
17328 "fn c() {\n d()\n .\n}\n",
17329 "OnTypeFormatting should triggered after autoindent applied"
17330 )
17331 })?;
17332
17333 Ok(Some(vec![]))
17334 }
17335 });
17336
17337 cx.simulate_keystroke(".");
17338 cx.run_until_parked();
17339
17340 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
17341 assert!(request.next().await.is_some());
17342 request.close();
17343 assert!(request.next().await.is_none());
17344}
17345
17346#[gpui::test]
17347async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
17348 init_test(cx, |_| {});
17349
17350 let fs = FakeFs::new(cx.executor());
17351 fs.insert_tree(
17352 path!("/a"),
17353 json!({
17354 "main.rs": "fn main() { let a = 5; }",
17355 "other.rs": "// Test file",
17356 }),
17357 )
17358 .await;
17359
17360 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17361
17362 let server_restarts = Arc::new(AtomicUsize::new(0));
17363 let closure_restarts = Arc::clone(&server_restarts);
17364 let language_server_name = "test language server";
17365 let language_name: LanguageName = "Rust".into();
17366
17367 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17368 language_registry.add(Arc::new(Language::new(
17369 LanguageConfig {
17370 name: language_name.clone(),
17371 matcher: LanguageMatcher {
17372 path_suffixes: vec!["rs".to_string()],
17373 ..Default::default()
17374 },
17375 ..Default::default()
17376 },
17377 Some(tree_sitter_rust::LANGUAGE.into()),
17378 )));
17379 let mut fake_servers = language_registry.register_fake_lsp(
17380 "Rust",
17381 FakeLspAdapter {
17382 name: language_server_name,
17383 initialization_options: Some(json!({
17384 "testOptionValue": true
17385 })),
17386 initializer: Some(Box::new(move |fake_server| {
17387 let task_restarts = Arc::clone(&closure_restarts);
17388 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
17389 task_restarts.fetch_add(1, atomic::Ordering::Release);
17390 futures::future::ready(Ok(()))
17391 });
17392 })),
17393 ..Default::default()
17394 },
17395 );
17396
17397 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17398 let _buffer = project
17399 .update(cx, |project, cx| {
17400 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
17401 })
17402 .await
17403 .unwrap();
17404 let _fake_server = fake_servers.next().await.unwrap();
17405 update_test_language_settings(cx, |language_settings| {
17406 language_settings.languages.0.insert(
17407 language_name.clone().0,
17408 LanguageSettingsContent {
17409 tab_size: NonZeroU32::new(8),
17410 ..Default::default()
17411 },
17412 );
17413 });
17414 cx.executor().run_until_parked();
17415 assert_eq!(
17416 server_restarts.load(atomic::Ordering::Acquire),
17417 0,
17418 "Should not restart LSP server on an unrelated change"
17419 );
17420
17421 update_test_project_settings(cx, |project_settings| {
17422 project_settings.lsp.insert(
17423 "Some other server name".into(),
17424 LspSettings {
17425 binary: None,
17426 settings: None,
17427 initialization_options: Some(json!({
17428 "some other init value": false
17429 })),
17430 enable_lsp_tasks: false,
17431 fetch: None,
17432 },
17433 );
17434 });
17435 cx.executor().run_until_parked();
17436 assert_eq!(
17437 server_restarts.load(atomic::Ordering::Acquire),
17438 0,
17439 "Should not restart LSP server on an unrelated LSP settings change"
17440 );
17441
17442 update_test_project_settings(cx, |project_settings| {
17443 project_settings.lsp.insert(
17444 language_server_name.into(),
17445 LspSettings {
17446 binary: None,
17447 settings: None,
17448 initialization_options: Some(json!({
17449 "anotherInitValue": false
17450 })),
17451 enable_lsp_tasks: false,
17452 fetch: None,
17453 },
17454 );
17455 });
17456 cx.executor().run_until_parked();
17457 assert_eq!(
17458 server_restarts.load(atomic::Ordering::Acquire),
17459 1,
17460 "Should restart LSP server on a related LSP settings change"
17461 );
17462
17463 update_test_project_settings(cx, |project_settings| {
17464 project_settings.lsp.insert(
17465 language_server_name.into(),
17466 LspSettings {
17467 binary: None,
17468 settings: None,
17469 initialization_options: Some(json!({
17470 "anotherInitValue": false
17471 })),
17472 enable_lsp_tasks: false,
17473 fetch: None,
17474 },
17475 );
17476 });
17477 cx.executor().run_until_parked();
17478 assert_eq!(
17479 server_restarts.load(atomic::Ordering::Acquire),
17480 1,
17481 "Should not restart LSP server on a related LSP settings change that is the same"
17482 );
17483
17484 update_test_project_settings(cx, |project_settings| {
17485 project_settings.lsp.insert(
17486 language_server_name.into(),
17487 LspSettings {
17488 binary: None,
17489 settings: None,
17490 initialization_options: None,
17491 enable_lsp_tasks: false,
17492 fetch: None,
17493 },
17494 );
17495 });
17496 cx.executor().run_until_parked();
17497 assert_eq!(
17498 server_restarts.load(atomic::Ordering::Acquire),
17499 2,
17500 "Should restart LSP server on another related LSP settings change"
17501 );
17502}
17503
17504#[gpui::test]
17505async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
17506 init_test(cx, |_| {});
17507
17508 let mut cx = EditorLspTestContext::new_rust(
17509 lsp::ServerCapabilities {
17510 completion_provider: Some(lsp::CompletionOptions {
17511 trigger_characters: Some(vec![".".to_string()]),
17512 resolve_provider: Some(true),
17513 ..Default::default()
17514 }),
17515 ..Default::default()
17516 },
17517 cx,
17518 )
17519 .await;
17520
17521 cx.set_state("fn main() { let a = 2ˇ; }");
17522 cx.simulate_keystroke(".");
17523 let completion_item = lsp::CompletionItem {
17524 label: "some".into(),
17525 kind: Some(lsp::CompletionItemKind::SNIPPET),
17526 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
17527 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
17528 kind: lsp::MarkupKind::Markdown,
17529 value: "```rust\nSome(2)\n```".to_string(),
17530 })),
17531 deprecated: Some(false),
17532 sort_text: Some("fffffff2".to_string()),
17533 filter_text: Some("some".to_string()),
17534 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
17535 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17536 range: lsp::Range {
17537 start: lsp::Position {
17538 line: 0,
17539 character: 22,
17540 },
17541 end: lsp::Position {
17542 line: 0,
17543 character: 22,
17544 },
17545 },
17546 new_text: "Some(2)".to_string(),
17547 })),
17548 additional_text_edits: Some(vec![lsp::TextEdit {
17549 range: lsp::Range {
17550 start: lsp::Position {
17551 line: 0,
17552 character: 20,
17553 },
17554 end: lsp::Position {
17555 line: 0,
17556 character: 22,
17557 },
17558 },
17559 new_text: "".to_string(),
17560 }]),
17561 ..Default::default()
17562 };
17563
17564 let closure_completion_item = completion_item.clone();
17565 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17566 let task_completion_item = closure_completion_item.clone();
17567 async move {
17568 Ok(Some(lsp::CompletionResponse::Array(vec![
17569 task_completion_item,
17570 ])))
17571 }
17572 });
17573
17574 request.next().await;
17575
17576 cx.condition(|editor, _| editor.context_menu_visible())
17577 .await;
17578 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
17579 editor
17580 .confirm_completion(&ConfirmCompletion::default(), window, cx)
17581 .unwrap()
17582 });
17583 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
17584
17585 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
17586 let task_completion_item = completion_item.clone();
17587 async move { Ok(task_completion_item) }
17588 })
17589 .next()
17590 .await
17591 .unwrap();
17592 apply_additional_edits.await.unwrap();
17593 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
17594}
17595
17596#[gpui::test]
17597async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
17598 init_test(cx, |_| {});
17599
17600 let mut cx = EditorLspTestContext::new_rust(
17601 lsp::ServerCapabilities {
17602 completion_provider: Some(lsp::CompletionOptions {
17603 trigger_characters: Some(vec![".".to_string()]),
17604 resolve_provider: Some(true),
17605 ..Default::default()
17606 }),
17607 ..Default::default()
17608 },
17609 cx,
17610 )
17611 .await;
17612
17613 cx.set_state("fn main() { let a = 2ˇ; }");
17614 cx.simulate_keystroke(".");
17615
17616 let item1 = lsp::CompletionItem {
17617 label: "method id()".to_string(),
17618 filter_text: Some("id".to_string()),
17619 detail: None,
17620 documentation: None,
17621 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17622 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17623 new_text: ".id".to_string(),
17624 })),
17625 ..lsp::CompletionItem::default()
17626 };
17627
17628 let item2 = 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
17640 let item1 = item1.clone();
17641 cx.set_request_handler::<lsp::request::Completion, _, _>({
17642 let item1 = item1.clone();
17643 move |_, _, _| {
17644 let item1 = item1.clone();
17645 let item2 = item2.clone();
17646 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
17647 }
17648 })
17649 .next()
17650 .await;
17651
17652 cx.condition(|editor, _| editor.context_menu_visible())
17653 .await;
17654 cx.update_editor(|editor, _, _| {
17655 let context_menu = editor.context_menu.borrow_mut();
17656 let context_menu = context_menu
17657 .as_ref()
17658 .expect("Should have the context menu deployed");
17659 match context_menu {
17660 CodeContextMenu::Completions(completions_menu) => {
17661 let completions = completions_menu.completions.borrow_mut();
17662 assert_eq!(
17663 completions
17664 .iter()
17665 .map(|completion| &completion.label.text)
17666 .collect::<Vec<_>>(),
17667 vec!["method id()", "other"]
17668 )
17669 }
17670 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17671 }
17672 });
17673
17674 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
17675 let item1 = item1.clone();
17676 move |_, item_to_resolve, _| {
17677 let item1 = item1.clone();
17678 async move {
17679 if item1 == item_to_resolve {
17680 Ok(lsp::CompletionItem {
17681 label: "method id()".to_string(),
17682 filter_text: Some("id".to_string()),
17683 detail: Some("Now resolved!".to_string()),
17684 documentation: Some(lsp::Documentation::String("Docs".to_string())),
17685 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17686 range: lsp::Range::new(
17687 lsp::Position::new(0, 22),
17688 lsp::Position::new(0, 22),
17689 ),
17690 new_text: ".id".to_string(),
17691 })),
17692 ..lsp::CompletionItem::default()
17693 })
17694 } else {
17695 Ok(item_to_resolve)
17696 }
17697 }
17698 }
17699 })
17700 .next()
17701 .await
17702 .unwrap();
17703 cx.run_until_parked();
17704
17705 cx.update_editor(|editor, window, cx| {
17706 editor.context_menu_next(&Default::default(), window, cx);
17707 });
17708
17709 cx.update_editor(|editor, _, _| {
17710 let context_menu = editor.context_menu.borrow_mut();
17711 let context_menu = context_menu
17712 .as_ref()
17713 .expect("Should have the context menu deployed");
17714 match context_menu {
17715 CodeContextMenu::Completions(completions_menu) => {
17716 let completions = completions_menu.completions.borrow_mut();
17717 assert_eq!(
17718 completions
17719 .iter()
17720 .map(|completion| &completion.label.text)
17721 .collect::<Vec<_>>(),
17722 vec!["method id() Now resolved!", "other"],
17723 "Should update first completion label, but not second as the filter text did not match."
17724 );
17725 }
17726 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17727 }
17728 });
17729}
17730
17731#[gpui::test]
17732async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
17733 init_test(cx, |_| {});
17734 let mut cx = EditorLspTestContext::new_rust(
17735 lsp::ServerCapabilities {
17736 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
17737 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
17738 completion_provider: Some(lsp::CompletionOptions {
17739 resolve_provider: Some(true),
17740 ..Default::default()
17741 }),
17742 ..Default::default()
17743 },
17744 cx,
17745 )
17746 .await;
17747 cx.set_state(indoc! {"
17748 struct TestStruct {
17749 field: i32
17750 }
17751
17752 fn mainˇ() {
17753 let unused_var = 42;
17754 let test_struct = TestStruct { field: 42 };
17755 }
17756 "});
17757 let symbol_range = cx.lsp_range(indoc! {"
17758 struct TestStruct {
17759 field: i32
17760 }
17761
17762 «fn main»() {
17763 let unused_var = 42;
17764 let test_struct = TestStruct { field: 42 };
17765 }
17766 "});
17767 let mut hover_requests =
17768 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
17769 Ok(Some(lsp::Hover {
17770 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
17771 kind: lsp::MarkupKind::Markdown,
17772 value: "Function documentation".to_string(),
17773 }),
17774 range: Some(symbol_range),
17775 }))
17776 });
17777
17778 // Case 1: Test that code action menu hide hover popover
17779 cx.dispatch_action(Hover);
17780 hover_requests.next().await;
17781 cx.condition(|editor, _| editor.hover_state.visible()).await;
17782 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
17783 move |_, _, _| async move {
17784 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
17785 lsp::CodeAction {
17786 title: "Remove unused variable".to_string(),
17787 kind: Some(CodeActionKind::QUICKFIX),
17788 edit: Some(lsp::WorkspaceEdit {
17789 changes: Some(
17790 [(
17791 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
17792 vec![lsp::TextEdit {
17793 range: lsp::Range::new(
17794 lsp::Position::new(5, 4),
17795 lsp::Position::new(5, 27),
17796 ),
17797 new_text: "".to_string(),
17798 }],
17799 )]
17800 .into_iter()
17801 .collect(),
17802 ),
17803 ..Default::default()
17804 }),
17805 ..Default::default()
17806 },
17807 )]))
17808 },
17809 );
17810 cx.update_editor(|editor, window, cx| {
17811 editor.toggle_code_actions(
17812 &ToggleCodeActions {
17813 deployed_from: None,
17814 quick_launch: false,
17815 },
17816 window,
17817 cx,
17818 );
17819 });
17820 code_action_requests.next().await;
17821 cx.run_until_parked();
17822 cx.condition(|editor, _| editor.context_menu_visible())
17823 .await;
17824 cx.update_editor(|editor, _, _| {
17825 assert!(
17826 !editor.hover_state.visible(),
17827 "Hover popover should be hidden when code action menu is shown"
17828 );
17829 // Hide code actions
17830 editor.context_menu.take();
17831 });
17832
17833 // Case 2: Test that code completions hide hover popover
17834 cx.dispatch_action(Hover);
17835 hover_requests.next().await;
17836 cx.condition(|editor, _| editor.hover_state.visible()).await;
17837 let counter = Arc::new(AtomicUsize::new(0));
17838 let mut completion_requests =
17839 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17840 let counter = counter.clone();
17841 async move {
17842 counter.fetch_add(1, atomic::Ordering::Release);
17843 Ok(Some(lsp::CompletionResponse::Array(vec![
17844 lsp::CompletionItem {
17845 label: "main".into(),
17846 kind: Some(lsp::CompletionItemKind::FUNCTION),
17847 detail: Some("() -> ()".to_string()),
17848 ..Default::default()
17849 },
17850 lsp::CompletionItem {
17851 label: "TestStruct".into(),
17852 kind: Some(lsp::CompletionItemKind::STRUCT),
17853 detail: Some("struct TestStruct".to_string()),
17854 ..Default::default()
17855 },
17856 ])))
17857 }
17858 });
17859 cx.update_editor(|editor, window, cx| {
17860 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
17861 });
17862 completion_requests.next().await;
17863 cx.condition(|editor, _| editor.context_menu_visible())
17864 .await;
17865 cx.update_editor(|editor, _, _| {
17866 assert!(
17867 !editor.hover_state.visible(),
17868 "Hover popover should be hidden when completion menu is shown"
17869 );
17870 });
17871}
17872
17873#[gpui::test]
17874async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
17875 init_test(cx, |_| {});
17876
17877 let mut cx = EditorLspTestContext::new_rust(
17878 lsp::ServerCapabilities {
17879 completion_provider: Some(lsp::CompletionOptions {
17880 trigger_characters: Some(vec![".".to_string()]),
17881 resolve_provider: Some(true),
17882 ..Default::default()
17883 }),
17884 ..Default::default()
17885 },
17886 cx,
17887 )
17888 .await;
17889
17890 cx.set_state("fn main() { let a = 2ˇ; }");
17891 cx.simulate_keystroke(".");
17892
17893 let unresolved_item_1 = lsp::CompletionItem {
17894 label: "id".to_string(),
17895 filter_text: Some("id".to_string()),
17896 detail: None,
17897 documentation: None,
17898 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17899 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17900 new_text: ".id".to_string(),
17901 })),
17902 ..lsp::CompletionItem::default()
17903 };
17904 let resolved_item_1 = lsp::CompletionItem {
17905 additional_text_edits: Some(vec![lsp::TextEdit {
17906 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17907 new_text: "!!".to_string(),
17908 }]),
17909 ..unresolved_item_1.clone()
17910 };
17911 let unresolved_item_2 = lsp::CompletionItem {
17912 label: "other".to_string(),
17913 filter_text: Some("other".to_string()),
17914 detail: None,
17915 documentation: None,
17916 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17917 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17918 new_text: ".other".to_string(),
17919 })),
17920 ..lsp::CompletionItem::default()
17921 };
17922 let resolved_item_2 = lsp::CompletionItem {
17923 additional_text_edits: Some(vec![lsp::TextEdit {
17924 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17925 new_text: "??".to_string(),
17926 }]),
17927 ..unresolved_item_2.clone()
17928 };
17929
17930 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
17931 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
17932 cx.lsp
17933 .server
17934 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
17935 let unresolved_item_1 = unresolved_item_1.clone();
17936 let resolved_item_1 = resolved_item_1.clone();
17937 let unresolved_item_2 = unresolved_item_2.clone();
17938 let resolved_item_2 = resolved_item_2.clone();
17939 let resolve_requests_1 = resolve_requests_1.clone();
17940 let resolve_requests_2 = resolve_requests_2.clone();
17941 move |unresolved_request, _| {
17942 let unresolved_item_1 = unresolved_item_1.clone();
17943 let resolved_item_1 = resolved_item_1.clone();
17944 let unresolved_item_2 = unresolved_item_2.clone();
17945 let resolved_item_2 = resolved_item_2.clone();
17946 let resolve_requests_1 = resolve_requests_1.clone();
17947 let resolve_requests_2 = resolve_requests_2.clone();
17948 async move {
17949 if unresolved_request == unresolved_item_1 {
17950 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
17951 Ok(resolved_item_1.clone())
17952 } else if unresolved_request == unresolved_item_2 {
17953 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
17954 Ok(resolved_item_2.clone())
17955 } else {
17956 panic!("Unexpected completion item {unresolved_request:?}")
17957 }
17958 }
17959 }
17960 })
17961 .detach();
17962
17963 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17964 let unresolved_item_1 = unresolved_item_1.clone();
17965 let unresolved_item_2 = unresolved_item_2.clone();
17966 async move {
17967 Ok(Some(lsp::CompletionResponse::Array(vec![
17968 unresolved_item_1,
17969 unresolved_item_2,
17970 ])))
17971 }
17972 })
17973 .next()
17974 .await;
17975
17976 cx.condition(|editor, _| editor.context_menu_visible())
17977 .await;
17978 cx.update_editor(|editor, _, _| {
17979 let context_menu = editor.context_menu.borrow_mut();
17980 let context_menu = context_menu
17981 .as_ref()
17982 .expect("Should have the context menu deployed");
17983 match context_menu {
17984 CodeContextMenu::Completions(completions_menu) => {
17985 let completions = completions_menu.completions.borrow_mut();
17986 assert_eq!(
17987 completions
17988 .iter()
17989 .map(|completion| &completion.label.text)
17990 .collect::<Vec<_>>(),
17991 vec!["id", "other"]
17992 )
17993 }
17994 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17995 }
17996 });
17997 cx.run_until_parked();
17998
17999 cx.update_editor(|editor, window, cx| {
18000 editor.context_menu_next(&ContextMenuNext, window, cx);
18001 });
18002 cx.run_until_parked();
18003 cx.update_editor(|editor, window, cx| {
18004 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
18005 });
18006 cx.run_until_parked();
18007 cx.update_editor(|editor, window, cx| {
18008 editor.context_menu_next(&ContextMenuNext, window, cx);
18009 });
18010 cx.run_until_parked();
18011 cx.update_editor(|editor, window, cx| {
18012 editor
18013 .compose_completion(&ComposeCompletion::default(), window, cx)
18014 .expect("No task returned")
18015 })
18016 .await
18017 .expect("Completion failed");
18018 cx.run_until_parked();
18019
18020 cx.update_editor(|editor, _, cx| {
18021 assert_eq!(
18022 resolve_requests_1.load(atomic::Ordering::Acquire),
18023 1,
18024 "Should always resolve once despite multiple selections"
18025 );
18026 assert_eq!(
18027 resolve_requests_2.load(atomic::Ordering::Acquire),
18028 1,
18029 "Should always resolve once after multiple selections and applying the completion"
18030 );
18031 assert_eq!(
18032 editor.text(cx),
18033 "fn main() { let a = ??.other; }",
18034 "Should use resolved data when applying the completion"
18035 );
18036 });
18037}
18038
18039#[gpui::test]
18040async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
18041 init_test(cx, |_| {});
18042
18043 let item_0 = lsp::CompletionItem {
18044 label: "abs".into(),
18045 insert_text: Some("abs".into()),
18046 data: Some(json!({ "very": "special"})),
18047 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
18048 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
18049 lsp::InsertReplaceEdit {
18050 new_text: "abs".to_string(),
18051 insert: lsp::Range::default(),
18052 replace: lsp::Range::default(),
18053 },
18054 )),
18055 ..lsp::CompletionItem::default()
18056 };
18057 let items = iter::once(item_0.clone())
18058 .chain((11..51).map(|i| lsp::CompletionItem {
18059 label: format!("item_{}", i),
18060 insert_text: Some(format!("item_{}", i)),
18061 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
18062 ..lsp::CompletionItem::default()
18063 }))
18064 .collect::<Vec<_>>();
18065
18066 let default_commit_characters = vec!["?".to_string()];
18067 let default_data = json!({ "default": "data"});
18068 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
18069 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
18070 let default_edit_range = lsp::Range {
18071 start: lsp::Position {
18072 line: 0,
18073 character: 5,
18074 },
18075 end: lsp::Position {
18076 line: 0,
18077 character: 5,
18078 },
18079 };
18080
18081 let mut cx = EditorLspTestContext::new_rust(
18082 lsp::ServerCapabilities {
18083 completion_provider: Some(lsp::CompletionOptions {
18084 trigger_characters: Some(vec![".".to_string()]),
18085 resolve_provider: Some(true),
18086 ..Default::default()
18087 }),
18088 ..Default::default()
18089 },
18090 cx,
18091 )
18092 .await;
18093
18094 cx.set_state("fn main() { let a = 2ˇ; }");
18095 cx.simulate_keystroke(".");
18096
18097 let completion_data = default_data.clone();
18098 let completion_characters = default_commit_characters.clone();
18099 let completion_items = items.clone();
18100 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18101 let default_data = completion_data.clone();
18102 let default_commit_characters = completion_characters.clone();
18103 let items = completion_items.clone();
18104 async move {
18105 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
18106 items,
18107 item_defaults: Some(lsp::CompletionListItemDefaults {
18108 data: Some(default_data.clone()),
18109 commit_characters: Some(default_commit_characters.clone()),
18110 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
18111 default_edit_range,
18112 )),
18113 insert_text_format: Some(default_insert_text_format),
18114 insert_text_mode: Some(default_insert_text_mode),
18115 }),
18116 ..lsp::CompletionList::default()
18117 })))
18118 }
18119 })
18120 .next()
18121 .await;
18122
18123 let resolved_items = Arc::new(Mutex::new(Vec::new()));
18124 cx.lsp
18125 .server
18126 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
18127 let closure_resolved_items = resolved_items.clone();
18128 move |item_to_resolve, _| {
18129 let closure_resolved_items = closure_resolved_items.clone();
18130 async move {
18131 closure_resolved_items.lock().push(item_to_resolve.clone());
18132 Ok(item_to_resolve)
18133 }
18134 }
18135 })
18136 .detach();
18137
18138 cx.condition(|editor, _| editor.context_menu_visible())
18139 .await;
18140 cx.run_until_parked();
18141 cx.update_editor(|editor, _, _| {
18142 let menu = editor.context_menu.borrow_mut();
18143 match menu.as_ref().expect("should have the completions menu") {
18144 CodeContextMenu::Completions(completions_menu) => {
18145 assert_eq!(
18146 completions_menu
18147 .entries
18148 .borrow()
18149 .iter()
18150 .map(|mat| mat.string.clone())
18151 .collect::<Vec<String>>(),
18152 items
18153 .iter()
18154 .map(|completion| completion.label.clone())
18155 .collect::<Vec<String>>()
18156 );
18157 }
18158 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
18159 }
18160 });
18161 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
18162 // with 4 from the end.
18163 assert_eq!(
18164 *resolved_items.lock(),
18165 [&items[0..16], &items[items.len() - 4..items.len()]]
18166 .concat()
18167 .iter()
18168 .cloned()
18169 .map(|mut item| {
18170 if item.data.is_none() {
18171 item.data = Some(default_data.clone());
18172 }
18173 item
18174 })
18175 .collect::<Vec<lsp::CompletionItem>>(),
18176 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
18177 );
18178 resolved_items.lock().clear();
18179
18180 cx.update_editor(|editor, window, cx| {
18181 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
18182 });
18183 cx.run_until_parked();
18184 // Completions that have already been resolved are skipped.
18185 assert_eq!(
18186 *resolved_items.lock(),
18187 items[items.len() - 17..items.len() - 4]
18188 .iter()
18189 .cloned()
18190 .map(|mut item| {
18191 if item.data.is_none() {
18192 item.data = Some(default_data.clone());
18193 }
18194 item
18195 })
18196 .collect::<Vec<lsp::CompletionItem>>()
18197 );
18198 resolved_items.lock().clear();
18199}
18200
18201#[gpui::test]
18202async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
18203 init_test(cx, |_| {});
18204
18205 let mut cx = EditorLspTestContext::new(
18206 Language::new(
18207 LanguageConfig {
18208 matcher: LanguageMatcher {
18209 path_suffixes: vec!["jsx".into()],
18210 ..Default::default()
18211 },
18212 overrides: [(
18213 "element".into(),
18214 LanguageConfigOverride {
18215 completion_query_characters: Override::Set(['-'].into_iter().collect()),
18216 ..Default::default()
18217 },
18218 )]
18219 .into_iter()
18220 .collect(),
18221 ..Default::default()
18222 },
18223 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
18224 )
18225 .with_override_query("(jsx_self_closing_element) @element")
18226 .unwrap(),
18227 lsp::ServerCapabilities {
18228 completion_provider: Some(lsp::CompletionOptions {
18229 trigger_characters: Some(vec![":".to_string()]),
18230 ..Default::default()
18231 }),
18232 ..Default::default()
18233 },
18234 cx,
18235 )
18236 .await;
18237
18238 cx.lsp
18239 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
18240 Ok(Some(lsp::CompletionResponse::Array(vec![
18241 lsp::CompletionItem {
18242 label: "bg-blue".into(),
18243 ..Default::default()
18244 },
18245 lsp::CompletionItem {
18246 label: "bg-red".into(),
18247 ..Default::default()
18248 },
18249 lsp::CompletionItem {
18250 label: "bg-yellow".into(),
18251 ..Default::default()
18252 },
18253 ])))
18254 });
18255
18256 cx.set_state(r#"<p class="bgˇ" />"#);
18257
18258 // Trigger completion when typing a dash, because the dash is an extra
18259 // word character in the 'element' scope, which contains the cursor.
18260 cx.simulate_keystroke("-");
18261 cx.executor().run_until_parked();
18262 cx.update_editor(|editor, _, _| {
18263 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18264 {
18265 assert_eq!(
18266 completion_menu_entries(menu),
18267 &["bg-blue", "bg-red", "bg-yellow"]
18268 );
18269 } else {
18270 panic!("expected completion menu to be open");
18271 }
18272 });
18273
18274 cx.simulate_keystroke("l");
18275 cx.executor().run_until_parked();
18276 cx.update_editor(|editor, _, _| {
18277 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18278 {
18279 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
18280 } else {
18281 panic!("expected completion menu to be open");
18282 }
18283 });
18284
18285 // When filtering completions, consider the character after the '-' to
18286 // be the start of a subword.
18287 cx.set_state(r#"<p class="yelˇ" />"#);
18288 cx.simulate_keystroke("l");
18289 cx.executor().run_until_parked();
18290 cx.update_editor(|editor, _, _| {
18291 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18292 {
18293 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
18294 } else {
18295 panic!("expected completion menu to be open");
18296 }
18297 });
18298}
18299
18300fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
18301 let entries = menu.entries.borrow();
18302 entries.iter().map(|mat| mat.string.clone()).collect()
18303}
18304
18305#[gpui::test]
18306async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
18307 init_test(cx, |settings| {
18308 settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
18309 });
18310
18311 let fs = FakeFs::new(cx.executor());
18312 fs.insert_file(path!("/file.ts"), Default::default()).await;
18313
18314 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
18315 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18316
18317 language_registry.add(Arc::new(Language::new(
18318 LanguageConfig {
18319 name: "TypeScript".into(),
18320 matcher: LanguageMatcher {
18321 path_suffixes: vec!["ts".to_string()],
18322 ..Default::default()
18323 },
18324 ..Default::default()
18325 },
18326 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
18327 )));
18328 update_test_language_settings(cx, |settings| {
18329 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
18330 });
18331
18332 let test_plugin = "test_plugin";
18333 let _ = language_registry.register_fake_lsp(
18334 "TypeScript",
18335 FakeLspAdapter {
18336 prettier_plugins: vec![test_plugin],
18337 ..Default::default()
18338 },
18339 );
18340
18341 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
18342 let buffer = project
18343 .update(cx, |project, cx| {
18344 project.open_local_buffer(path!("/file.ts"), cx)
18345 })
18346 .await
18347 .unwrap();
18348
18349 let buffer_text = "one\ntwo\nthree\n";
18350 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
18351 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
18352 editor.update_in(cx, |editor, window, cx| {
18353 editor.set_text(buffer_text, window, cx)
18354 });
18355
18356 editor
18357 .update_in(cx, |editor, window, cx| {
18358 editor.perform_format(
18359 project.clone(),
18360 FormatTrigger::Manual,
18361 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18362 window,
18363 cx,
18364 )
18365 })
18366 .unwrap()
18367 .await;
18368 assert_eq!(
18369 editor.update(cx, |editor, cx| editor.text(cx)),
18370 buffer_text.to_string() + prettier_format_suffix,
18371 "Test prettier formatting was not applied to the original buffer text",
18372 );
18373
18374 update_test_language_settings(cx, |settings| {
18375 settings.defaults.formatter = Some(FormatterList::default())
18376 });
18377 let format = editor.update_in(cx, |editor, window, cx| {
18378 editor.perform_format(
18379 project.clone(),
18380 FormatTrigger::Manual,
18381 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18382 window,
18383 cx,
18384 )
18385 });
18386 format.await.unwrap();
18387 assert_eq!(
18388 editor.update(cx, |editor, cx| editor.text(cx)),
18389 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
18390 "Autoformatting (via test prettier) was not applied to the original buffer text",
18391 );
18392}
18393
18394#[gpui::test]
18395async fn test_addition_reverts(cx: &mut TestAppContext) {
18396 init_test(cx, |_| {});
18397 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18398 let base_text = indoc! {r#"
18399 struct Row;
18400 struct Row1;
18401 struct Row2;
18402
18403 struct Row4;
18404 struct Row5;
18405 struct Row6;
18406
18407 struct Row8;
18408 struct Row9;
18409 struct Row10;"#};
18410
18411 // When addition hunks are not adjacent to carets, no hunk revert is performed
18412 assert_hunk_revert(
18413 indoc! {r#"struct Row;
18414 struct Row1;
18415 struct Row1.1;
18416 struct Row1.2;
18417 struct Row2;ˇ
18418
18419 struct Row4;
18420 struct Row5;
18421 struct Row6;
18422
18423 struct Row8;
18424 ˇstruct Row9;
18425 struct Row9.1;
18426 struct Row9.2;
18427 struct Row9.3;
18428 struct Row10;"#},
18429 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18430 indoc! {r#"struct Row;
18431 struct Row1;
18432 struct Row1.1;
18433 struct Row1.2;
18434 struct Row2;ˇ
18435
18436 struct Row4;
18437 struct Row5;
18438 struct Row6;
18439
18440 struct Row8;
18441 ˇstruct Row9;
18442 struct Row9.1;
18443 struct Row9.2;
18444 struct Row9.3;
18445 struct Row10;"#},
18446 base_text,
18447 &mut cx,
18448 );
18449 // Same for selections
18450 assert_hunk_revert(
18451 indoc! {r#"struct Row;
18452 struct Row1;
18453 struct Row2;
18454 struct Row2.1;
18455 struct Row2.2;
18456 «ˇ
18457 struct Row4;
18458 struct» Row5;
18459 «struct Row6;
18460 ˇ»
18461 struct Row9.1;
18462 struct Row9.2;
18463 struct Row9.3;
18464 struct Row8;
18465 struct Row9;
18466 struct Row10;"#},
18467 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18468 indoc! {r#"struct Row;
18469 struct Row1;
18470 struct Row2;
18471 struct Row2.1;
18472 struct Row2.2;
18473 «ˇ
18474 struct Row4;
18475 struct» Row5;
18476 «struct Row6;
18477 ˇ»
18478 struct Row9.1;
18479 struct Row9.2;
18480 struct Row9.3;
18481 struct Row8;
18482 struct Row9;
18483 struct Row10;"#},
18484 base_text,
18485 &mut cx,
18486 );
18487
18488 // When carets and selections intersect the addition hunks, those are reverted.
18489 // Adjacent carets got merged.
18490 assert_hunk_revert(
18491 indoc! {r#"struct Row;
18492 ˇ// something on the top
18493 struct Row1;
18494 struct Row2;
18495 struct Roˇw3.1;
18496 struct Row2.2;
18497 struct Row2.3;ˇ
18498
18499 struct Row4;
18500 struct ˇRow5.1;
18501 struct Row5.2;
18502 struct «Rowˇ»5.3;
18503 struct Row5;
18504 struct Row6;
18505 ˇ
18506 struct Row9.1;
18507 struct «Rowˇ»9.2;
18508 struct «ˇRow»9.3;
18509 struct Row8;
18510 struct Row9;
18511 «ˇ// something on bottom»
18512 struct Row10;"#},
18513 vec![
18514 DiffHunkStatusKind::Added,
18515 DiffHunkStatusKind::Added,
18516 DiffHunkStatusKind::Added,
18517 DiffHunkStatusKind::Added,
18518 DiffHunkStatusKind::Added,
18519 ],
18520 indoc! {r#"struct Row;
18521 ˇstruct Row1;
18522 struct Row2;
18523 ˇ
18524 struct Row4;
18525 ˇstruct Row5;
18526 struct Row6;
18527 ˇ
18528 ˇstruct Row8;
18529 struct Row9;
18530 ˇstruct Row10;"#},
18531 base_text,
18532 &mut cx,
18533 );
18534}
18535
18536#[gpui::test]
18537async fn test_modification_reverts(cx: &mut TestAppContext) {
18538 init_test(cx, |_| {});
18539 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18540 let base_text = indoc! {r#"
18541 struct Row;
18542 struct Row1;
18543 struct Row2;
18544
18545 struct Row4;
18546 struct Row5;
18547 struct Row6;
18548
18549 struct Row8;
18550 struct Row9;
18551 struct Row10;"#};
18552
18553 // Modification hunks behave the same as the addition ones.
18554 assert_hunk_revert(
18555 indoc! {r#"struct Row;
18556 struct Row1;
18557 struct Row33;
18558 ˇ
18559 struct Row4;
18560 struct Row5;
18561 struct Row6;
18562 ˇ
18563 struct Row99;
18564 struct Row9;
18565 struct Row10;"#},
18566 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18567 indoc! {r#"struct Row;
18568 struct Row1;
18569 struct Row33;
18570 ˇ
18571 struct Row4;
18572 struct Row5;
18573 struct Row6;
18574 ˇ
18575 struct Row99;
18576 struct Row9;
18577 struct Row10;"#},
18578 base_text,
18579 &mut cx,
18580 );
18581 assert_hunk_revert(
18582 indoc! {r#"struct Row;
18583 struct Row1;
18584 struct Row33;
18585 «ˇ
18586 struct Row4;
18587 struct» Row5;
18588 «struct Row6;
18589 ˇ»
18590 struct Row99;
18591 struct Row9;
18592 struct Row10;"#},
18593 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18594 indoc! {r#"struct Row;
18595 struct Row1;
18596 struct Row33;
18597 «ˇ
18598 struct Row4;
18599 struct» Row5;
18600 «struct Row6;
18601 ˇ»
18602 struct Row99;
18603 struct Row9;
18604 struct Row10;"#},
18605 base_text,
18606 &mut cx,
18607 );
18608
18609 assert_hunk_revert(
18610 indoc! {r#"ˇstruct Row1.1;
18611 struct Row1;
18612 «ˇstr»uct Row22;
18613
18614 struct ˇRow44;
18615 struct Row5;
18616 struct «Rˇ»ow66;ˇ
18617
18618 «struˇ»ct Row88;
18619 struct Row9;
18620 struct Row1011;ˇ"#},
18621 vec![
18622 DiffHunkStatusKind::Modified,
18623 DiffHunkStatusKind::Modified,
18624 DiffHunkStatusKind::Modified,
18625 DiffHunkStatusKind::Modified,
18626 DiffHunkStatusKind::Modified,
18627 DiffHunkStatusKind::Modified,
18628 ],
18629 indoc! {r#"struct Row;
18630 ˇstruct Row1;
18631 struct Row2;
18632 ˇ
18633 struct Row4;
18634 ˇstruct Row5;
18635 struct Row6;
18636 ˇ
18637 struct Row8;
18638 ˇstruct Row9;
18639 struct Row10;ˇ"#},
18640 base_text,
18641 &mut cx,
18642 );
18643}
18644
18645#[gpui::test]
18646async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
18647 init_test(cx, |_| {});
18648 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18649 let base_text = indoc! {r#"
18650 one
18651
18652 two
18653 three
18654 "#};
18655
18656 cx.set_head_text(base_text);
18657 cx.set_state("\nˇ\n");
18658 cx.executor().run_until_parked();
18659 cx.update_editor(|editor, _window, cx| {
18660 editor.expand_selected_diff_hunks(cx);
18661 });
18662 cx.executor().run_until_parked();
18663 cx.update_editor(|editor, window, cx| {
18664 editor.backspace(&Default::default(), window, cx);
18665 });
18666 cx.run_until_parked();
18667 cx.assert_state_with_diff(
18668 indoc! {r#"
18669
18670 - two
18671 - threeˇ
18672 +
18673 "#}
18674 .to_string(),
18675 );
18676}
18677
18678#[gpui::test]
18679async fn test_deletion_reverts(cx: &mut TestAppContext) {
18680 init_test(cx, |_| {});
18681 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18682 let base_text = indoc! {r#"struct Row;
18683struct Row1;
18684struct Row2;
18685
18686struct Row4;
18687struct Row5;
18688struct Row6;
18689
18690struct Row8;
18691struct Row9;
18692struct Row10;"#};
18693
18694 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
18695 assert_hunk_revert(
18696 indoc! {r#"struct Row;
18697 struct Row2;
18698
18699 ˇstruct Row4;
18700 struct Row5;
18701 struct Row6;
18702 ˇ
18703 struct Row8;
18704 struct Row10;"#},
18705 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18706 indoc! {r#"struct Row;
18707 struct Row2;
18708
18709 ˇstruct Row4;
18710 struct Row5;
18711 struct Row6;
18712 ˇ
18713 struct Row8;
18714 struct Row10;"#},
18715 base_text,
18716 &mut cx,
18717 );
18718 assert_hunk_revert(
18719 indoc! {r#"struct Row;
18720 struct Row2;
18721
18722 «ˇstruct Row4;
18723 struct» Row5;
18724 «struct Row6;
18725 ˇ»
18726 struct Row8;
18727 struct Row10;"#},
18728 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18729 indoc! {r#"struct Row;
18730 struct Row2;
18731
18732 «ˇstruct Row4;
18733 struct» Row5;
18734 «struct Row6;
18735 ˇ»
18736 struct Row8;
18737 struct Row10;"#},
18738 base_text,
18739 &mut cx,
18740 );
18741
18742 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
18743 assert_hunk_revert(
18744 indoc! {r#"struct Row;
18745 ˇstruct Row2;
18746
18747 struct Row4;
18748 struct Row5;
18749 struct Row6;
18750
18751 struct Row8;ˇ
18752 struct Row10;"#},
18753 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18754 indoc! {r#"struct Row;
18755 struct Row1;
18756 ˇstruct Row2;
18757
18758 struct Row4;
18759 struct Row5;
18760 struct Row6;
18761
18762 struct Row8;ˇ
18763 struct Row9;
18764 struct Row10;"#},
18765 base_text,
18766 &mut cx,
18767 );
18768 assert_hunk_revert(
18769 indoc! {r#"struct Row;
18770 struct Row2«ˇ;
18771 struct Row4;
18772 struct» Row5;
18773 «struct Row6;
18774
18775 struct Row8;ˇ»
18776 struct Row10;"#},
18777 vec![
18778 DiffHunkStatusKind::Deleted,
18779 DiffHunkStatusKind::Deleted,
18780 DiffHunkStatusKind::Deleted,
18781 ],
18782 indoc! {r#"struct Row;
18783 struct Row1;
18784 struct Row2«ˇ;
18785
18786 struct Row4;
18787 struct» Row5;
18788 «struct Row6;
18789
18790 struct Row8;ˇ»
18791 struct Row9;
18792 struct Row10;"#},
18793 base_text,
18794 &mut cx,
18795 );
18796}
18797
18798#[gpui::test]
18799async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
18800 init_test(cx, |_| {});
18801
18802 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
18803 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
18804 let base_text_3 =
18805 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
18806
18807 let text_1 = edit_first_char_of_every_line(base_text_1);
18808 let text_2 = edit_first_char_of_every_line(base_text_2);
18809 let text_3 = edit_first_char_of_every_line(base_text_3);
18810
18811 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
18812 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
18813 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
18814
18815 let multibuffer = cx.new(|cx| {
18816 let mut multibuffer = MultiBuffer::new(ReadWrite);
18817 multibuffer.push_excerpts(
18818 buffer_1.clone(),
18819 [
18820 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18821 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18822 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18823 ],
18824 cx,
18825 );
18826 multibuffer.push_excerpts(
18827 buffer_2.clone(),
18828 [
18829 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18830 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18831 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18832 ],
18833 cx,
18834 );
18835 multibuffer.push_excerpts(
18836 buffer_3.clone(),
18837 [
18838 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18839 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18840 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18841 ],
18842 cx,
18843 );
18844 multibuffer
18845 });
18846
18847 let fs = FakeFs::new(cx.executor());
18848 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
18849 let (editor, cx) = cx
18850 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
18851 editor.update_in(cx, |editor, _window, cx| {
18852 for (buffer, diff_base) in [
18853 (buffer_1.clone(), base_text_1),
18854 (buffer_2.clone(), base_text_2),
18855 (buffer_3.clone(), base_text_3),
18856 ] {
18857 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
18858 editor
18859 .buffer
18860 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
18861 }
18862 });
18863 cx.executor().run_until_parked();
18864
18865 editor.update_in(cx, |editor, window, cx| {
18866 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}");
18867 editor.select_all(&SelectAll, window, cx);
18868 editor.git_restore(&Default::default(), window, cx);
18869 });
18870 cx.executor().run_until_parked();
18871
18872 // When all ranges are selected, all buffer hunks are reverted.
18873 editor.update(cx, |editor, cx| {
18874 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");
18875 });
18876 buffer_1.update(cx, |buffer, _| {
18877 assert_eq!(buffer.text(), base_text_1);
18878 });
18879 buffer_2.update(cx, |buffer, _| {
18880 assert_eq!(buffer.text(), base_text_2);
18881 });
18882 buffer_3.update(cx, |buffer, _| {
18883 assert_eq!(buffer.text(), base_text_3);
18884 });
18885
18886 editor.update_in(cx, |editor, window, cx| {
18887 editor.undo(&Default::default(), window, cx);
18888 });
18889
18890 editor.update_in(cx, |editor, window, cx| {
18891 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18892 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
18893 });
18894 editor.git_restore(&Default::default(), window, cx);
18895 });
18896
18897 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
18898 // but not affect buffer_2 and its related excerpts.
18899 editor.update(cx, |editor, cx| {
18900 assert_eq!(
18901 editor.text(cx),
18902 "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}"
18903 );
18904 });
18905 buffer_1.update(cx, |buffer, _| {
18906 assert_eq!(buffer.text(), base_text_1);
18907 });
18908 buffer_2.update(cx, |buffer, _| {
18909 assert_eq!(
18910 buffer.text(),
18911 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
18912 );
18913 });
18914 buffer_3.update(cx, |buffer, _| {
18915 assert_eq!(
18916 buffer.text(),
18917 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
18918 );
18919 });
18920
18921 fn edit_first_char_of_every_line(text: &str) -> String {
18922 text.split('\n')
18923 .map(|line| format!("X{}", &line[1..]))
18924 .collect::<Vec<_>>()
18925 .join("\n")
18926 }
18927}
18928
18929#[gpui::test]
18930async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
18931 init_test(cx, |_| {});
18932
18933 let cols = 4;
18934 let rows = 10;
18935 let sample_text_1 = sample_text(rows, cols, 'a');
18936 assert_eq!(
18937 sample_text_1,
18938 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
18939 );
18940 let sample_text_2 = sample_text(rows, cols, 'l');
18941 assert_eq!(
18942 sample_text_2,
18943 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
18944 );
18945 let sample_text_3 = sample_text(rows, cols, 'v');
18946 assert_eq!(
18947 sample_text_3,
18948 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
18949 );
18950
18951 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
18952 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
18953 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
18954
18955 let multi_buffer = cx.new(|cx| {
18956 let mut multibuffer = MultiBuffer::new(ReadWrite);
18957 multibuffer.push_excerpts(
18958 buffer_1.clone(),
18959 [
18960 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18961 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18962 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18963 ],
18964 cx,
18965 );
18966 multibuffer.push_excerpts(
18967 buffer_2.clone(),
18968 [
18969 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18970 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18971 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18972 ],
18973 cx,
18974 );
18975 multibuffer.push_excerpts(
18976 buffer_3.clone(),
18977 [
18978 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18979 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18980 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18981 ],
18982 cx,
18983 );
18984 multibuffer
18985 });
18986
18987 let fs = FakeFs::new(cx.executor());
18988 fs.insert_tree(
18989 "/a",
18990 json!({
18991 "main.rs": sample_text_1,
18992 "other.rs": sample_text_2,
18993 "lib.rs": sample_text_3,
18994 }),
18995 )
18996 .await;
18997 let project = Project::test(fs, ["/a".as_ref()], cx).await;
18998 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18999 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19000 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19001 Editor::new(
19002 EditorMode::full(),
19003 multi_buffer,
19004 Some(project.clone()),
19005 window,
19006 cx,
19007 )
19008 });
19009 let multibuffer_item_id = workspace
19010 .update(cx, |workspace, window, cx| {
19011 assert!(
19012 workspace.active_item(cx).is_none(),
19013 "active item should be None before the first item is added"
19014 );
19015 workspace.add_item_to_active_pane(
19016 Box::new(multi_buffer_editor.clone()),
19017 None,
19018 true,
19019 window,
19020 cx,
19021 );
19022 let active_item = workspace
19023 .active_item(cx)
19024 .expect("should have an active item after adding the multi buffer");
19025 assert_eq!(
19026 active_item.buffer_kind(cx),
19027 ItemBufferKind::Multibuffer,
19028 "A multi buffer was expected to active after adding"
19029 );
19030 active_item.item_id()
19031 })
19032 .unwrap();
19033 cx.executor().run_until_parked();
19034
19035 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19036 editor.change_selections(
19037 SelectionEffects::scroll(Autoscroll::Next),
19038 window,
19039 cx,
19040 |s| s.select_ranges(Some(1..2)),
19041 );
19042 editor.open_excerpts(&OpenExcerpts, window, cx);
19043 });
19044 cx.executor().run_until_parked();
19045 let first_item_id = workspace
19046 .update(cx, |workspace, window, cx| {
19047 let active_item = workspace
19048 .active_item(cx)
19049 .expect("should have an active item after navigating into the 1st buffer");
19050 let first_item_id = active_item.item_id();
19051 assert_ne!(
19052 first_item_id, multibuffer_item_id,
19053 "Should navigate into the 1st buffer and activate it"
19054 );
19055 assert_eq!(
19056 active_item.buffer_kind(cx),
19057 ItemBufferKind::Singleton,
19058 "New active item should be a singleton buffer"
19059 );
19060 assert_eq!(
19061 active_item
19062 .act_as::<Editor>(cx)
19063 .expect("should have navigated into an editor for the 1st buffer")
19064 .read(cx)
19065 .text(cx),
19066 sample_text_1
19067 );
19068
19069 workspace
19070 .go_back(workspace.active_pane().downgrade(), window, cx)
19071 .detach_and_log_err(cx);
19072
19073 first_item_id
19074 })
19075 .unwrap();
19076 cx.executor().run_until_parked();
19077 workspace
19078 .update(cx, |workspace, _, cx| {
19079 let active_item = workspace
19080 .active_item(cx)
19081 .expect("should have an active item after navigating back");
19082 assert_eq!(
19083 active_item.item_id(),
19084 multibuffer_item_id,
19085 "Should navigate back to the multi buffer"
19086 );
19087 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19088 })
19089 .unwrap();
19090
19091 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19092 editor.change_selections(
19093 SelectionEffects::scroll(Autoscroll::Next),
19094 window,
19095 cx,
19096 |s| s.select_ranges(Some(39..40)),
19097 );
19098 editor.open_excerpts(&OpenExcerpts, window, cx);
19099 });
19100 cx.executor().run_until_parked();
19101 let second_item_id = workspace
19102 .update(cx, |workspace, window, cx| {
19103 let active_item = workspace
19104 .active_item(cx)
19105 .expect("should have an active item after navigating into the 2nd buffer");
19106 let second_item_id = active_item.item_id();
19107 assert_ne!(
19108 second_item_id, multibuffer_item_id,
19109 "Should navigate away from the multibuffer"
19110 );
19111 assert_ne!(
19112 second_item_id, first_item_id,
19113 "Should navigate into the 2nd buffer and activate it"
19114 );
19115 assert_eq!(
19116 active_item.buffer_kind(cx),
19117 ItemBufferKind::Singleton,
19118 "New active item should be a singleton buffer"
19119 );
19120 assert_eq!(
19121 active_item
19122 .act_as::<Editor>(cx)
19123 .expect("should have navigated into an editor")
19124 .read(cx)
19125 .text(cx),
19126 sample_text_2
19127 );
19128
19129 workspace
19130 .go_back(workspace.active_pane().downgrade(), window, cx)
19131 .detach_and_log_err(cx);
19132
19133 second_item_id
19134 })
19135 .unwrap();
19136 cx.executor().run_until_parked();
19137 workspace
19138 .update(cx, |workspace, _, cx| {
19139 let active_item = workspace
19140 .active_item(cx)
19141 .expect("should have an active item after navigating back from the 2nd buffer");
19142 assert_eq!(
19143 active_item.item_id(),
19144 multibuffer_item_id,
19145 "Should navigate back from the 2nd buffer to the multi buffer"
19146 );
19147 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19148 })
19149 .unwrap();
19150
19151 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19152 editor.change_selections(
19153 SelectionEffects::scroll(Autoscroll::Next),
19154 window,
19155 cx,
19156 |s| s.select_ranges(Some(70..70)),
19157 );
19158 editor.open_excerpts(&OpenExcerpts, window, cx);
19159 });
19160 cx.executor().run_until_parked();
19161 workspace
19162 .update(cx, |workspace, window, cx| {
19163 let active_item = workspace
19164 .active_item(cx)
19165 .expect("should have an active item after navigating into the 3rd buffer");
19166 let third_item_id = active_item.item_id();
19167 assert_ne!(
19168 third_item_id, multibuffer_item_id,
19169 "Should navigate into the 3rd buffer and activate it"
19170 );
19171 assert_ne!(third_item_id, first_item_id);
19172 assert_ne!(third_item_id, second_item_id);
19173 assert_eq!(
19174 active_item.buffer_kind(cx),
19175 ItemBufferKind::Singleton,
19176 "New active item should be a singleton buffer"
19177 );
19178 assert_eq!(
19179 active_item
19180 .act_as::<Editor>(cx)
19181 .expect("should have navigated into an editor")
19182 .read(cx)
19183 .text(cx),
19184 sample_text_3
19185 );
19186
19187 workspace
19188 .go_back(workspace.active_pane().downgrade(), window, cx)
19189 .detach_and_log_err(cx);
19190 })
19191 .unwrap();
19192 cx.executor().run_until_parked();
19193 workspace
19194 .update(cx, |workspace, _, cx| {
19195 let active_item = workspace
19196 .active_item(cx)
19197 .expect("should have an active item after navigating back from the 3rd buffer");
19198 assert_eq!(
19199 active_item.item_id(),
19200 multibuffer_item_id,
19201 "Should navigate back from the 3rd buffer to the multi buffer"
19202 );
19203 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19204 })
19205 .unwrap();
19206}
19207
19208#[gpui::test]
19209async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19210 init_test(cx, |_| {});
19211
19212 let mut cx = EditorTestContext::new(cx).await;
19213
19214 let diff_base = r#"
19215 use some::mod;
19216
19217 const A: u32 = 42;
19218
19219 fn main() {
19220 println!("hello");
19221
19222 println!("world");
19223 }
19224 "#
19225 .unindent();
19226
19227 cx.set_state(
19228 &r#"
19229 use some::modified;
19230
19231 ˇ
19232 fn main() {
19233 println!("hello there");
19234
19235 println!("around the");
19236 println!("world");
19237 }
19238 "#
19239 .unindent(),
19240 );
19241
19242 cx.set_head_text(&diff_base);
19243 executor.run_until_parked();
19244
19245 cx.update_editor(|editor, window, cx| {
19246 editor.go_to_next_hunk(&GoToHunk, window, cx);
19247 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19248 });
19249 executor.run_until_parked();
19250 cx.assert_state_with_diff(
19251 r#"
19252 use some::modified;
19253
19254
19255 fn main() {
19256 - println!("hello");
19257 + ˇ println!("hello there");
19258
19259 println!("around the");
19260 println!("world");
19261 }
19262 "#
19263 .unindent(),
19264 );
19265
19266 cx.update_editor(|editor, window, cx| {
19267 for _ in 0..2 {
19268 editor.go_to_next_hunk(&GoToHunk, window, cx);
19269 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19270 }
19271 });
19272 executor.run_until_parked();
19273 cx.assert_state_with_diff(
19274 r#"
19275 - use some::mod;
19276 + ˇuse some::modified;
19277
19278
19279 fn main() {
19280 - println!("hello");
19281 + println!("hello there");
19282
19283 + println!("around the");
19284 println!("world");
19285 }
19286 "#
19287 .unindent(),
19288 );
19289
19290 cx.update_editor(|editor, window, cx| {
19291 editor.go_to_next_hunk(&GoToHunk, window, cx);
19292 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19293 });
19294 executor.run_until_parked();
19295 cx.assert_state_with_diff(
19296 r#"
19297 - use some::mod;
19298 + use some::modified;
19299
19300 - const A: u32 = 42;
19301 ˇ
19302 fn main() {
19303 - println!("hello");
19304 + println!("hello there");
19305
19306 + println!("around the");
19307 println!("world");
19308 }
19309 "#
19310 .unindent(),
19311 );
19312
19313 cx.update_editor(|editor, window, cx| {
19314 editor.cancel(&Cancel, window, cx);
19315 });
19316
19317 cx.assert_state_with_diff(
19318 r#"
19319 use some::modified;
19320
19321 ˇ
19322 fn main() {
19323 println!("hello there");
19324
19325 println!("around the");
19326 println!("world");
19327 }
19328 "#
19329 .unindent(),
19330 );
19331}
19332
19333#[gpui::test]
19334async fn test_diff_base_change_with_expanded_diff_hunks(
19335 executor: BackgroundExecutor,
19336 cx: &mut TestAppContext,
19337) {
19338 init_test(cx, |_| {});
19339
19340 let mut cx = EditorTestContext::new(cx).await;
19341
19342 let diff_base = r#"
19343 use some::mod1;
19344 use some::mod2;
19345
19346 const A: u32 = 42;
19347 const B: u32 = 42;
19348 const C: u32 = 42;
19349
19350 fn main() {
19351 println!("hello");
19352
19353 println!("world");
19354 }
19355 "#
19356 .unindent();
19357
19358 cx.set_state(
19359 &r#"
19360 use some::mod2;
19361
19362 const A: u32 = 42;
19363 const C: u32 = 42;
19364
19365 fn main(ˇ) {
19366 //println!("hello");
19367
19368 println!("world");
19369 //
19370 //
19371 }
19372 "#
19373 .unindent(),
19374 );
19375
19376 cx.set_head_text(&diff_base);
19377 executor.run_until_parked();
19378
19379 cx.update_editor(|editor, window, cx| {
19380 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19381 });
19382 executor.run_until_parked();
19383 cx.assert_state_with_diff(
19384 r#"
19385 - use some::mod1;
19386 use some::mod2;
19387
19388 const A: u32 = 42;
19389 - const B: u32 = 42;
19390 const C: u32 = 42;
19391
19392 fn main(ˇ) {
19393 - println!("hello");
19394 + //println!("hello");
19395
19396 println!("world");
19397 + //
19398 + //
19399 }
19400 "#
19401 .unindent(),
19402 );
19403
19404 cx.set_head_text("new diff base!");
19405 executor.run_until_parked();
19406 cx.assert_state_with_diff(
19407 r#"
19408 - new diff base!
19409 + use some::mod2;
19410 +
19411 + const A: u32 = 42;
19412 + const C: u32 = 42;
19413 +
19414 + fn main(ˇ) {
19415 + //println!("hello");
19416 +
19417 + println!("world");
19418 + //
19419 + //
19420 + }
19421 "#
19422 .unindent(),
19423 );
19424}
19425
19426#[gpui::test]
19427async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
19428 init_test(cx, |_| {});
19429
19430 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19431 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19432 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19433 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19434 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
19435 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
19436
19437 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
19438 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
19439 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
19440
19441 let multi_buffer = cx.new(|cx| {
19442 let mut multibuffer = MultiBuffer::new(ReadWrite);
19443 multibuffer.push_excerpts(
19444 buffer_1.clone(),
19445 [
19446 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19447 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19448 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19449 ],
19450 cx,
19451 );
19452 multibuffer.push_excerpts(
19453 buffer_2.clone(),
19454 [
19455 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19456 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19457 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19458 ],
19459 cx,
19460 );
19461 multibuffer.push_excerpts(
19462 buffer_3.clone(),
19463 [
19464 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19465 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19466 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19467 ],
19468 cx,
19469 );
19470 multibuffer
19471 });
19472
19473 let editor =
19474 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19475 editor
19476 .update(cx, |editor, _window, cx| {
19477 for (buffer, diff_base) in [
19478 (buffer_1.clone(), file_1_old),
19479 (buffer_2.clone(), file_2_old),
19480 (buffer_3.clone(), file_3_old),
19481 ] {
19482 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
19483 editor
19484 .buffer
19485 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
19486 }
19487 })
19488 .unwrap();
19489
19490 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19491 cx.run_until_parked();
19492
19493 cx.assert_editor_state(
19494 &"
19495 ˇaaa
19496 ccc
19497 ddd
19498
19499 ggg
19500 hhh
19501
19502
19503 lll
19504 mmm
19505 NNN
19506
19507 qqq
19508 rrr
19509
19510 uuu
19511 111
19512 222
19513 333
19514
19515 666
19516 777
19517
19518 000
19519 !!!"
19520 .unindent(),
19521 );
19522
19523 cx.update_editor(|editor, window, cx| {
19524 editor.select_all(&SelectAll, window, cx);
19525 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19526 });
19527 cx.executor().run_until_parked();
19528
19529 cx.assert_state_with_diff(
19530 "
19531 «aaa
19532 - bbb
19533 ccc
19534 ddd
19535
19536 ggg
19537 hhh
19538
19539
19540 lll
19541 mmm
19542 - nnn
19543 + NNN
19544
19545 qqq
19546 rrr
19547
19548 uuu
19549 111
19550 222
19551 333
19552
19553 + 666
19554 777
19555
19556 000
19557 !!!ˇ»"
19558 .unindent(),
19559 );
19560}
19561
19562#[gpui::test]
19563async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
19564 init_test(cx, |_| {});
19565
19566 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
19567 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
19568
19569 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
19570 let multi_buffer = cx.new(|cx| {
19571 let mut multibuffer = MultiBuffer::new(ReadWrite);
19572 multibuffer.push_excerpts(
19573 buffer.clone(),
19574 [
19575 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
19576 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
19577 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
19578 ],
19579 cx,
19580 );
19581 multibuffer
19582 });
19583
19584 let editor =
19585 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19586 editor
19587 .update(cx, |editor, _window, cx| {
19588 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
19589 editor
19590 .buffer
19591 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
19592 })
19593 .unwrap();
19594
19595 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19596 cx.run_until_parked();
19597
19598 cx.update_editor(|editor, window, cx| {
19599 editor.expand_all_diff_hunks(&Default::default(), window, cx)
19600 });
19601 cx.executor().run_until_parked();
19602
19603 // When the start of a hunk coincides with the start of its excerpt,
19604 // the hunk is expanded. When the start of a hunk is earlier than
19605 // the start of its excerpt, the hunk is not expanded.
19606 cx.assert_state_with_diff(
19607 "
19608 ˇaaa
19609 - bbb
19610 + BBB
19611
19612 - ddd
19613 - eee
19614 + DDD
19615 + EEE
19616 fff
19617
19618 iii
19619 "
19620 .unindent(),
19621 );
19622}
19623
19624#[gpui::test]
19625async fn test_edits_around_expanded_insertion_hunks(
19626 executor: BackgroundExecutor,
19627 cx: &mut TestAppContext,
19628) {
19629 init_test(cx, |_| {});
19630
19631 let mut cx = EditorTestContext::new(cx).await;
19632
19633 let diff_base = r#"
19634 use some::mod1;
19635 use some::mod2;
19636
19637 const A: u32 = 42;
19638
19639 fn main() {
19640 println!("hello");
19641
19642 println!("world");
19643 }
19644 "#
19645 .unindent();
19646 executor.run_until_parked();
19647 cx.set_state(
19648 &r#"
19649 use some::mod1;
19650 use some::mod2;
19651
19652 const A: u32 = 42;
19653 const B: u32 = 42;
19654 const C: u32 = 42;
19655 ˇ
19656
19657 fn main() {
19658 println!("hello");
19659
19660 println!("world");
19661 }
19662 "#
19663 .unindent(),
19664 );
19665
19666 cx.set_head_text(&diff_base);
19667 executor.run_until_parked();
19668
19669 cx.update_editor(|editor, window, cx| {
19670 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19671 });
19672 executor.run_until_parked();
19673
19674 cx.assert_state_with_diff(
19675 r#"
19676 use some::mod1;
19677 use some::mod2;
19678
19679 const A: u32 = 42;
19680 + const B: u32 = 42;
19681 + const C: u32 = 42;
19682 + ˇ
19683
19684 fn main() {
19685 println!("hello");
19686
19687 println!("world");
19688 }
19689 "#
19690 .unindent(),
19691 );
19692
19693 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
19694 executor.run_until_parked();
19695
19696 cx.assert_state_with_diff(
19697 r#"
19698 use some::mod1;
19699 use some::mod2;
19700
19701 const A: u32 = 42;
19702 + const B: u32 = 42;
19703 + const C: u32 = 42;
19704 + const D: u32 = 42;
19705 + ˇ
19706
19707 fn main() {
19708 println!("hello");
19709
19710 println!("world");
19711 }
19712 "#
19713 .unindent(),
19714 );
19715
19716 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
19717 executor.run_until_parked();
19718
19719 cx.assert_state_with_diff(
19720 r#"
19721 use some::mod1;
19722 use some::mod2;
19723
19724 const A: u32 = 42;
19725 + const B: u32 = 42;
19726 + const C: u32 = 42;
19727 + const D: u32 = 42;
19728 + const E: u32 = 42;
19729 + ˇ
19730
19731 fn main() {
19732 println!("hello");
19733
19734 println!("world");
19735 }
19736 "#
19737 .unindent(),
19738 );
19739
19740 cx.update_editor(|editor, window, cx| {
19741 editor.delete_line(&DeleteLine, window, cx);
19742 });
19743 executor.run_until_parked();
19744
19745 cx.assert_state_with_diff(
19746 r#"
19747 use some::mod1;
19748 use some::mod2;
19749
19750 const A: u32 = 42;
19751 + const B: u32 = 42;
19752 + const C: u32 = 42;
19753 + const D: u32 = 42;
19754 + const E: u32 = 42;
19755 ˇ
19756 fn main() {
19757 println!("hello");
19758
19759 println!("world");
19760 }
19761 "#
19762 .unindent(),
19763 );
19764
19765 cx.update_editor(|editor, window, cx| {
19766 editor.move_up(&MoveUp, window, cx);
19767 editor.delete_line(&DeleteLine, window, cx);
19768 editor.move_up(&MoveUp, window, cx);
19769 editor.delete_line(&DeleteLine, window, cx);
19770 editor.move_up(&MoveUp, window, cx);
19771 editor.delete_line(&DeleteLine, window, cx);
19772 });
19773 executor.run_until_parked();
19774 cx.assert_state_with_diff(
19775 r#"
19776 use some::mod1;
19777 use some::mod2;
19778
19779 const A: u32 = 42;
19780 + const B: u32 = 42;
19781 ˇ
19782 fn main() {
19783 println!("hello");
19784
19785 println!("world");
19786 }
19787 "#
19788 .unindent(),
19789 );
19790
19791 cx.update_editor(|editor, window, cx| {
19792 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
19793 editor.delete_line(&DeleteLine, window, cx);
19794 });
19795 executor.run_until_parked();
19796 cx.assert_state_with_diff(
19797 r#"
19798 ˇ
19799 fn main() {
19800 println!("hello");
19801
19802 println!("world");
19803 }
19804 "#
19805 .unindent(),
19806 );
19807}
19808
19809#[gpui::test]
19810async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
19811 init_test(cx, |_| {});
19812
19813 let mut cx = EditorTestContext::new(cx).await;
19814 cx.set_head_text(indoc! { "
19815 one
19816 two
19817 three
19818 four
19819 five
19820 "
19821 });
19822 cx.set_state(indoc! { "
19823 one
19824 ˇthree
19825 five
19826 "});
19827 cx.run_until_parked();
19828 cx.update_editor(|editor, window, cx| {
19829 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19830 });
19831 cx.assert_state_with_diff(
19832 indoc! { "
19833 one
19834 - two
19835 ˇthree
19836 - four
19837 five
19838 "}
19839 .to_string(),
19840 );
19841 cx.update_editor(|editor, window, cx| {
19842 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19843 });
19844
19845 cx.assert_state_with_diff(
19846 indoc! { "
19847 one
19848 ˇthree
19849 five
19850 "}
19851 .to_string(),
19852 );
19853
19854 cx.set_state(indoc! { "
19855 one
19856 ˇTWO
19857 three
19858 four
19859 five
19860 "});
19861 cx.run_until_parked();
19862 cx.update_editor(|editor, window, cx| {
19863 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19864 });
19865
19866 cx.assert_state_with_diff(
19867 indoc! { "
19868 one
19869 - two
19870 + ˇTWO
19871 three
19872 four
19873 five
19874 "}
19875 .to_string(),
19876 );
19877 cx.update_editor(|editor, window, cx| {
19878 editor.move_up(&Default::default(), window, cx);
19879 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19880 });
19881 cx.assert_state_with_diff(
19882 indoc! { "
19883 one
19884 ˇTWO
19885 three
19886 four
19887 five
19888 "}
19889 .to_string(),
19890 );
19891}
19892
19893#[gpui::test]
19894async fn test_edits_around_expanded_deletion_hunks(
19895 executor: BackgroundExecutor,
19896 cx: &mut TestAppContext,
19897) {
19898 init_test(cx, |_| {});
19899
19900 let mut cx = EditorTestContext::new(cx).await;
19901
19902 let diff_base = r#"
19903 use some::mod1;
19904 use some::mod2;
19905
19906 const A: u32 = 42;
19907 const B: u32 = 42;
19908 const C: u32 = 42;
19909
19910
19911 fn main() {
19912 println!("hello");
19913
19914 println!("world");
19915 }
19916 "#
19917 .unindent();
19918 executor.run_until_parked();
19919 cx.set_state(
19920 &r#"
19921 use some::mod1;
19922 use some::mod2;
19923
19924 ˇconst B: u32 = 42;
19925 const C: u32 = 42;
19926
19927
19928 fn main() {
19929 println!("hello");
19930
19931 println!("world");
19932 }
19933 "#
19934 .unindent(),
19935 );
19936
19937 cx.set_head_text(&diff_base);
19938 executor.run_until_parked();
19939
19940 cx.update_editor(|editor, window, cx| {
19941 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19942 });
19943 executor.run_until_parked();
19944
19945 cx.assert_state_with_diff(
19946 r#"
19947 use some::mod1;
19948 use some::mod2;
19949
19950 - const A: u32 = 42;
19951 ˇconst B: u32 = 42;
19952 const C: u32 = 42;
19953
19954
19955 fn main() {
19956 println!("hello");
19957
19958 println!("world");
19959 }
19960 "#
19961 .unindent(),
19962 );
19963
19964 cx.update_editor(|editor, window, cx| {
19965 editor.delete_line(&DeleteLine, window, cx);
19966 });
19967 executor.run_until_parked();
19968 cx.assert_state_with_diff(
19969 r#"
19970 use some::mod1;
19971 use some::mod2;
19972
19973 - const A: u32 = 42;
19974 - const B: u32 = 42;
19975 ˇconst C: u32 = 42;
19976
19977
19978 fn main() {
19979 println!("hello");
19980
19981 println!("world");
19982 }
19983 "#
19984 .unindent(),
19985 );
19986
19987 cx.update_editor(|editor, window, cx| {
19988 editor.delete_line(&DeleteLine, window, cx);
19989 });
19990 executor.run_until_parked();
19991 cx.assert_state_with_diff(
19992 r#"
19993 use some::mod1;
19994 use some::mod2;
19995
19996 - const A: u32 = 42;
19997 - const B: u32 = 42;
19998 - const C: u32 = 42;
19999 ˇ
20000
20001 fn main() {
20002 println!("hello");
20003
20004 println!("world");
20005 }
20006 "#
20007 .unindent(),
20008 );
20009
20010 cx.update_editor(|editor, window, cx| {
20011 editor.handle_input("replacement", window, cx);
20012 });
20013 executor.run_until_parked();
20014 cx.assert_state_with_diff(
20015 r#"
20016 use some::mod1;
20017 use some::mod2;
20018
20019 - const A: u32 = 42;
20020 - const B: u32 = 42;
20021 - const C: u32 = 42;
20022 -
20023 + replacementˇ
20024
20025 fn main() {
20026 println!("hello");
20027
20028 println!("world");
20029 }
20030 "#
20031 .unindent(),
20032 );
20033}
20034
20035#[gpui::test]
20036async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20037 init_test(cx, |_| {});
20038
20039 let mut cx = EditorTestContext::new(cx).await;
20040
20041 let base_text = r#"
20042 one
20043 two
20044 three
20045 four
20046 five
20047 "#
20048 .unindent();
20049 executor.run_until_parked();
20050 cx.set_state(
20051 &r#"
20052 one
20053 two
20054 fˇour
20055 five
20056 "#
20057 .unindent(),
20058 );
20059
20060 cx.set_head_text(&base_text);
20061 executor.run_until_parked();
20062
20063 cx.update_editor(|editor, window, cx| {
20064 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20065 });
20066 executor.run_until_parked();
20067
20068 cx.assert_state_with_diff(
20069 r#"
20070 one
20071 two
20072 - three
20073 fˇour
20074 five
20075 "#
20076 .unindent(),
20077 );
20078
20079 cx.update_editor(|editor, window, cx| {
20080 editor.backspace(&Backspace, window, cx);
20081 editor.backspace(&Backspace, window, cx);
20082 });
20083 executor.run_until_parked();
20084 cx.assert_state_with_diff(
20085 r#"
20086 one
20087 two
20088 - threeˇ
20089 - four
20090 + our
20091 five
20092 "#
20093 .unindent(),
20094 );
20095}
20096
20097#[gpui::test]
20098async fn test_edit_after_expanded_modification_hunk(
20099 executor: BackgroundExecutor,
20100 cx: &mut TestAppContext,
20101) {
20102 init_test(cx, |_| {});
20103
20104 let mut cx = EditorTestContext::new(cx).await;
20105
20106 let diff_base = r#"
20107 use some::mod1;
20108 use some::mod2;
20109
20110 const A: u32 = 42;
20111 const B: u32 = 42;
20112 const C: u32 = 42;
20113 const D: u32 = 42;
20114
20115
20116 fn main() {
20117 println!("hello");
20118
20119 println!("world");
20120 }"#
20121 .unindent();
20122
20123 cx.set_state(
20124 &r#"
20125 use some::mod1;
20126 use some::mod2;
20127
20128 const A: u32 = 42;
20129 const B: u32 = 42;
20130 const C: u32 = 43ˇ
20131 const D: u32 = 42;
20132
20133
20134 fn main() {
20135 println!("hello");
20136
20137 println!("world");
20138 }"#
20139 .unindent(),
20140 );
20141
20142 cx.set_head_text(&diff_base);
20143 executor.run_until_parked();
20144 cx.update_editor(|editor, window, cx| {
20145 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20146 });
20147 executor.run_until_parked();
20148
20149 cx.assert_state_with_diff(
20150 r#"
20151 use some::mod1;
20152 use some::mod2;
20153
20154 const A: u32 = 42;
20155 const B: u32 = 42;
20156 - const C: u32 = 42;
20157 + const C: u32 = 43ˇ
20158 const D: u32 = 42;
20159
20160
20161 fn main() {
20162 println!("hello");
20163
20164 println!("world");
20165 }"#
20166 .unindent(),
20167 );
20168
20169 cx.update_editor(|editor, window, cx| {
20170 editor.handle_input("\nnew_line\n", window, cx);
20171 });
20172 executor.run_until_parked();
20173
20174 cx.assert_state_with_diff(
20175 r#"
20176 use some::mod1;
20177 use some::mod2;
20178
20179 const A: u32 = 42;
20180 const B: u32 = 42;
20181 - const C: u32 = 42;
20182 + const C: u32 = 43
20183 + new_line
20184 + ˇ
20185 const D: u32 = 42;
20186
20187
20188 fn main() {
20189 println!("hello");
20190
20191 println!("world");
20192 }"#
20193 .unindent(),
20194 );
20195}
20196
20197#[gpui::test]
20198async fn test_stage_and_unstage_added_file_hunk(
20199 executor: BackgroundExecutor,
20200 cx: &mut TestAppContext,
20201) {
20202 init_test(cx, |_| {});
20203
20204 let mut cx = EditorTestContext::new(cx).await;
20205 cx.update_editor(|editor, _, cx| {
20206 editor.set_expand_all_diff_hunks(cx);
20207 });
20208
20209 let working_copy = r#"
20210 ˇfn main() {
20211 println!("hello, world!");
20212 }
20213 "#
20214 .unindent();
20215
20216 cx.set_state(&working_copy);
20217 executor.run_until_parked();
20218
20219 cx.assert_state_with_diff(
20220 r#"
20221 + ˇfn main() {
20222 + println!("hello, world!");
20223 + }
20224 "#
20225 .unindent(),
20226 );
20227 cx.assert_index_text(None);
20228
20229 cx.update_editor(|editor, window, cx| {
20230 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20231 });
20232 executor.run_until_parked();
20233 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
20234 cx.assert_state_with_diff(
20235 r#"
20236 + ˇfn main() {
20237 + println!("hello, world!");
20238 + }
20239 "#
20240 .unindent(),
20241 );
20242
20243 cx.update_editor(|editor, window, cx| {
20244 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20245 });
20246 executor.run_until_parked();
20247 cx.assert_index_text(None);
20248}
20249
20250async fn setup_indent_guides_editor(
20251 text: &str,
20252 cx: &mut TestAppContext,
20253) -> (BufferId, EditorTestContext) {
20254 init_test(cx, |_| {});
20255
20256 let mut cx = EditorTestContext::new(cx).await;
20257
20258 let buffer_id = cx.update_editor(|editor, window, cx| {
20259 editor.set_text(text, window, cx);
20260 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
20261
20262 buffer_ids[0]
20263 });
20264
20265 (buffer_id, cx)
20266}
20267
20268fn assert_indent_guides(
20269 range: Range<u32>,
20270 expected: Vec<IndentGuide>,
20271 active_indices: Option<Vec<usize>>,
20272 cx: &mut EditorTestContext,
20273) {
20274 let indent_guides = cx.update_editor(|editor, window, cx| {
20275 let snapshot = editor.snapshot(window, cx).display_snapshot;
20276 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
20277 editor,
20278 MultiBufferRow(range.start)..MultiBufferRow(range.end),
20279 true,
20280 &snapshot,
20281 cx,
20282 );
20283
20284 indent_guides.sort_by(|a, b| {
20285 a.depth.cmp(&b.depth).then(
20286 a.start_row
20287 .cmp(&b.start_row)
20288 .then(a.end_row.cmp(&b.end_row)),
20289 )
20290 });
20291 indent_guides
20292 });
20293
20294 if let Some(expected) = active_indices {
20295 let active_indices = cx.update_editor(|editor, window, cx| {
20296 let snapshot = editor.snapshot(window, cx).display_snapshot;
20297 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
20298 });
20299
20300 assert_eq!(
20301 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
20302 expected,
20303 "Active indent guide indices do not match"
20304 );
20305 }
20306
20307 assert_eq!(indent_guides, expected, "Indent guides do not match");
20308}
20309
20310fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
20311 IndentGuide {
20312 buffer_id,
20313 start_row: MultiBufferRow(start_row),
20314 end_row: MultiBufferRow(end_row),
20315 depth,
20316 tab_size: 4,
20317 settings: IndentGuideSettings {
20318 enabled: true,
20319 line_width: 1,
20320 active_line_width: 1,
20321 coloring: IndentGuideColoring::default(),
20322 background_coloring: IndentGuideBackgroundColoring::default(),
20323 },
20324 }
20325}
20326
20327#[gpui::test]
20328async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
20329 let (buffer_id, mut cx) = setup_indent_guides_editor(
20330 &"
20331 fn main() {
20332 let a = 1;
20333 }"
20334 .unindent(),
20335 cx,
20336 )
20337 .await;
20338
20339 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20340}
20341
20342#[gpui::test]
20343async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
20344 let (buffer_id, mut cx) = setup_indent_guides_editor(
20345 &"
20346 fn main() {
20347 let a = 1;
20348 let b = 2;
20349 }"
20350 .unindent(),
20351 cx,
20352 )
20353 .await;
20354
20355 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
20356}
20357
20358#[gpui::test]
20359async fn test_indent_guide_nested(cx: &mut TestAppContext) {
20360 let (buffer_id, mut cx) = setup_indent_guides_editor(
20361 &"
20362 fn main() {
20363 let a = 1;
20364 if a == 3 {
20365 let b = 2;
20366 } else {
20367 let c = 3;
20368 }
20369 }"
20370 .unindent(),
20371 cx,
20372 )
20373 .await;
20374
20375 assert_indent_guides(
20376 0..8,
20377 vec![
20378 indent_guide(buffer_id, 1, 6, 0),
20379 indent_guide(buffer_id, 3, 3, 1),
20380 indent_guide(buffer_id, 5, 5, 1),
20381 ],
20382 None,
20383 &mut cx,
20384 );
20385}
20386
20387#[gpui::test]
20388async fn test_indent_guide_tab(cx: &mut TestAppContext) {
20389 let (buffer_id, mut cx) = setup_indent_guides_editor(
20390 &"
20391 fn main() {
20392 let a = 1;
20393 let b = 2;
20394 let c = 3;
20395 }"
20396 .unindent(),
20397 cx,
20398 )
20399 .await;
20400
20401 assert_indent_guides(
20402 0..5,
20403 vec![
20404 indent_guide(buffer_id, 1, 3, 0),
20405 indent_guide(buffer_id, 2, 2, 1),
20406 ],
20407 None,
20408 &mut cx,
20409 );
20410}
20411
20412#[gpui::test]
20413async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
20414 let (buffer_id, mut cx) = setup_indent_guides_editor(
20415 &"
20416 fn main() {
20417 let a = 1;
20418
20419 let c = 3;
20420 }"
20421 .unindent(),
20422 cx,
20423 )
20424 .await;
20425
20426 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
20427}
20428
20429#[gpui::test]
20430async fn test_indent_guide_complex(cx: &mut TestAppContext) {
20431 let (buffer_id, mut cx) = setup_indent_guides_editor(
20432 &"
20433 fn main() {
20434 let a = 1;
20435
20436 let c = 3;
20437
20438 if a == 3 {
20439 let b = 2;
20440 } else {
20441 let c = 3;
20442 }
20443 }"
20444 .unindent(),
20445 cx,
20446 )
20447 .await;
20448
20449 assert_indent_guides(
20450 0..11,
20451 vec![
20452 indent_guide(buffer_id, 1, 9, 0),
20453 indent_guide(buffer_id, 6, 6, 1),
20454 indent_guide(buffer_id, 8, 8, 1),
20455 ],
20456 None,
20457 &mut cx,
20458 );
20459}
20460
20461#[gpui::test]
20462async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
20463 let (buffer_id, mut cx) = setup_indent_guides_editor(
20464 &"
20465 fn main() {
20466 let a = 1;
20467
20468 let c = 3;
20469
20470 if a == 3 {
20471 let b = 2;
20472 } else {
20473 let c = 3;
20474 }
20475 }"
20476 .unindent(),
20477 cx,
20478 )
20479 .await;
20480
20481 assert_indent_guides(
20482 1..11,
20483 vec![
20484 indent_guide(buffer_id, 1, 9, 0),
20485 indent_guide(buffer_id, 6, 6, 1),
20486 indent_guide(buffer_id, 8, 8, 1),
20487 ],
20488 None,
20489 &mut cx,
20490 );
20491}
20492
20493#[gpui::test]
20494async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
20495 let (buffer_id, mut cx) = setup_indent_guides_editor(
20496 &"
20497 fn main() {
20498 let a = 1;
20499
20500 let c = 3;
20501
20502 if a == 3 {
20503 let b = 2;
20504 } else {
20505 let c = 3;
20506 }
20507 }"
20508 .unindent(),
20509 cx,
20510 )
20511 .await;
20512
20513 assert_indent_guides(
20514 1..10,
20515 vec![
20516 indent_guide(buffer_id, 1, 9, 0),
20517 indent_guide(buffer_id, 6, 6, 1),
20518 indent_guide(buffer_id, 8, 8, 1),
20519 ],
20520 None,
20521 &mut cx,
20522 );
20523}
20524
20525#[gpui::test]
20526async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
20527 let (buffer_id, mut cx) = setup_indent_guides_editor(
20528 &"
20529 fn main() {
20530 if a {
20531 b(
20532 c,
20533 d,
20534 )
20535 } else {
20536 e(
20537 f
20538 )
20539 }
20540 }"
20541 .unindent(),
20542 cx,
20543 )
20544 .await;
20545
20546 assert_indent_guides(
20547 0..11,
20548 vec![
20549 indent_guide(buffer_id, 1, 10, 0),
20550 indent_guide(buffer_id, 2, 5, 1),
20551 indent_guide(buffer_id, 7, 9, 1),
20552 indent_guide(buffer_id, 3, 4, 2),
20553 indent_guide(buffer_id, 8, 8, 2),
20554 ],
20555 None,
20556 &mut cx,
20557 );
20558
20559 cx.update_editor(|editor, window, cx| {
20560 editor.fold_at(MultiBufferRow(2), window, cx);
20561 assert_eq!(
20562 editor.display_text(cx),
20563 "
20564 fn main() {
20565 if a {
20566 b(⋯
20567 )
20568 } else {
20569 e(
20570 f
20571 )
20572 }
20573 }"
20574 .unindent()
20575 );
20576 });
20577
20578 assert_indent_guides(
20579 0..11,
20580 vec![
20581 indent_guide(buffer_id, 1, 10, 0),
20582 indent_guide(buffer_id, 2, 5, 1),
20583 indent_guide(buffer_id, 7, 9, 1),
20584 indent_guide(buffer_id, 8, 8, 2),
20585 ],
20586 None,
20587 &mut cx,
20588 );
20589}
20590
20591#[gpui::test]
20592async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
20593 let (buffer_id, mut cx) = setup_indent_guides_editor(
20594 &"
20595 block1
20596 block2
20597 block3
20598 block4
20599 block2
20600 block1
20601 block1"
20602 .unindent(),
20603 cx,
20604 )
20605 .await;
20606
20607 assert_indent_guides(
20608 1..10,
20609 vec![
20610 indent_guide(buffer_id, 1, 4, 0),
20611 indent_guide(buffer_id, 2, 3, 1),
20612 indent_guide(buffer_id, 3, 3, 2),
20613 ],
20614 None,
20615 &mut cx,
20616 );
20617}
20618
20619#[gpui::test]
20620async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
20621 let (buffer_id, mut cx) = setup_indent_guides_editor(
20622 &"
20623 block1
20624 block2
20625 block3
20626
20627 block1
20628 block1"
20629 .unindent(),
20630 cx,
20631 )
20632 .await;
20633
20634 assert_indent_guides(
20635 0..6,
20636 vec![
20637 indent_guide(buffer_id, 1, 2, 0),
20638 indent_guide(buffer_id, 2, 2, 1),
20639 ],
20640 None,
20641 &mut cx,
20642 );
20643}
20644
20645#[gpui::test]
20646async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
20647 let (buffer_id, mut cx) = setup_indent_guides_editor(
20648 &"
20649 function component() {
20650 \treturn (
20651 \t\t\t
20652 \t\t<div>
20653 \t\t\t<abc></abc>
20654 \t\t</div>
20655 \t)
20656 }"
20657 .unindent(),
20658 cx,
20659 )
20660 .await;
20661
20662 assert_indent_guides(
20663 0..8,
20664 vec![
20665 indent_guide(buffer_id, 1, 6, 0),
20666 indent_guide(buffer_id, 2, 5, 1),
20667 indent_guide(buffer_id, 4, 4, 2),
20668 ],
20669 None,
20670 &mut cx,
20671 );
20672}
20673
20674#[gpui::test]
20675async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
20676 let (buffer_id, mut cx) = setup_indent_guides_editor(
20677 &"
20678 function component() {
20679 \treturn (
20680 \t
20681 \t\t<div>
20682 \t\t\t<abc></abc>
20683 \t\t</div>
20684 \t)
20685 }"
20686 .unindent(),
20687 cx,
20688 )
20689 .await;
20690
20691 assert_indent_guides(
20692 0..8,
20693 vec![
20694 indent_guide(buffer_id, 1, 6, 0),
20695 indent_guide(buffer_id, 2, 5, 1),
20696 indent_guide(buffer_id, 4, 4, 2),
20697 ],
20698 None,
20699 &mut cx,
20700 );
20701}
20702
20703#[gpui::test]
20704async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
20705 let (buffer_id, mut cx) = setup_indent_guides_editor(
20706 &"
20707 block1
20708
20709
20710
20711 block2
20712 "
20713 .unindent(),
20714 cx,
20715 )
20716 .await;
20717
20718 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20719}
20720
20721#[gpui::test]
20722async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
20723 let (buffer_id, mut cx) = setup_indent_guides_editor(
20724 &"
20725 def a:
20726 \tb = 3
20727 \tif True:
20728 \t\tc = 4
20729 \t\td = 5
20730 \tprint(b)
20731 "
20732 .unindent(),
20733 cx,
20734 )
20735 .await;
20736
20737 assert_indent_guides(
20738 0..6,
20739 vec![
20740 indent_guide(buffer_id, 1, 5, 0),
20741 indent_guide(buffer_id, 3, 4, 1),
20742 ],
20743 None,
20744 &mut cx,
20745 );
20746}
20747
20748#[gpui::test]
20749async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
20750 let (buffer_id, mut cx) = setup_indent_guides_editor(
20751 &"
20752 fn main() {
20753 let a = 1;
20754 }"
20755 .unindent(),
20756 cx,
20757 )
20758 .await;
20759
20760 cx.update_editor(|editor, window, cx| {
20761 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20762 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20763 });
20764 });
20765
20766 assert_indent_guides(
20767 0..3,
20768 vec![indent_guide(buffer_id, 1, 1, 0)],
20769 Some(vec![0]),
20770 &mut cx,
20771 );
20772}
20773
20774#[gpui::test]
20775async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
20776 let (buffer_id, mut cx) = setup_indent_guides_editor(
20777 &"
20778 fn main() {
20779 if 1 == 2 {
20780 let a = 1;
20781 }
20782 }"
20783 .unindent(),
20784 cx,
20785 )
20786 .await;
20787
20788 cx.update_editor(|editor, window, cx| {
20789 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20790 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20791 });
20792 });
20793
20794 assert_indent_guides(
20795 0..4,
20796 vec![
20797 indent_guide(buffer_id, 1, 3, 0),
20798 indent_guide(buffer_id, 2, 2, 1),
20799 ],
20800 Some(vec![1]),
20801 &mut cx,
20802 );
20803
20804 cx.update_editor(|editor, window, cx| {
20805 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20806 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20807 });
20808 });
20809
20810 assert_indent_guides(
20811 0..4,
20812 vec![
20813 indent_guide(buffer_id, 1, 3, 0),
20814 indent_guide(buffer_id, 2, 2, 1),
20815 ],
20816 Some(vec![1]),
20817 &mut cx,
20818 );
20819
20820 cx.update_editor(|editor, window, cx| {
20821 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20822 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
20823 });
20824 });
20825
20826 assert_indent_guides(
20827 0..4,
20828 vec![
20829 indent_guide(buffer_id, 1, 3, 0),
20830 indent_guide(buffer_id, 2, 2, 1),
20831 ],
20832 Some(vec![0]),
20833 &mut cx,
20834 );
20835}
20836
20837#[gpui::test]
20838async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
20839 let (buffer_id, mut cx) = setup_indent_guides_editor(
20840 &"
20841 fn main() {
20842 let a = 1;
20843
20844 let b = 2;
20845 }"
20846 .unindent(),
20847 cx,
20848 )
20849 .await;
20850
20851 cx.update_editor(|editor, window, cx| {
20852 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20853 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20854 });
20855 });
20856
20857 assert_indent_guides(
20858 0..5,
20859 vec![indent_guide(buffer_id, 1, 3, 0)],
20860 Some(vec![0]),
20861 &mut cx,
20862 );
20863}
20864
20865#[gpui::test]
20866async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
20867 let (buffer_id, mut cx) = setup_indent_guides_editor(
20868 &"
20869 def m:
20870 a = 1
20871 pass"
20872 .unindent(),
20873 cx,
20874 )
20875 .await;
20876
20877 cx.update_editor(|editor, window, cx| {
20878 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20879 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20880 });
20881 });
20882
20883 assert_indent_guides(
20884 0..3,
20885 vec![indent_guide(buffer_id, 1, 2, 0)],
20886 Some(vec![0]),
20887 &mut cx,
20888 );
20889}
20890
20891#[gpui::test]
20892async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
20893 init_test(cx, |_| {});
20894 let mut cx = EditorTestContext::new(cx).await;
20895 let text = indoc! {
20896 "
20897 impl A {
20898 fn b() {
20899 0;
20900 3;
20901 5;
20902 6;
20903 7;
20904 }
20905 }
20906 "
20907 };
20908 let base_text = indoc! {
20909 "
20910 impl A {
20911 fn b() {
20912 0;
20913 1;
20914 2;
20915 3;
20916 4;
20917 }
20918 fn c() {
20919 5;
20920 6;
20921 7;
20922 }
20923 }
20924 "
20925 };
20926
20927 cx.update_editor(|editor, window, cx| {
20928 editor.set_text(text, window, cx);
20929
20930 editor.buffer().update(cx, |multibuffer, cx| {
20931 let buffer = multibuffer.as_singleton().unwrap();
20932 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
20933
20934 multibuffer.set_all_diff_hunks_expanded(cx);
20935 multibuffer.add_diff(diff, cx);
20936
20937 buffer.read(cx).remote_id()
20938 })
20939 });
20940 cx.run_until_parked();
20941
20942 cx.assert_state_with_diff(
20943 indoc! { "
20944 impl A {
20945 fn b() {
20946 0;
20947 - 1;
20948 - 2;
20949 3;
20950 - 4;
20951 - }
20952 - fn c() {
20953 5;
20954 6;
20955 7;
20956 }
20957 }
20958 ˇ"
20959 }
20960 .to_string(),
20961 );
20962
20963 let mut actual_guides = cx.update_editor(|editor, window, cx| {
20964 editor
20965 .snapshot(window, cx)
20966 .buffer_snapshot()
20967 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
20968 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
20969 .collect::<Vec<_>>()
20970 });
20971 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
20972 assert_eq!(
20973 actual_guides,
20974 vec![
20975 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
20976 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
20977 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
20978 ]
20979 );
20980}
20981
20982#[gpui::test]
20983async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20984 init_test(cx, |_| {});
20985 let mut cx = EditorTestContext::new(cx).await;
20986
20987 let diff_base = r#"
20988 a
20989 b
20990 c
20991 "#
20992 .unindent();
20993
20994 cx.set_state(
20995 &r#"
20996 ˇA
20997 b
20998 C
20999 "#
21000 .unindent(),
21001 );
21002 cx.set_head_text(&diff_base);
21003 cx.update_editor(|editor, window, cx| {
21004 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21005 });
21006 executor.run_until_parked();
21007
21008 let both_hunks_expanded = r#"
21009 - a
21010 + ˇA
21011 b
21012 - c
21013 + C
21014 "#
21015 .unindent();
21016
21017 cx.assert_state_with_diff(both_hunks_expanded.clone());
21018
21019 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21020 let snapshot = editor.snapshot(window, cx);
21021 let hunks = editor
21022 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21023 .collect::<Vec<_>>();
21024 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21025 let buffer_id = hunks[0].buffer_id;
21026 hunks
21027 .into_iter()
21028 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
21029 .collect::<Vec<_>>()
21030 });
21031 assert_eq!(hunk_ranges.len(), 2);
21032
21033 cx.update_editor(|editor, _, cx| {
21034 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21035 });
21036 executor.run_until_parked();
21037
21038 let second_hunk_expanded = r#"
21039 ˇA
21040 b
21041 - c
21042 + C
21043 "#
21044 .unindent();
21045
21046 cx.assert_state_with_diff(second_hunk_expanded);
21047
21048 cx.update_editor(|editor, _, cx| {
21049 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21050 });
21051 executor.run_until_parked();
21052
21053 cx.assert_state_with_diff(both_hunks_expanded.clone());
21054
21055 cx.update_editor(|editor, _, cx| {
21056 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21057 });
21058 executor.run_until_parked();
21059
21060 let first_hunk_expanded = r#"
21061 - a
21062 + ˇA
21063 b
21064 C
21065 "#
21066 .unindent();
21067
21068 cx.assert_state_with_diff(first_hunk_expanded);
21069
21070 cx.update_editor(|editor, _, cx| {
21071 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21072 });
21073 executor.run_until_parked();
21074
21075 cx.assert_state_with_diff(both_hunks_expanded);
21076
21077 cx.set_state(
21078 &r#"
21079 ˇA
21080 b
21081 "#
21082 .unindent(),
21083 );
21084 cx.run_until_parked();
21085
21086 // TODO this cursor position seems bad
21087 cx.assert_state_with_diff(
21088 r#"
21089 - ˇa
21090 + A
21091 b
21092 "#
21093 .unindent(),
21094 );
21095
21096 cx.update_editor(|editor, window, cx| {
21097 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21098 });
21099
21100 cx.assert_state_with_diff(
21101 r#"
21102 - ˇa
21103 + A
21104 b
21105 - c
21106 "#
21107 .unindent(),
21108 );
21109
21110 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21111 let snapshot = editor.snapshot(window, cx);
21112 let hunks = editor
21113 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21114 .collect::<Vec<_>>();
21115 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21116 let buffer_id = hunks[0].buffer_id;
21117 hunks
21118 .into_iter()
21119 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
21120 .collect::<Vec<_>>()
21121 });
21122 assert_eq!(hunk_ranges.len(), 2);
21123
21124 cx.update_editor(|editor, _, cx| {
21125 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21126 });
21127 executor.run_until_parked();
21128
21129 cx.assert_state_with_diff(
21130 r#"
21131 - ˇa
21132 + A
21133 b
21134 "#
21135 .unindent(),
21136 );
21137}
21138
21139#[gpui::test]
21140async fn test_toggle_deletion_hunk_at_start_of_file(
21141 executor: BackgroundExecutor,
21142 cx: &mut TestAppContext,
21143) {
21144 init_test(cx, |_| {});
21145 let mut cx = EditorTestContext::new(cx).await;
21146
21147 let diff_base = r#"
21148 a
21149 b
21150 c
21151 "#
21152 .unindent();
21153
21154 cx.set_state(
21155 &r#"
21156 ˇb
21157 c
21158 "#
21159 .unindent(),
21160 );
21161 cx.set_head_text(&diff_base);
21162 cx.update_editor(|editor, window, cx| {
21163 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21164 });
21165 executor.run_until_parked();
21166
21167 let hunk_expanded = r#"
21168 - a
21169 ˇb
21170 c
21171 "#
21172 .unindent();
21173
21174 cx.assert_state_with_diff(hunk_expanded.clone());
21175
21176 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21177 let snapshot = editor.snapshot(window, cx);
21178 let hunks = editor
21179 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21180 .collect::<Vec<_>>();
21181 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21182 let buffer_id = hunks[0].buffer_id;
21183 hunks
21184 .into_iter()
21185 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
21186 .collect::<Vec<_>>()
21187 });
21188 assert_eq!(hunk_ranges.len(), 1);
21189
21190 cx.update_editor(|editor, _, cx| {
21191 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21192 });
21193 executor.run_until_parked();
21194
21195 let hunk_collapsed = r#"
21196 ˇb
21197 c
21198 "#
21199 .unindent();
21200
21201 cx.assert_state_with_diff(hunk_collapsed);
21202
21203 cx.update_editor(|editor, _, cx| {
21204 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21205 });
21206 executor.run_until_parked();
21207
21208 cx.assert_state_with_diff(hunk_expanded);
21209}
21210
21211#[gpui::test]
21212async fn test_display_diff_hunks(cx: &mut TestAppContext) {
21213 init_test(cx, |_| {});
21214
21215 let fs = FakeFs::new(cx.executor());
21216 fs.insert_tree(
21217 path!("/test"),
21218 json!({
21219 ".git": {},
21220 "file-1": "ONE\n",
21221 "file-2": "TWO\n",
21222 "file-3": "THREE\n",
21223 }),
21224 )
21225 .await;
21226
21227 fs.set_head_for_repo(
21228 path!("/test/.git").as_ref(),
21229 &[
21230 ("file-1", "one\n".into()),
21231 ("file-2", "two\n".into()),
21232 ("file-3", "three\n".into()),
21233 ],
21234 "deadbeef",
21235 );
21236
21237 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
21238 let mut buffers = vec![];
21239 for i in 1..=3 {
21240 let buffer = project
21241 .update(cx, |project, cx| {
21242 let path = format!(path!("/test/file-{}"), i);
21243 project.open_local_buffer(path, cx)
21244 })
21245 .await
21246 .unwrap();
21247 buffers.push(buffer);
21248 }
21249
21250 let multibuffer = cx.new(|cx| {
21251 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
21252 multibuffer.set_all_diff_hunks_expanded(cx);
21253 for buffer in &buffers {
21254 let snapshot = buffer.read(cx).snapshot();
21255 multibuffer.set_excerpts_for_path(
21256 PathKey::with_sort_prefix(0, buffer.read(cx).file().unwrap().path().clone()),
21257 buffer.clone(),
21258 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
21259 2,
21260 cx,
21261 );
21262 }
21263 multibuffer
21264 });
21265
21266 let editor = cx.add_window(|window, cx| {
21267 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
21268 });
21269 cx.run_until_parked();
21270
21271 let snapshot = editor
21272 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21273 .unwrap();
21274 let hunks = snapshot
21275 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
21276 .map(|hunk| match hunk {
21277 DisplayDiffHunk::Unfolded {
21278 display_row_range, ..
21279 } => display_row_range,
21280 DisplayDiffHunk::Folded { .. } => unreachable!(),
21281 })
21282 .collect::<Vec<_>>();
21283 assert_eq!(
21284 hunks,
21285 [
21286 DisplayRow(2)..DisplayRow(4),
21287 DisplayRow(7)..DisplayRow(9),
21288 DisplayRow(12)..DisplayRow(14),
21289 ]
21290 );
21291}
21292
21293#[gpui::test]
21294async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
21295 init_test(cx, |_| {});
21296
21297 let mut cx = EditorTestContext::new(cx).await;
21298 cx.set_head_text(indoc! { "
21299 one
21300 two
21301 three
21302 four
21303 five
21304 "
21305 });
21306 cx.set_index_text(indoc! { "
21307 one
21308 two
21309 three
21310 four
21311 five
21312 "
21313 });
21314 cx.set_state(indoc! {"
21315 one
21316 TWO
21317 ˇTHREE
21318 FOUR
21319 five
21320 "});
21321 cx.run_until_parked();
21322 cx.update_editor(|editor, window, cx| {
21323 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21324 });
21325 cx.run_until_parked();
21326 cx.assert_index_text(Some(indoc! {"
21327 one
21328 TWO
21329 THREE
21330 FOUR
21331 five
21332 "}));
21333 cx.set_state(indoc! { "
21334 one
21335 TWO
21336 ˇTHREE-HUNDRED
21337 FOUR
21338 five
21339 "});
21340 cx.run_until_parked();
21341 cx.update_editor(|editor, window, cx| {
21342 let snapshot = editor.snapshot(window, cx);
21343 let hunks = editor
21344 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21345 .collect::<Vec<_>>();
21346 assert_eq!(hunks.len(), 1);
21347 assert_eq!(
21348 hunks[0].status(),
21349 DiffHunkStatus {
21350 kind: DiffHunkStatusKind::Modified,
21351 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
21352 }
21353 );
21354
21355 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21356 });
21357 cx.run_until_parked();
21358 cx.assert_index_text(Some(indoc! {"
21359 one
21360 TWO
21361 THREE-HUNDRED
21362 FOUR
21363 five
21364 "}));
21365}
21366
21367#[gpui::test]
21368fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
21369 init_test(cx, |_| {});
21370
21371 let editor = cx.add_window(|window, cx| {
21372 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
21373 build_editor(buffer, window, cx)
21374 });
21375
21376 let render_args = Arc::new(Mutex::new(None));
21377 let snapshot = editor
21378 .update(cx, |editor, window, cx| {
21379 let snapshot = editor.buffer().read(cx).snapshot(cx);
21380 let range =
21381 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
21382
21383 struct RenderArgs {
21384 row: MultiBufferRow,
21385 folded: bool,
21386 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
21387 }
21388
21389 let crease = Crease::inline(
21390 range,
21391 FoldPlaceholder::test(),
21392 {
21393 let toggle_callback = render_args.clone();
21394 move |row, folded, callback, _window, _cx| {
21395 *toggle_callback.lock() = Some(RenderArgs {
21396 row,
21397 folded,
21398 callback,
21399 });
21400 div()
21401 }
21402 },
21403 |_row, _folded, _window, _cx| div(),
21404 );
21405
21406 editor.insert_creases(Some(crease), cx);
21407 let snapshot = editor.snapshot(window, cx);
21408 let _div =
21409 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
21410 snapshot
21411 })
21412 .unwrap();
21413
21414 let render_args = render_args.lock().take().unwrap();
21415 assert_eq!(render_args.row, MultiBufferRow(1));
21416 assert!(!render_args.folded);
21417 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21418
21419 cx.update_window(*editor, |_, window, cx| {
21420 (render_args.callback)(true, window, cx)
21421 })
21422 .unwrap();
21423 let snapshot = editor
21424 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21425 .unwrap();
21426 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
21427
21428 cx.update_window(*editor, |_, window, cx| {
21429 (render_args.callback)(false, window, cx)
21430 })
21431 .unwrap();
21432 let snapshot = editor
21433 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21434 .unwrap();
21435 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21436}
21437
21438#[gpui::test]
21439async fn test_input_text(cx: &mut TestAppContext) {
21440 init_test(cx, |_| {});
21441 let mut cx = EditorTestContext::new(cx).await;
21442
21443 cx.set_state(
21444 &r#"ˇone
21445 two
21446
21447 three
21448 fourˇ
21449 five
21450
21451 siˇx"#
21452 .unindent(),
21453 );
21454
21455 cx.dispatch_action(HandleInput(String::new()));
21456 cx.assert_editor_state(
21457 &r#"ˇone
21458 two
21459
21460 three
21461 fourˇ
21462 five
21463
21464 siˇx"#
21465 .unindent(),
21466 );
21467
21468 cx.dispatch_action(HandleInput("AAAA".to_string()));
21469 cx.assert_editor_state(
21470 &r#"AAAAˇone
21471 two
21472
21473 three
21474 fourAAAAˇ
21475 five
21476
21477 siAAAAˇx"#
21478 .unindent(),
21479 );
21480}
21481
21482#[gpui::test]
21483async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
21484 init_test(cx, |_| {});
21485
21486 let mut cx = EditorTestContext::new(cx).await;
21487 cx.set_state(
21488 r#"let foo = 1;
21489let foo = 2;
21490let foo = 3;
21491let fooˇ = 4;
21492let foo = 5;
21493let foo = 6;
21494let foo = 7;
21495let foo = 8;
21496let foo = 9;
21497let foo = 10;
21498let foo = 11;
21499let foo = 12;
21500let foo = 13;
21501let foo = 14;
21502let foo = 15;"#,
21503 );
21504
21505 cx.update_editor(|e, window, cx| {
21506 assert_eq!(
21507 e.next_scroll_position,
21508 NextScrollCursorCenterTopBottom::Center,
21509 "Default next scroll direction is center",
21510 );
21511
21512 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21513 assert_eq!(
21514 e.next_scroll_position,
21515 NextScrollCursorCenterTopBottom::Top,
21516 "After center, next scroll direction should be top",
21517 );
21518
21519 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21520 assert_eq!(
21521 e.next_scroll_position,
21522 NextScrollCursorCenterTopBottom::Bottom,
21523 "After top, next scroll direction should be bottom",
21524 );
21525
21526 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21527 assert_eq!(
21528 e.next_scroll_position,
21529 NextScrollCursorCenterTopBottom::Center,
21530 "After bottom, scrolling should start over",
21531 );
21532
21533 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21534 assert_eq!(
21535 e.next_scroll_position,
21536 NextScrollCursorCenterTopBottom::Top,
21537 "Scrolling continues if retriggered fast enough"
21538 );
21539 });
21540
21541 cx.executor()
21542 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
21543 cx.executor().run_until_parked();
21544 cx.update_editor(|e, _, _| {
21545 assert_eq!(
21546 e.next_scroll_position,
21547 NextScrollCursorCenterTopBottom::Center,
21548 "If scrolling is not triggered fast enough, it should reset"
21549 );
21550 });
21551}
21552
21553#[gpui::test]
21554async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
21555 init_test(cx, |_| {});
21556 let mut cx = EditorLspTestContext::new_rust(
21557 lsp::ServerCapabilities {
21558 definition_provider: Some(lsp::OneOf::Left(true)),
21559 references_provider: Some(lsp::OneOf::Left(true)),
21560 ..lsp::ServerCapabilities::default()
21561 },
21562 cx,
21563 )
21564 .await;
21565
21566 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
21567 let go_to_definition = cx
21568 .lsp
21569 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21570 move |params, _| async move {
21571 if empty_go_to_definition {
21572 Ok(None)
21573 } else {
21574 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
21575 uri: params.text_document_position_params.text_document.uri,
21576 range: lsp::Range::new(
21577 lsp::Position::new(4, 3),
21578 lsp::Position::new(4, 6),
21579 ),
21580 })))
21581 }
21582 },
21583 );
21584 let references = cx
21585 .lsp
21586 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21587 Ok(Some(vec![lsp::Location {
21588 uri: params.text_document_position.text_document.uri,
21589 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
21590 }]))
21591 });
21592 (go_to_definition, references)
21593 };
21594
21595 cx.set_state(
21596 &r#"fn one() {
21597 let mut a = ˇtwo();
21598 }
21599
21600 fn two() {}"#
21601 .unindent(),
21602 );
21603 set_up_lsp_handlers(false, &mut cx);
21604 let navigated = cx
21605 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21606 .await
21607 .expect("Failed to navigate to definition");
21608 assert_eq!(
21609 navigated,
21610 Navigated::Yes,
21611 "Should have navigated to definition from the GetDefinition response"
21612 );
21613 cx.assert_editor_state(
21614 &r#"fn one() {
21615 let mut a = two();
21616 }
21617
21618 fn «twoˇ»() {}"#
21619 .unindent(),
21620 );
21621
21622 let editors = cx.update_workspace(|workspace, _, cx| {
21623 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21624 });
21625 cx.update_editor(|_, _, test_editor_cx| {
21626 assert_eq!(
21627 editors.len(),
21628 1,
21629 "Initially, only one, test, editor should be open in the workspace"
21630 );
21631 assert_eq!(
21632 test_editor_cx.entity(),
21633 editors.last().expect("Asserted len is 1").clone()
21634 );
21635 });
21636
21637 set_up_lsp_handlers(true, &mut cx);
21638 let navigated = cx
21639 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21640 .await
21641 .expect("Failed to navigate to lookup references");
21642 assert_eq!(
21643 navigated,
21644 Navigated::Yes,
21645 "Should have navigated to references as a fallback after empty GoToDefinition response"
21646 );
21647 // We should not change the selections in the existing file,
21648 // if opening another milti buffer with the references
21649 cx.assert_editor_state(
21650 &r#"fn one() {
21651 let mut a = two();
21652 }
21653
21654 fn «twoˇ»() {}"#
21655 .unindent(),
21656 );
21657 let editors = cx.update_workspace(|workspace, _, cx| {
21658 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21659 });
21660 cx.update_editor(|_, _, test_editor_cx| {
21661 assert_eq!(
21662 editors.len(),
21663 2,
21664 "After falling back to references search, we open a new editor with the results"
21665 );
21666 let references_fallback_text = editors
21667 .into_iter()
21668 .find(|new_editor| *new_editor != test_editor_cx.entity())
21669 .expect("Should have one non-test editor now")
21670 .read(test_editor_cx)
21671 .text(test_editor_cx);
21672 assert_eq!(
21673 references_fallback_text, "fn one() {\n let mut a = two();\n}",
21674 "Should use the range from the references response and not the GoToDefinition one"
21675 );
21676 });
21677}
21678
21679#[gpui::test]
21680async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
21681 init_test(cx, |_| {});
21682 cx.update(|cx| {
21683 let mut editor_settings = EditorSettings::get_global(cx).clone();
21684 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
21685 EditorSettings::override_global(editor_settings, cx);
21686 });
21687 let mut cx = EditorLspTestContext::new_rust(
21688 lsp::ServerCapabilities {
21689 definition_provider: Some(lsp::OneOf::Left(true)),
21690 references_provider: Some(lsp::OneOf::Left(true)),
21691 ..lsp::ServerCapabilities::default()
21692 },
21693 cx,
21694 )
21695 .await;
21696 let original_state = r#"fn one() {
21697 let mut a = ˇtwo();
21698 }
21699
21700 fn two() {}"#
21701 .unindent();
21702 cx.set_state(&original_state);
21703
21704 let mut go_to_definition = cx
21705 .lsp
21706 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21707 move |_, _| async move { Ok(None) },
21708 );
21709 let _references = cx
21710 .lsp
21711 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
21712 panic!("Should not call for references with no go to definition fallback")
21713 });
21714
21715 let navigated = cx
21716 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21717 .await
21718 .expect("Failed to navigate to lookup references");
21719 go_to_definition
21720 .next()
21721 .await
21722 .expect("Should have called the go_to_definition handler");
21723
21724 assert_eq!(
21725 navigated,
21726 Navigated::No,
21727 "Should have navigated to references as a fallback after empty GoToDefinition response"
21728 );
21729 cx.assert_editor_state(&original_state);
21730 let editors = cx.update_workspace(|workspace, _, cx| {
21731 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21732 });
21733 cx.update_editor(|_, _, _| {
21734 assert_eq!(
21735 editors.len(),
21736 1,
21737 "After unsuccessful fallback, no other editor should have been opened"
21738 );
21739 });
21740}
21741
21742#[gpui::test]
21743async fn test_find_all_references_editor_reuse(cx: &mut TestAppContext) {
21744 init_test(cx, |_| {});
21745 let mut cx = EditorLspTestContext::new_rust(
21746 lsp::ServerCapabilities {
21747 references_provider: Some(lsp::OneOf::Left(true)),
21748 ..lsp::ServerCapabilities::default()
21749 },
21750 cx,
21751 )
21752 .await;
21753
21754 cx.set_state(
21755 &r#"
21756 fn one() {
21757 let mut a = two();
21758 }
21759
21760 fn ˇtwo() {}"#
21761 .unindent(),
21762 );
21763 cx.lsp
21764 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21765 Ok(Some(vec![
21766 lsp::Location {
21767 uri: params.text_document_position.text_document.uri.clone(),
21768 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21769 },
21770 lsp::Location {
21771 uri: params.text_document_position.text_document.uri,
21772 range: lsp::Range::new(lsp::Position::new(4, 4), lsp::Position::new(4, 7)),
21773 },
21774 ]))
21775 });
21776 let navigated = cx
21777 .update_editor(|editor, window, cx| {
21778 editor.find_all_references(&FindAllReferences, window, cx)
21779 })
21780 .unwrap()
21781 .await
21782 .expect("Failed to navigate to references");
21783 assert_eq!(
21784 navigated,
21785 Navigated::Yes,
21786 "Should have navigated to references from the FindAllReferences response"
21787 );
21788 cx.assert_editor_state(
21789 &r#"fn one() {
21790 let mut a = two();
21791 }
21792
21793 fn ˇtwo() {}"#
21794 .unindent(),
21795 );
21796
21797 let editors = cx.update_workspace(|workspace, _, cx| {
21798 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21799 });
21800 cx.update_editor(|_, _, _| {
21801 assert_eq!(editors.len(), 2, "We should have opened a new multibuffer");
21802 });
21803
21804 cx.set_state(
21805 &r#"fn one() {
21806 let mut a = ˇtwo();
21807 }
21808
21809 fn two() {}"#
21810 .unindent(),
21811 );
21812 let navigated = cx
21813 .update_editor(|editor, window, cx| {
21814 editor.find_all_references(&FindAllReferences, window, cx)
21815 })
21816 .unwrap()
21817 .await
21818 .expect("Failed to navigate to references");
21819 assert_eq!(
21820 navigated,
21821 Navigated::Yes,
21822 "Should have navigated to references from the FindAllReferences response"
21823 );
21824 cx.assert_editor_state(
21825 &r#"fn one() {
21826 let mut a = ˇtwo();
21827 }
21828
21829 fn two() {}"#
21830 .unindent(),
21831 );
21832 let editors = cx.update_workspace(|workspace, _, cx| {
21833 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21834 });
21835 cx.update_editor(|_, _, _| {
21836 assert_eq!(
21837 editors.len(),
21838 2,
21839 "should have re-used the previous multibuffer"
21840 );
21841 });
21842
21843 cx.set_state(
21844 &r#"fn one() {
21845 let mut a = ˇtwo();
21846 }
21847 fn three() {}
21848 fn two() {}"#
21849 .unindent(),
21850 );
21851 cx.lsp
21852 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21853 Ok(Some(vec![
21854 lsp::Location {
21855 uri: params.text_document_position.text_document.uri.clone(),
21856 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21857 },
21858 lsp::Location {
21859 uri: params.text_document_position.text_document.uri,
21860 range: lsp::Range::new(lsp::Position::new(5, 4), lsp::Position::new(5, 7)),
21861 },
21862 ]))
21863 });
21864 let navigated = cx
21865 .update_editor(|editor, window, cx| {
21866 editor.find_all_references(&FindAllReferences, window, cx)
21867 })
21868 .unwrap()
21869 .await
21870 .expect("Failed to navigate to references");
21871 assert_eq!(
21872 navigated,
21873 Navigated::Yes,
21874 "Should have navigated to references from the FindAllReferences response"
21875 );
21876 cx.assert_editor_state(
21877 &r#"fn one() {
21878 let mut a = ˇtwo();
21879 }
21880 fn three() {}
21881 fn two() {}"#
21882 .unindent(),
21883 );
21884 let editors = cx.update_workspace(|workspace, _, cx| {
21885 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21886 });
21887 cx.update_editor(|_, _, _| {
21888 assert_eq!(
21889 editors.len(),
21890 3,
21891 "should have used a new multibuffer as offsets changed"
21892 );
21893 });
21894}
21895#[gpui::test]
21896async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
21897 init_test(cx, |_| {});
21898
21899 let language = Arc::new(Language::new(
21900 LanguageConfig::default(),
21901 Some(tree_sitter_rust::LANGUAGE.into()),
21902 ));
21903
21904 let text = r#"
21905 #[cfg(test)]
21906 mod tests() {
21907 #[test]
21908 fn runnable_1() {
21909 let a = 1;
21910 }
21911
21912 #[test]
21913 fn runnable_2() {
21914 let a = 1;
21915 let b = 2;
21916 }
21917 }
21918 "#
21919 .unindent();
21920
21921 let fs = FakeFs::new(cx.executor());
21922 fs.insert_file("/file.rs", Default::default()).await;
21923
21924 let project = Project::test(fs, ["/a".as_ref()], cx).await;
21925 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21926 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21927 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
21928 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
21929
21930 let editor = cx.new_window_entity(|window, cx| {
21931 Editor::new(
21932 EditorMode::full(),
21933 multi_buffer,
21934 Some(project.clone()),
21935 window,
21936 cx,
21937 )
21938 });
21939
21940 editor.update_in(cx, |editor, window, cx| {
21941 let snapshot = editor.buffer().read(cx).snapshot(cx);
21942 editor.tasks.insert(
21943 (buffer.read(cx).remote_id(), 3),
21944 RunnableTasks {
21945 templates: vec![],
21946 offset: snapshot.anchor_before(43),
21947 column: 0,
21948 extra_variables: HashMap::default(),
21949 context_range: BufferOffset(43)..BufferOffset(85),
21950 },
21951 );
21952 editor.tasks.insert(
21953 (buffer.read(cx).remote_id(), 8),
21954 RunnableTasks {
21955 templates: vec![],
21956 offset: snapshot.anchor_before(86),
21957 column: 0,
21958 extra_variables: HashMap::default(),
21959 context_range: BufferOffset(86)..BufferOffset(191),
21960 },
21961 );
21962
21963 // Test finding task when cursor is inside function body
21964 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21965 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
21966 });
21967 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
21968 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
21969
21970 // Test finding task when cursor is on function name
21971 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21972 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
21973 });
21974 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
21975 assert_eq!(row, 8, "Should find task when cursor is on function name");
21976 });
21977}
21978
21979#[gpui::test]
21980async fn test_folding_buffers(cx: &mut TestAppContext) {
21981 init_test(cx, |_| {});
21982
21983 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
21984 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
21985 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
21986
21987 let fs = FakeFs::new(cx.executor());
21988 fs.insert_tree(
21989 path!("/a"),
21990 json!({
21991 "first.rs": sample_text_1,
21992 "second.rs": sample_text_2,
21993 "third.rs": sample_text_3,
21994 }),
21995 )
21996 .await;
21997 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21998 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21999 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22000 let worktree = project.update(cx, |project, cx| {
22001 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22002 assert_eq!(worktrees.len(), 1);
22003 worktrees.pop().unwrap()
22004 });
22005 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22006
22007 let buffer_1 = project
22008 .update(cx, |project, cx| {
22009 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
22010 })
22011 .await
22012 .unwrap();
22013 let buffer_2 = project
22014 .update(cx, |project, cx| {
22015 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
22016 })
22017 .await
22018 .unwrap();
22019 let buffer_3 = project
22020 .update(cx, |project, cx| {
22021 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
22022 })
22023 .await
22024 .unwrap();
22025
22026 let multi_buffer = cx.new(|cx| {
22027 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22028 multi_buffer.push_excerpts(
22029 buffer_1.clone(),
22030 [
22031 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22032 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22033 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22034 ],
22035 cx,
22036 );
22037 multi_buffer.push_excerpts(
22038 buffer_2.clone(),
22039 [
22040 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22041 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22042 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22043 ],
22044 cx,
22045 );
22046 multi_buffer.push_excerpts(
22047 buffer_3.clone(),
22048 [
22049 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22050 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22051 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22052 ],
22053 cx,
22054 );
22055 multi_buffer
22056 });
22057 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22058 Editor::new(
22059 EditorMode::full(),
22060 multi_buffer.clone(),
22061 Some(project.clone()),
22062 window,
22063 cx,
22064 )
22065 });
22066
22067 assert_eq!(
22068 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22069 "\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",
22070 );
22071
22072 multi_buffer_editor.update(cx, |editor, cx| {
22073 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
22074 });
22075 assert_eq!(
22076 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22077 "\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",
22078 "After folding the first buffer, its text should not be displayed"
22079 );
22080
22081 multi_buffer_editor.update(cx, |editor, cx| {
22082 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
22083 });
22084 assert_eq!(
22085 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22086 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
22087 "After folding the second buffer, its text should not be displayed"
22088 );
22089
22090 multi_buffer_editor.update(cx, |editor, cx| {
22091 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
22092 });
22093 assert_eq!(
22094 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22095 "\n\n\n\n\n",
22096 "After folding the third buffer, its text should not be displayed"
22097 );
22098
22099 // Emulate selection inside the fold logic, that should work
22100 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22101 editor
22102 .snapshot(window, cx)
22103 .next_line_boundary(Point::new(0, 4));
22104 });
22105
22106 multi_buffer_editor.update(cx, |editor, cx| {
22107 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
22108 });
22109 assert_eq!(
22110 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22111 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
22112 "After unfolding the second buffer, its text should be displayed"
22113 );
22114
22115 // Typing inside of buffer 1 causes that buffer to be unfolded.
22116 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22117 assert_eq!(
22118 multi_buffer
22119 .read(cx)
22120 .snapshot(cx)
22121 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
22122 .collect::<String>(),
22123 "bbbb"
22124 );
22125 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
22126 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
22127 });
22128 editor.handle_input("B", window, cx);
22129 });
22130
22131 assert_eq!(
22132 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22133 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
22134 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
22135 );
22136
22137 multi_buffer_editor.update(cx, |editor, cx| {
22138 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
22139 });
22140 assert_eq!(
22141 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22142 "\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",
22143 "After unfolding the all buffers, all original text should be displayed"
22144 );
22145}
22146
22147#[gpui::test]
22148async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
22149 init_test(cx, |_| {});
22150
22151 let sample_text_1 = "1111\n2222\n3333".to_string();
22152 let sample_text_2 = "4444\n5555\n6666".to_string();
22153 let sample_text_3 = "7777\n8888\n9999".to_string();
22154
22155 let fs = FakeFs::new(cx.executor());
22156 fs.insert_tree(
22157 path!("/a"),
22158 json!({
22159 "first.rs": sample_text_1,
22160 "second.rs": sample_text_2,
22161 "third.rs": sample_text_3,
22162 }),
22163 )
22164 .await;
22165 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22166 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22167 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22168 let worktree = project.update(cx, |project, cx| {
22169 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22170 assert_eq!(worktrees.len(), 1);
22171 worktrees.pop().unwrap()
22172 });
22173 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22174
22175 let buffer_1 = project
22176 .update(cx, |project, cx| {
22177 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
22178 })
22179 .await
22180 .unwrap();
22181 let buffer_2 = project
22182 .update(cx, |project, cx| {
22183 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
22184 })
22185 .await
22186 .unwrap();
22187 let buffer_3 = project
22188 .update(cx, |project, cx| {
22189 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
22190 })
22191 .await
22192 .unwrap();
22193
22194 let multi_buffer = cx.new(|cx| {
22195 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22196 multi_buffer.push_excerpts(
22197 buffer_1.clone(),
22198 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22199 cx,
22200 );
22201 multi_buffer.push_excerpts(
22202 buffer_2.clone(),
22203 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22204 cx,
22205 );
22206 multi_buffer.push_excerpts(
22207 buffer_3.clone(),
22208 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22209 cx,
22210 );
22211 multi_buffer
22212 });
22213
22214 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22215 Editor::new(
22216 EditorMode::full(),
22217 multi_buffer,
22218 Some(project.clone()),
22219 window,
22220 cx,
22221 )
22222 });
22223
22224 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
22225 assert_eq!(
22226 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22227 full_text,
22228 );
22229
22230 multi_buffer_editor.update(cx, |editor, cx| {
22231 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
22232 });
22233 assert_eq!(
22234 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22235 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
22236 "After folding the first buffer, its text should not be displayed"
22237 );
22238
22239 multi_buffer_editor.update(cx, |editor, cx| {
22240 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
22241 });
22242
22243 assert_eq!(
22244 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22245 "\n\n\n\n\n\n7777\n8888\n9999",
22246 "After folding the second buffer, its text should not be displayed"
22247 );
22248
22249 multi_buffer_editor.update(cx, |editor, cx| {
22250 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
22251 });
22252 assert_eq!(
22253 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22254 "\n\n\n\n\n",
22255 "After folding the third buffer, its text should not be displayed"
22256 );
22257
22258 multi_buffer_editor.update(cx, |editor, cx| {
22259 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
22260 });
22261 assert_eq!(
22262 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22263 "\n\n\n\n4444\n5555\n6666\n\n",
22264 "After unfolding the second buffer, its text should be displayed"
22265 );
22266
22267 multi_buffer_editor.update(cx, |editor, cx| {
22268 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
22269 });
22270 assert_eq!(
22271 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22272 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
22273 "After unfolding the first buffer, its text should be displayed"
22274 );
22275
22276 multi_buffer_editor.update(cx, |editor, cx| {
22277 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
22278 });
22279 assert_eq!(
22280 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22281 full_text,
22282 "After unfolding all buffers, all original text should be displayed"
22283 );
22284}
22285
22286#[gpui::test]
22287async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
22288 init_test(cx, |_| {});
22289
22290 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
22291
22292 let fs = FakeFs::new(cx.executor());
22293 fs.insert_tree(
22294 path!("/a"),
22295 json!({
22296 "main.rs": sample_text,
22297 }),
22298 )
22299 .await;
22300 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22301 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22302 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22303 let worktree = project.update(cx, |project, cx| {
22304 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22305 assert_eq!(worktrees.len(), 1);
22306 worktrees.pop().unwrap()
22307 });
22308 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22309
22310 let buffer_1 = project
22311 .update(cx, |project, cx| {
22312 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22313 })
22314 .await
22315 .unwrap();
22316
22317 let multi_buffer = cx.new(|cx| {
22318 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22319 multi_buffer.push_excerpts(
22320 buffer_1.clone(),
22321 [ExcerptRange::new(
22322 Point::new(0, 0)
22323 ..Point::new(
22324 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
22325 0,
22326 ),
22327 )],
22328 cx,
22329 );
22330 multi_buffer
22331 });
22332 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22333 Editor::new(
22334 EditorMode::full(),
22335 multi_buffer,
22336 Some(project.clone()),
22337 window,
22338 cx,
22339 )
22340 });
22341
22342 let selection_range = Point::new(1, 0)..Point::new(2, 0);
22343 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22344 enum TestHighlight {}
22345 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
22346 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
22347 editor.highlight_text::<TestHighlight>(
22348 vec![highlight_range.clone()],
22349 HighlightStyle::color(Hsla::green()),
22350 cx,
22351 );
22352 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22353 s.select_ranges(Some(highlight_range))
22354 });
22355 });
22356
22357 let full_text = format!("\n\n{sample_text}");
22358 assert_eq!(
22359 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22360 full_text,
22361 );
22362}
22363
22364#[gpui::test]
22365async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
22366 init_test(cx, |_| {});
22367 cx.update(|cx| {
22368 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
22369 "keymaps/default-linux.json",
22370 cx,
22371 )
22372 .unwrap();
22373 cx.bind_keys(default_key_bindings);
22374 });
22375
22376 let (editor, cx) = cx.add_window_view(|window, cx| {
22377 let multi_buffer = MultiBuffer::build_multi(
22378 [
22379 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
22380 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
22381 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
22382 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
22383 ],
22384 cx,
22385 );
22386 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
22387
22388 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
22389 // fold all but the second buffer, so that we test navigating between two
22390 // adjacent folded buffers, as well as folded buffers at the start and
22391 // end the multibuffer
22392 editor.fold_buffer(buffer_ids[0], cx);
22393 editor.fold_buffer(buffer_ids[2], cx);
22394 editor.fold_buffer(buffer_ids[3], cx);
22395
22396 editor
22397 });
22398 cx.simulate_resize(size(px(1000.), px(1000.)));
22399
22400 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
22401 cx.assert_excerpts_with_selections(indoc! {"
22402 [EXCERPT]
22403 ˇ[FOLDED]
22404 [EXCERPT]
22405 a1
22406 b1
22407 [EXCERPT]
22408 [FOLDED]
22409 [EXCERPT]
22410 [FOLDED]
22411 "
22412 });
22413 cx.simulate_keystroke("down");
22414 cx.assert_excerpts_with_selections(indoc! {"
22415 [EXCERPT]
22416 [FOLDED]
22417 [EXCERPT]
22418 ˇa1
22419 b1
22420 [EXCERPT]
22421 [FOLDED]
22422 [EXCERPT]
22423 [FOLDED]
22424 "
22425 });
22426 cx.simulate_keystroke("down");
22427 cx.assert_excerpts_with_selections(indoc! {"
22428 [EXCERPT]
22429 [FOLDED]
22430 [EXCERPT]
22431 a1
22432 ˇb1
22433 [EXCERPT]
22434 [FOLDED]
22435 [EXCERPT]
22436 [FOLDED]
22437 "
22438 });
22439 cx.simulate_keystroke("down");
22440 cx.assert_excerpts_with_selections(indoc! {"
22441 [EXCERPT]
22442 [FOLDED]
22443 [EXCERPT]
22444 a1
22445 b1
22446 ˇ[EXCERPT]
22447 [FOLDED]
22448 [EXCERPT]
22449 [FOLDED]
22450 "
22451 });
22452 cx.simulate_keystroke("down");
22453 cx.assert_excerpts_with_selections(indoc! {"
22454 [EXCERPT]
22455 [FOLDED]
22456 [EXCERPT]
22457 a1
22458 b1
22459 [EXCERPT]
22460 ˇ[FOLDED]
22461 [EXCERPT]
22462 [FOLDED]
22463 "
22464 });
22465 for _ in 0..5 {
22466 cx.simulate_keystroke("down");
22467 cx.assert_excerpts_with_selections(indoc! {"
22468 [EXCERPT]
22469 [FOLDED]
22470 [EXCERPT]
22471 a1
22472 b1
22473 [EXCERPT]
22474 [FOLDED]
22475 [EXCERPT]
22476 ˇ[FOLDED]
22477 "
22478 });
22479 }
22480
22481 cx.simulate_keystroke("up");
22482 cx.assert_excerpts_with_selections(indoc! {"
22483 [EXCERPT]
22484 [FOLDED]
22485 [EXCERPT]
22486 a1
22487 b1
22488 [EXCERPT]
22489 ˇ[FOLDED]
22490 [EXCERPT]
22491 [FOLDED]
22492 "
22493 });
22494 cx.simulate_keystroke("up");
22495 cx.assert_excerpts_with_selections(indoc! {"
22496 [EXCERPT]
22497 [FOLDED]
22498 [EXCERPT]
22499 a1
22500 b1
22501 ˇ[EXCERPT]
22502 [FOLDED]
22503 [EXCERPT]
22504 [FOLDED]
22505 "
22506 });
22507 cx.simulate_keystroke("up");
22508 cx.assert_excerpts_with_selections(indoc! {"
22509 [EXCERPT]
22510 [FOLDED]
22511 [EXCERPT]
22512 a1
22513 ˇb1
22514 [EXCERPT]
22515 [FOLDED]
22516 [EXCERPT]
22517 [FOLDED]
22518 "
22519 });
22520 cx.simulate_keystroke("up");
22521 cx.assert_excerpts_with_selections(indoc! {"
22522 [EXCERPT]
22523 [FOLDED]
22524 [EXCERPT]
22525 ˇa1
22526 b1
22527 [EXCERPT]
22528 [FOLDED]
22529 [EXCERPT]
22530 [FOLDED]
22531 "
22532 });
22533 for _ in 0..5 {
22534 cx.simulate_keystroke("up");
22535 cx.assert_excerpts_with_selections(indoc! {"
22536 [EXCERPT]
22537 ˇ[FOLDED]
22538 [EXCERPT]
22539 a1
22540 b1
22541 [EXCERPT]
22542 [FOLDED]
22543 [EXCERPT]
22544 [FOLDED]
22545 "
22546 });
22547 }
22548}
22549
22550#[gpui::test]
22551async fn test_edit_prediction_text(cx: &mut TestAppContext) {
22552 init_test(cx, |_| {});
22553
22554 // Simple insertion
22555 assert_highlighted_edits(
22556 "Hello, world!",
22557 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
22558 true,
22559 cx,
22560 |highlighted_edits, cx| {
22561 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
22562 assert_eq!(highlighted_edits.highlights.len(), 1);
22563 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
22564 assert_eq!(
22565 highlighted_edits.highlights[0].1.background_color,
22566 Some(cx.theme().status().created_background)
22567 );
22568 },
22569 )
22570 .await;
22571
22572 // Replacement
22573 assert_highlighted_edits(
22574 "This is a test.",
22575 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
22576 false,
22577 cx,
22578 |highlighted_edits, cx| {
22579 assert_eq!(highlighted_edits.text, "That is a test.");
22580 assert_eq!(highlighted_edits.highlights.len(), 1);
22581 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
22582 assert_eq!(
22583 highlighted_edits.highlights[0].1.background_color,
22584 Some(cx.theme().status().created_background)
22585 );
22586 },
22587 )
22588 .await;
22589
22590 // Multiple edits
22591 assert_highlighted_edits(
22592 "Hello, world!",
22593 vec![
22594 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
22595 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
22596 ],
22597 false,
22598 cx,
22599 |highlighted_edits, cx| {
22600 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
22601 assert_eq!(highlighted_edits.highlights.len(), 2);
22602 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
22603 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
22604 assert_eq!(
22605 highlighted_edits.highlights[0].1.background_color,
22606 Some(cx.theme().status().created_background)
22607 );
22608 assert_eq!(
22609 highlighted_edits.highlights[1].1.background_color,
22610 Some(cx.theme().status().created_background)
22611 );
22612 },
22613 )
22614 .await;
22615
22616 // Multiple lines with edits
22617 assert_highlighted_edits(
22618 "First line\nSecond line\nThird line\nFourth line",
22619 vec![
22620 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
22621 (
22622 Point::new(2, 0)..Point::new(2, 10),
22623 "New third line".to_string(),
22624 ),
22625 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
22626 ],
22627 false,
22628 cx,
22629 |highlighted_edits, cx| {
22630 assert_eq!(
22631 highlighted_edits.text,
22632 "Second modified\nNew third line\nFourth updated line"
22633 );
22634 assert_eq!(highlighted_edits.highlights.len(), 3);
22635 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
22636 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
22637 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
22638 for highlight in &highlighted_edits.highlights {
22639 assert_eq!(
22640 highlight.1.background_color,
22641 Some(cx.theme().status().created_background)
22642 );
22643 }
22644 },
22645 )
22646 .await;
22647}
22648
22649#[gpui::test]
22650async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
22651 init_test(cx, |_| {});
22652
22653 // Deletion
22654 assert_highlighted_edits(
22655 "Hello, world!",
22656 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
22657 true,
22658 cx,
22659 |highlighted_edits, cx| {
22660 assert_eq!(highlighted_edits.text, "Hello, world!");
22661 assert_eq!(highlighted_edits.highlights.len(), 1);
22662 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
22663 assert_eq!(
22664 highlighted_edits.highlights[0].1.background_color,
22665 Some(cx.theme().status().deleted_background)
22666 );
22667 },
22668 )
22669 .await;
22670
22671 // Insertion
22672 assert_highlighted_edits(
22673 "Hello, world!",
22674 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
22675 true,
22676 cx,
22677 |highlighted_edits, cx| {
22678 assert_eq!(highlighted_edits.highlights.len(), 1);
22679 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
22680 assert_eq!(
22681 highlighted_edits.highlights[0].1.background_color,
22682 Some(cx.theme().status().created_background)
22683 );
22684 },
22685 )
22686 .await;
22687}
22688
22689async fn assert_highlighted_edits(
22690 text: &str,
22691 edits: Vec<(Range<Point>, String)>,
22692 include_deletions: bool,
22693 cx: &mut TestAppContext,
22694 assertion_fn: impl Fn(HighlightedText, &App),
22695) {
22696 let window = cx.add_window(|window, cx| {
22697 let buffer = MultiBuffer::build_simple(text, cx);
22698 Editor::new(EditorMode::full(), buffer, None, window, cx)
22699 });
22700 let cx = &mut VisualTestContext::from_window(*window, cx);
22701
22702 let (buffer, snapshot) = window
22703 .update(cx, |editor, _window, cx| {
22704 (
22705 editor.buffer().clone(),
22706 editor.buffer().read(cx).snapshot(cx),
22707 )
22708 })
22709 .unwrap();
22710
22711 let edits = edits
22712 .into_iter()
22713 .map(|(range, edit)| {
22714 (
22715 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
22716 edit,
22717 )
22718 })
22719 .collect::<Vec<_>>();
22720
22721 let text_anchor_edits = edits
22722 .clone()
22723 .into_iter()
22724 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
22725 .collect::<Vec<_>>();
22726
22727 let edit_preview = window
22728 .update(cx, |_, _window, cx| {
22729 buffer
22730 .read(cx)
22731 .as_singleton()
22732 .unwrap()
22733 .read(cx)
22734 .preview_edits(text_anchor_edits.into(), cx)
22735 })
22736 .unwrap()
22737 .await;
22738
22739 cx.update(|_window, cx| {
22740 let highlighted_edits = edit_prediction_edit_text(
22741 snapshot.as_singleton().unwrap().2,
22742 &edits,
22743 &edit_preview,
22744 include_deletions,
22745 cx,
22746 );
22747 assertion_fn(highlighted_edits, cx)
22748 });
22749}
22750
22751#[track_caller]
22752fn assert_breakpoint(
22753 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
22754 path: &Arc<Path>,
22755 expected: Vec<(u32, Breakpoint)>,
22756) {
22757 if expected.is_empty() {
22758 assert!(!breakpoints.contains_key(path), "{}", path.display());
22759 } else {
22760 let mut breakpoint = breakpoints
22761 .get(path)
22762 .unwrap()
22763 .iter()
22764 .map(|breakpoint| {
22765 (
22766 breakpoint.row,
22767 Breakpoint {
22768 message: breakpoint.message.clone(),
22769 state: breakpoint.state,
22770 condition: breakpoint.condition.clone(),
22771 hit_condition: breakpoint.hit_condition.clone(),
22772 },
22773 )
22774 })
22775 .collect::<Vec<_>>();
22776
22777 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
22778
22779 assert_eq!(expected, breakpoint);
22780 }
22781}
22782
22783fn add_log_breakpoint_at_cursor(
22784 editor: &mut Editor,
22785 log_message: &str,
22786 window: &mut Window,
22787 cx: &mut Context<Editor>,
22788) {
22789 let (anchor, bp) = editor
22790 .breakpoints_at_cursors(window, cx)
22791 .first()
22792 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
22793 .unwrap_or_else(|| {
22794 let snapshot = editor.snapshot(window, cx);
22795 let cursor_position: Point =
22796 editor.selections.newest(&snapshot.display_snapshot).head();
22797
22798 let breakpoint_position = snapshot
22799 .buffer_snapshot()
22800 .anchor_before(Point::new(cursor_position.row, 0));
22801
22802 (breakpoint_position, Breakpoint::new_log(log_message))
22803 });
22804
22805 editor.edit_breakpoint_at_anchor(
22806 anchor,
22807 bp,
22808 BreakpointEditAction::EditLogMessage(log_message.into()),
22809 cx,
22810 );
22811}
22812
22813#[gpui::test]
22814async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
22815 init_test(cx, |_| {});
22816
22817 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22818 let fs = FakeFs::new(cx.executor());
22819 fs.insert_tree(
22820 path!("/a"),
22821 json!({
22822 "main.rs": sample_text,
22823 }),
22824 )
22825 .await;
22826 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22827 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22828 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22829
22830 let fs = FakeFs::new(cx.executor());
22831 fs.insert_tree(
22832 path!("/a"),
22833 json!({
22834 "main.rs": sample_text,
22835 }),
22836 )
22837 .await;
22838 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22839 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22840 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22841 let worktree_id = workspace
22842 .update(cx, |workspace, _window, cx| {
22843 workspace.project().update(cx, |project, cx| {
22844 project.worktrees(cx).next().unwrap().read(cx).id()
22845 })
22846 })
22847 .unwrap();
22848
22849 let buffer = project
22850 .update(cx, |project, cx| {
22851 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22852 })
22853 .await
22854 .unwrap();
22855
22856 let (editor, cx) = cx.add_window_view(|window, cx| {
22857 Editor::new(
22858 EditorMode::full(),
22859 MultiBuffer::build_from_buffer(buffer, cx),
22860 Some(project.clone()),
22861 window,
22862 cx,
22863 )
22864 });
22865
22866 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22867 let abs_path = project.read_with(cx, |project, cx| {
22868 project
22869 .absolute_path(&project_path, cx)
22870 .map(Arc::from)
22871 .unwrap()
22872 });
22873
22874 // assert we can add breakpoint on the first line
22875 editor.update_in(cx, |editor, window, cx| {
22876 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22877 editor.move_to_end(&MoveToEnd, window, cx);
22878 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22879 });
22880
22881 let breakpoints = editor.update(cx, |editor, cx| {
22882 editor
22883 .breakpoint_store()
22884 .as_ref()
22885 .unwrap()
22886 .read(cx)
22887 .all_source_breakpoints(cx)
22888 });
22889
22890 assert_eq!(1, breakpoints.len());
22891 assert_breakpoint(
22892 &breakpoints,
22893 &abs_path,
22894 vec![
22895 (0, Breakpoint::new_standard()),
22896 (3, Breakpoint::new_standard()),
22897 ],
22898 );
22899
22900 editor.update_in(cx, |editor, window, cx| {
22901 editor.move_to_beginning(&MoveToBeginning, window, cx);
22902 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22903 });
22904
22905 let breakpoints = editor.update(cx, |editor, cx| {
22906 editor
22907 .breakpoint_store()
22908 .as_ref()
22909 .unwrap()
22910 .read(cx)
22911 .all_source_breakpoints(cx)
22912 });
22913
22914 assert_eq!(1, breakpoints.len());
22915 assert_breakpoint(
22916 &breakpoints,
22917 &abs_path,
22918 vec![(3, Breakpoint::new_standard())],
22919 );
22920
22921 editor.update_in(cx, |editor, window, cx| {
22922 editor.move_to_end(&MoveToEnd, window, cx);
22923 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22924 });
22925
22926 let breakpoints = editor.update(cx, |editor, cx| {
22927 editor
22928 .breakpoint_store()
22929 .as_ref()
22930 .unwrap()
22931 .read(cx)
22932 .all_source_breakpoints(cx)
22933 });
22934
22935 assert_eq!(0, breakpoints.len());
22936 assert_breakpoint(&breakpoints, &abs_path, vec![]);
22937}
22938
22939#[gpui::test]
22940async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
22941 init_test(cx, |_| {});
22942
22943 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22944
22945 let fs = FakeFs::new(cx.executor());
22946 fs.insert_tree(
22947 path!("/a"),
22948 json!({
22949 "main.rs": sample_text,
22950 }),
22951 )
22952 .await;
22953 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22954 let (workspace, cx) =
22955 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22956
22957 let worktree_id = workspace.update(cx, |workspace, cx| {
22958 workspace.project().update(cx, |project, cx| {
22959 project.worktrees(cx).next().unwrap().read(cx).id()
22960 })
22961 });
22962
22963 let buffer = project
22964 .update(cx, |project, cx| {
22965 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22966 })
22967 .await
22968 .unwrap();
22969
22970 let (editor, cx) = cx.add_window_view(|window, cx| {
22971 Editor::new(
22972 EditorMode::full(),
22973 MultiBuffer::build_from_buffer(buffer, cx),
22974 Some(project.clone()),
22975 window,
22976 cx,
22977 )
22978 });
22979
22980 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22981 let abs_path = project.read_with(cx, |project, cx| {
22982 project
22983 .absolute_path(&project_path, cx)
22984 .map(Arc::from)
22985 .unwrap()
22986 });
22987
22988 editor.update_in(cx, |editor, window, cx| {
22989 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
22990 });
22991
22992 let breakpoints = editor.update(cx, |editor, cx| {
22993 editor
22994 .breakpoint_store()
22995 .as_ref()
22996 .unwrap()
22997 .read(cx)
22998 .all_source_breakpoints(cx)
22999 });
23000
23001 assert_breakpoint(
23002 &breakpoints,
23003 &abs_path,
23004 vec![(0, Breakpoint::new_log("hello world"))],
23005 );
23006
23007 // Removing a log message from a log breakpoint should remove it
23008 editor.update_in(cx, |editor, window, cx| {
23009 add_log_breakpoint_at_cursor(editor, "", window, cx);
23010 });
23011
23012 let breakpoints = editor.update(cx, |editor, cx| {
23013 editor
23014 .breakpoint_store()
23015 .as_ref()
23016 .unwrap()
23017 .read(cx)
23018 .all_source_breakpoints(cx)
23019 });
23020
23021 assert_breakpoint(&breakpoints, &abs_path, vec![]);
23022
23023 editor.update_in(cx, |editor, window, cx| {
23024 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23025 editor.move_to_end(&MoveToEnd, window, cx);
23026 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23027 // Not adding a log message to a standard breakpoint shouldn't remove it
23028 add_log_breakpoint_at_cursor(editor, "", window, cx);
23029 });
23030
23031 let breakpoints = editor.update(cx, |editor, cx| {
23032 editor
23033 .breakpoint_store()
23034 .as_ref()
23035 .unwrap()
23036 .read(cx)
23037 .all_source_breakpoints(cx)
23038 });
23039
23040 assert_breakpoint(
23041 &breakpoints,
23042 &abs_path,
23043 vec![
23044 (0, Breakpoint::new_standard()),
23045 (3, Breakpoint::new_standard()),
23046 ],
23047 );
23048
23049 editor.update_in(cx, |editor, window, cx| {
23050 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
23051 });
23052
23053 let breakpoints = editor.update(cx, |editor, cx| {
23054 editor
23055 .breakpoint_store()
23056 .as_ref()
23057 .unwrap()
23058 .read(cx)
23059 .all_source_breakpoints(cx)
23060 });
23061
23062 assert_breakpoint(
23063 &breakpoints,
23064 &abs_path,
23065 vec![
23066 (0, Breakpoint::new_standard()),
23067 (3, Breakpoint::new_log("hello world")),
23068 ],
23069 );
23070
23071 editor.update_in(cx, |editor, window, cx| {
23072 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
23073 });
23074
23075 let breakpoints = editor.update(cx, |editor, cx| {
23076 editor
23077 .breakpoint_store()
23078 .as_ref()
23079 .unwrap()
23080 .read(cx)
23081 .all_source_breakpoints(cx)
23082 });
23083
23084 assert_breakpoint(
23085 &breakpoints,
23086 &abs_path,
23087 vec![
23088 (0, Breakpoint::new_standard()),
23089 (3, Breakpoint::new_log("hello Earth!!")),
23090 ],
23091 );
23092}
23093
23094/// This also tests that Editor::breakpoint_at_cursor_head is working properly
23095/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
23096/// or when breakpoints were placed out of order. This tests for a regression too
23097#[gpui::test]
23098async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
23099 init_test(cx, |_| {});
23100
23101 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
23102 let fs = FakeFs::new(cx.executor());
23103 fs.insert_tree(
23104 path!("/a"),
23105 json!({
23106 "main.rs": sample_text,
23107 }),
23108 )
23109 .await;
23110 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23111 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23112 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23113
23114 let fs = FakeFs::new(cx.executor());
23115 fs.insert_tree(
23116 path!("/a"),
23117 json!({
23118 "main.rs": sample_text,
23119 }),
23120 )
23121 .await;
23122 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23123 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23124 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23125 let worktree_id = workspace
23126 .update(cx, |workspace, _window, cx| {
23127 workspace.project().update(cx, |project, cx| {
23128 project.worktrees(cx).next().unwrap().read(cx).id()
23129 })
23130 })
23131 .unwrap();
23132
23133 let buffer = project
23134 .update(cx, |project, cx| {
23135 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23136 })
23137 .await
23138 .unwrap();
23139
23140 let (editor, cx) = cx.add_window_view(|window, cx| {
23141 Editor::new(
23142 EditorMode::full(),
23143 MultiBuffer::build_from_buffer(buffer, cx),
23144 Some(project.clone()),
23145 window,
23146 cx,
23147 )
23148 });
23149
23150 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
23151 let abs_path = project.read_with(cx, |project, cx| {
23152 project
23153 .absolute_path(&project_path, cx)
23154 .map(Arc::from)
23155 .unwrap()
23156 });
23157
23158 // assert we can add breakpoint on the first line
23159 editor.update_in(cx, |editor, window, cx| {
23160 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23161 editor.move_to_end(&MoveToEnd, window, cx);
23162 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23163 editor.move_up(&MoveUp, window, cx);
23164 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23165 });
23166
23167 let breakpoints = editor.update(cx, |editor, cx| {
23168 editor
23169 .breakpoint_store()
23170 .as_ref()
23171 .unwrap()
23172 .read(cx)
23173 .all_source_breakpoints(cx)
23174 });
23175
23176 assert_eq!(1, breakpoints.len());
23177 assert_breakpoint(
23178 &breakpoints,
23179 &abs_path,
23180 vec![
23181 (0, Breakpoint::new_standard()),
23182 (2, Breakpoint::new_standard()),
23183 (3, Breakpoint::new_standard()),
23184 ],
23185 );
23186
23187 editor.update_in(cx, |editor, window, cx| {
23188 editor.move_to_beginning(&MoveToBeginning, window, cx);
23189 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23190 editor.move_to_end(&MoveToEnd, window, cx);
23191 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23192 // Disabling a breakpoint that doesn't exist should do nothing
23193 editor.move_up(&MoveUp, window, cx);
23194 editor.move_up(&MoveUp, window, cx);
23195 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23196 });
23197
23198 let breakpoints = editor.update(cx, |editor, cx| {
23199 editor
23200 .breakpoint_store()
23201 .as_ref()
23202 .unwrap()
23203 .read(cx)
23204 .all_source_breakpoints(cx)
23205 });
23206
23207 let disable_breakpoint = {
23208 let mut bp = Breakpoint::new_standard();
23209 bp.state = BreakpointState::Disabled;
23210 bp
23211 };
23212
23213 assert_eq!(1, breakpoints.len());
23214 assert_breakpoint(
23215 &breakpoints,
23216 &abs_path,
23217 vec![
23218 (0, disable_breakpoint.clone()),
23219 (2, Breakpoint::new_standard()),
23220 (3, disable_breakpoint.clone()),
23221 ],
23222 );
23223
23224 editor.update_in(cx, |editor, window, cx| {
23225 editor.move_to_beginning(&MoveToBeginning, window, cx);
23226 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
23227 editor.move_to_end(&MoveToEnd, window, cx);
23228 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
23229 editor.move_up(&MoveUp, window, cx);
23230 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23231 });
23232
23233 let breakpoints = editor.update(cx, |editor, cx| {
23234 editor
23235 .breakpoint_store()
23236 .as_ref()
23237 .unwrap()
23238 .read(cx)
23239 .all_source_breakpoints(cx)
23240 });
23241
23242 assert_eq!(1, breakpoints.len());
23243 assert_breakpoint(
23244 &breakpoints,
23245 &abs_path,
23246 vec![
23247 (0, Breakpoint::new_standard()),
23248 (2, disable_breakpoint),
23249 (3, Breakpoint::new_standard()),
23250 ],
23251 );
23252}
23253
23254#[gpui::test]
23255async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
23256 init_test(cx, |_| {});
23257 let capabilities = lsp::ServerCapabilities {
23258 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
23259 prepare_provider: Some(true),
23260 work_done_progress_options: Default::default(),
23261 })),
23262 ..Default::default()
23263 };
23264 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23265
23266 cx.set_state(indoc! {"
23267 struct Fˇoo {}
23268 "});
23269
23270 cx.update_editor(|editor, _, cx| {
23271 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23272 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23273 editor.highlight_background::<DocumentHighlightRead>(
23274 &[highlight_range],
23275 |theme| theme.colors().editor_document_highlight_read_background,
23276 cx,
23277 );
23278 });
23279
23280 let mut prepare_rename_handler = cx
23281 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
23282 move |_, _, _| async move {
23283 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
23284 start: lsp::Position {
23285 line: 0,
23286 character: 7,
23287 },
23288 end: lsp::Position {
23289 line: 0,
23290 character: 10,
23291 },
23292 })))
23293 },
23294 );
23295 let prepare_rename_task = cx
23296 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23297 .expect("Prepare rename was not started");
23298 prepare_rename_handler.next().await.unwrap();
23299 prepare_rename_task.await.expect("Prepare rename failed");
23300
23301 let mut rename_handler =
23302 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23303 let edit = lsp::TextEdit {
23304 range: lsp::Range {
23305 start: lsp::Position {
23306 line: 0,
23307 character: 7,
23308 },
23309 end: lsp::Position {
23310 line: 0,
23311 character: 10,
23312 },
23313 },
23314 new_text: "FooRenamed".to_string(),
23315 };
23316 Ok(Some(lsp::WorkspaceEdit::new(
23317 // Specify the same edit twice
23318 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
23319 )))
23320 });
23321 let rename_task = cx
23322 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23323 .expect("Confirm rename was not started");
23324 rename_handler.next().await.unwrap();
23325 rename_task.await.expect("Confirm rename failed");
23326 cx.run_until_parked();
23327
23328 // Despite two edits, only one is actually applied as those are identical
23329 cx.assert_editor_state(indoc! {"
23330 struct FooRenamedˇ {}
23331 "});
23332}
23333
23334#[gpui::test]
23335async fn test_rename_without_prepare(cx: &mut TestAppContext) {
23336 init_test(cx, |_| {});
23337 // These capabilities indicate that the server does not support prepare rename.
23338 let capabilities = lsp::ServerCapabilities {
23339 rename_provider: Some(lsp::OneOf::Left(true)),
23340 ..Default::default()
23341 };
23342 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23343
23344 cx.set_state(indoc! {"
23345 struct Fˇoo {}
23346 "});
23347
23348 cx.update_editor(|editor, _window, cx| {
23349 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23350 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23351 editor.highlight_background::<DocumentHighlightRead>(
23352 &[highlight_range],
23353 |theme| theme.colors().editor_document_highlight_read_background,
23354 cx,
23355 );
23356 });
23357
23358 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23359 .expect("Prepare rename was not started")
23360 .await
23361 .expect("Prepare rename failed");
23362
23363 let mut rename_handler =
23364 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23365 let edit = lsp::TextEdit {
23366 range: lsp::Range {
23367 start: lsp::Position {
23368 line: 0,
23369 character: 7,
23370 },
23371 end: lsp::Position {
23372 line: 0,
23373 character: 10,
23374 },
23375 },
23376 new_text: "FooRenamed".to_string(),
23377 };
23378 Ok(Some(lsp::WorkspaceEdit::new(
23379 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
23380 )))
23381 });
23382 let rename_task = cx
23383 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23384 .expect("Confirm rename was not started");
23385 rename_handler.next().await.unwrap();
23386 rename_task.await.expect("Confirm rename failed");
23387 cx.run_until_parked();
23388
23389 // Correct range is renamed, as `surrounding_word` is used to find it.
23390 cx.assert_editor_state(indoc! {"
23391 struct FooRenamedˇ {}
23392 "});
23393}
23394
23395#[gpui::test]
23396async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
23397 init_test(cx, |_| {});
23398 let mut cx = EditorTestContext::new(cx).await;
23399
23400 let language = Arc::new(
23401 Language::new(
23402 LanguageConfig::default(),
23403 Some(tree_sitter_html::LANGUAGE.into()),
23404 )
23405 .with_brackets_query(
23406 r#"
23407 ("<" @open "/>" @close)
23408 ("</" @open ">" @close)
23409 ("<" @open ">" @close)
23410 ("\"" @open "\"" @close)
23411 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
23412 "#,
23413 )
23414 .unwrap(),
23415 );
23416 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23417
23418 cx.set_state(indoc! {"
23419 <span>ˇ</span>
23420 "});
23421 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23422 cx.assert_editor_state(indoc! {"
23423 <span>
23424 ˇ
23425 </span>
23426 "});
23427
23428 cx.set_state(indoc! {"
23429 <span><span></span>ˇ</span>
23430 "});
23431 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23432 cx.assert_editor_state(indoc! {"
23433 <span><span></span>
23434 ˇ</span>
23435 "});
23436
23437 cx.set_state(indoc! {"
23438 <span>ˇ
23439 </span>
23440 "});
23441 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23442 cx.assert_editor_state(indoc! {"
23443 <span>
23444 ˇ
23445 </span>
23446 "});
23447}
23448
23449#[gpui::test(iterations = 10)]
23450async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
23451 init_test(cx, |_| {});
23452
23453 let fs = FakeFs::new(cx.executor());
23454 fs.insert_tree(
23455 path!("/dir"),
23456 json!({
23457 "a.ts": "a",
23458 }),
23459 )
23460 .await;
23461
23462 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
23463 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23464 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23465
23466 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23467 language_registry.add(Arc::new(Language::new(
23468 LanguageConfig {
23469 name: "TypeScript".into(),
23470 matcher: LanguageMatcher {
23471 path_suffixes: vec!["ts".to_string()],
23472 ..Default::default()
23473 },
23474 ..Default::default()
23475 },
23476 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
23477 )));
23478 let mut fake_language_servers = language_registry.register_fake_lsp(
23479 "TypeScript",
23480 FakeLspAdapter {
23481 capabilities: lsp::ServerCapabilities {
23482 code_lens_provider: Some(lsp::CodeLensOptions {
23483 resolve_provider: Some(true),
23484 }),
23485 execute_command_provider: Some(lsp::ExecuteCommandOptions {
23486 commands: vec!["_the/command".to_string()],
23487 ..lsp::ExecuteCommandOptions::default()
23488 }),
23489 ..lsp::ServerCapabilities::default()
23490 },
23491 ..FakeLspAdapter::default()
23492 },
23493 );
23494
23495 let editor = workspace
23496 .update(cx, |workspace, window, cx| {
23497 workspace.open_abs_path(
23498 PathBuf::from(path!("/dir/a.ts")),
23499 OpenOptions::default(),
23500 window,
23501 cx,
23502 )
23503 })
23504 .unwrap()
23505 .await
23506 .unwrap()
23507 .downcast::<Editor>()
23508 .unwrap();
23509 cx.executor().run_until_parked();
23510
23511 let fake_server = fake_language_servers.next().await.unwrap();
23512
23513 let buffer = editor.update(cx, |editor, cx| {
23514 editor
23515 .buffer()
23516 .read(cx)
23517 .as_singleton()
23518 .expect("have opened a single file by path")
23519 });
23520
23521 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
23522 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
23523 drop(buffer_snapshot);
23524 let actions = cx
23525 .update_window(*workspace, |_, window, cx| {
23526 project.code_actions(&buffer, anchor..anchor, window, cx)
23527 })
23528 .unwrap();
23529
23530 fake_server
23531 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23532 Ok(Some(vec![
23533 lsp::CodeLens {
23534 range: lsp::Range::default(),
23535 command: Some(lsp::Command {
23536 title: "Code lens command".to_owned(),
23537 command: "_the/command".to_owned(),
23538 arguments: None,
23539 }),
23540 data: None,
23541 },
23542 lsp::CodeLens {
23543 range: lsp::Range::default(),
23544 command: Some(lsp::Command {
23545 title: "Command not in capabilities".to_owned(),
23546 command: "not in capabilities".to_owned(),
23547 arguments: None,
23548 }),
23549 data: None,
23550 },
23551 lsp::CodeLens {
23552 range: lsp::Range {
23553 start: lsp::Position {
23554 line: 1,
23555 character: 1,
23556 },
23557 end: lsp::Position {
23558 line: 1,
23559 character: 1,
23560 },
23561 },
23562 command: Some(lsp::Command {
23563 title: "Command not in range".to_owned(),
23564 command: "_the/command".to_owned(),
23565 arguments: None,
23566 }),
23567 data: None,
23568 },
23569 ]))
23570 })
23571 .next()
23572 .await;
23573
23574 let actions = actions.await.unwrap();
23575 assert_eq!(
23576 actions.len(),
23577 1,
23578 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
23579 );
23580 let action = actions[0].clone();
23581 let apply = project.update(cx, |project, cx| {
23582 project.apply_code_action(buffer.clone(), action, true, cx)
23583 });
23584
23585 // Resolving the code action does not populate its edits. In absence of
23586 // edits, we must execute the given command.
23587 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
23588 |mut lens, _| async move {
23589 let lens_command = lens.command.as_mut().expect("should have a command");
23590 assert_eq!(lens_command.title, "Code lens command");
23591 lens_command.arguments = Some(vec![json!("the-argument")]);
23592 Ok(lens)
23593 },
23594 );
23595
23596 // While executing the command, the language server sends the editor
23597 // a `workspaceEdit` request.
23598 fake_server
23599 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
23600 let fake = fake_server.clone();
23601 move |params, _| {
23602 assert_eq!(params.command, "_the/command");
23603 let fake = fake.clone();
23604 async move {
23605 fake.server
23606 .request::<lsp::request::ApplyWorkspaceEdit>(
23607 lsp::ApplyWorkspaceEditParams {
23608 label: None,
23609 edit: lsp::WorkspaceEdit {
23610 changes: Some(
23611 [(
23612 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
23613 vec![lsp::TextEdit {
23614 range: lsp::Range::new(
23615 lsp::Position::new(0, 0),
23616 lsp::Position::new(0, 0),
23617 ),
23618 new_text: "X".into(),
23619 }],
23620 )]
23621 .into_iter()
23622 .collect(),
23623 ),
23624 ..lsp::WorkspaceEdit::default()
23625 },
23626 },
23627 )
23628 .await
23629 .into_response()
23630 .unwrap();
23631 Ok(Some(json!(null)))
23632 }
23633 }
23634 })
23635 .next()
23636 .await;
23637
23638 // Applying the code lens command returns a project transaction containing the edits
23639 // sent by the language server in its `workspaceEdit` request.
23640 let transaction = apply.await.unwrap();
23641 assert!(transaction.0.contains_key(&buffer));
23642 buffer.update(cx, |buffer, cx| {
23643 assert_eq!(buffer.text(), "Xa");
23644 buffer.undo(cx);
23645 assert_eq!(buffer.text(), "a");
23646 });
23647
23648 let actions_after_edits = cx
23649 .update_window(*workspace, |_, window, cx| {
23650 project.code_actions(&buffer, anchor..anchor, window, cx)
23651 })
23652 .unwrap()
23653 .await
23654 .unwrap();
23655 assert_eq!(
23656 actions, actions_after_edits,
23657 "For the same selection, same code lens actions should be returned"
23658 );
23659
23660 let _responses =
23661 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23662 panic!("No more code lens requests are expected");
23663 });
23664 editor.update_in(cx, |editor, window, cx| {
23665 editor.select_all(&SelectAll, window, cx);
23666 });
23667 cx.executor().run_until_parked();
23668 let new_actions = cx
23669 .update_window(*workspace, |_, window, cx| {
23670 project.code_actions(&buffer, anchor..anchor, window, cx)
23671 })
23672 .unwrap()
23673 .await
23674 .unwrap();
23675 assert_eq!(
23676 actions, new_actions,
23677 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
23678 );
23679}
23680
23681#[gpui::test]
23682async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
23683 init_test(cx, |_| {});
23684
23685 let fs = FakeFs::new(cx.executor());
23686 let main_text = r#"fn main() {
23687println!("1");
23688println!("2");
23689println!("3");
23690println!("4");
23691println!("5");
23692}"#;
23693 let lib_text = "mod foo {}";
23694 fs.insert_tree(
23695 path!("/a"),
23696 json!({
23697 "lib.rs": lib_text,
23698 "main.rs": main_text,
23699 }),
23700 )
23701 .await;
23702
23703 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23704 let (workspace, cx) =
23705 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23706 let worktree_id = workspace.update(cx, |workspace, cx| {
23707 workspace.project().update(cx, |project, cx| {
23708 project.worktrees(cx).next().unwrap().read(cx).id()
23709 })
23710 });
23711
23712 let expected_ranges = vec![
23713 Point::new(0, 0)..Point::new(0, 0),
23714 Point::new(1, 0)..Point::new(1, 1),
23715 Point::new(2, 0)..Point::new(2, 2),
23716 Point::new(3, 0)..Point::new(3, 3),
23717 ];
23718
23719 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23720 let editor_1 = workspace
23721 .update_in(cx, |workspace, window, cx| {
23722 workspace.open_path(
23723 (worktree_id, rel_path("main.rs")),
23724 Some(pane_1.downgrade()),
23725 true,
23726 window,
23727 cx,
23728 )
23729 })
23730 .unwrap()
23731 .await
23732 .downcast::<Editor>()
23733 .unwrap();
23734 pane_1.update(cx, |pane, cx| {
23735 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23736 open_editor.update(cx, |editor, cx| {
23737 assert_eq!(
23738 editor.display_text(cx),
23739 main_text,
23740 "Original main.rs text on initial open",
23741 );
23742 assert_eq!(
23743 editor
23744 .selections
23745 .all::<Point>(&editor.display_snapshot(cx))
23746 .into_iter()
23747 .map(|s| s.range())
23748 .collect::<Vec<_>>(),
23749 vec![Point::zero()..Point::zero()],
23750 "Default selections on initial open",
23751 );
23752 })
23753 });
23754 editor_1.update_in(cx, |editor, window, cx| {
23755 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23756 s.select_ranges(expected_ranges.clone());
23757 });
23758 });
23759
23760 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
23761 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
23762 });
23763 let editor_2 = workspace
23764 .update_in(cx, |workspace, window, cx| {
23765 workspace.open_path(
23766 (worktree_id, rel_path("main.rs")),
23767 Some(pane_2.downgrade()),
23768 true,
23769 window,
23770 cx,
23771 )
23772 })
23773 .unwrap()
23774 .await
23775 .downcast::<Editor>()
23776 .unwrap();
23777 pane_2.update(cx, |pane, cx| {
23778 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23779 open_editor.update(cx, |editor, cx| {
23780 assert_eq!(
23781 editor.display_text(cx),
23782 main_text,
23783 "Original main.rs text on initial open in another panel",
23784 );
23785 assert_eq!(
23786 editor
23787 .selections
23788 .all::<Point>(&editor.display_snapshot(cx))
23789 .into_iter()
23790 .map(|s| s.range())
23791 .collect::<Vec<_>>(),
23792 vec![Point::zero()..Point::zero()],
23793 "Default selections on initial open in another panel",
23794 );
23795 })
23796 });
23797
23798 editor_2.update_in(cx, |editor, window, cx| {
23799 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
23800 });
23801
23802 let _other_editor_1 = workspace
23803 .update_in(cx, |workspace, window, cx| {
23804 workspace.open_path(
23805 (worktree_id, rel_path("lib.rs")),
23806 Some(pane_1.downgrade()),
23807 true,
23808 window,
23809 cx,
23810 )
23811 })
23812 .unwrap()
23813 .await
23814 .downcast::<Editor>()
23815 .unwrap();
23816 pane_1
23817 .update_in(cx, |pane, window, cx| {
23818 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23819 })
23820 .await
23821 .unwrap();
23822 drop(editor_1);
23823 pane_1.update(cx, |pane, cx| {
23824 pane.active_item()
23825 .unwrap()
23826 .downcast::<Editor>()
23827 .unwrap()
23828 .update(cx, |editor, cx| {
23829 assert_eq!(
23830 editor.display_text(cx),
23831 lib_text,
23832 "Other file should be open and active",
23833 );
23834 });
23835 assert_eq!(pane.items().count(), 1, "No other editors should be open");
23836 });
23837
23838 let _other_editor_2 = workspace
23839 .update_in(cx, |workspace, window, cx| {
23840 workspace.open_path(
23841 (worktree_id, rel_path("lib.rs")),
23842 Some(pane_2.downgrade()),
23843 true,
23844 window,
23845 cx,
23846 )
23847 })
23848 .unwrap()
23849 .await
23850 .downcast::<Editor>()
23851 .unwrap();
23852 pane_2
23853 .update_in(cx, |pane, window, cx| {
23854 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23855 })
23856 .await
23857 .unwrap();
23858 drop(editor_2);
23859 pane_2.update(cx, |pane, cx| {
23860 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23861 open_editor.update(cx, |editor, cx| {
23862 assert_eq!(
23863 editor.display_text(cx),
23864 lib_text,
23865 "Other file should be open and active in another panel too",
23866 );
23867 });
23868 assert_eq!(
23869 pane.items().count(),
23870 1,
23871 "No other editors should be open in another pane",
23872 );
23873 });
23874
23875 let _editor_1_reopened = workspace
23876 .update_in(cx, |workspace, window, cx| {
23877 workspace.open_path(
23878 (worktree_id, rel_path("main.rs")),
23879 Some(pane_1.downgrade()),
23880 true,
23881 window,
23882 cx,
23883 )
23884 })
23885 .unwrap()
23886 .await
23887 .downcast::<Editor>()
23888 .unwrap();
23889 let _editor_2_reopened = workspace
23890 .update_in(cx, |workspace, window, cx| {
23891 workspace.open_path(
23892 (worktree_id, rel_path("main.rs")),
23893 Some(pane_2.downgrade()),
23894 true,
23895 window,
23896 cx,
23897 )
23898 })
23899 .unwrap()
23900 .await
23901 .downcast::<Editor>()
23902 .unwrap();
23903 pane_1.update(cx, |pane, cx| {
23904 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23905 open_editor.update(cx, |editor, cx| {
23906 assert_eq!(
23907 editor.display_text(cx),
23908 main_text,
23909 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
23910 );
23911 assert_eq!(
23912 editor
23913 .selections
23914 .all::<Point>(&editor.display_snapshot(cx))
23915 .into_iter()
23916 .map(|s| s.range())
23917 .collect::<Vec<_>>(),
23918 expected_ranges,
23919 "Previous editor in the 1st panel had selections and should get them restored on reopen",
23920 );
23921 })
23922 });
23923 pane_2.update(cx, |pane, cx| {
23924 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23925 open_editor.update(cx, |editor, cx| {
23926 assert_eq!(
23927 editor.display_text(cx),
23928 r#"fn main() {
23929⋯rintln!("1");
23930⋯intln!("2");
23931⋯ntln!("3");
23932println!("4");
23933println!("5");
23934}"#,
23935 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
23936 );
23937 assert_eq!(
23938 editor
23939 .selections
23940 .all::<Point>(&editor.display_snapshot(cx))
23941 .into_iter()
23942 .map(|s| s.range())
23943 .collect::<Vec<_>>(),
23944 vec![Point::zero()..Point::zero()],
23945 "Previous editor in the 2nd pane had no selections changed hence should restore none",
23946 );
23947 })
23948 });
23949}
23950
23951#[gpui::test]
23952async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
23953 init_test(cx, |_| {});
23954
23955 let fs = FakeFs::new(cx.executor());
23956 let main_text = r#"fn main() {
23957println!("1");
23958println!("2");
23959println!("3");
23960println!("4");
23961println!("5");
23962}"#;
23963 let lib_text = "mod foo {}";
23964 fs.insert_tree(
23965 path!("/a"),
23966 json!({
23967 "lib.rs": lib_text,
23968 "main.rs": main_text,
23969 }),
23970 )
23971 .await;
23972
23973 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23974 let (workspace, cx) =
23975 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23976 let worktree_id = workspace.update(cx, |workspace, cx| {
23977 workspace.project().update(cx, |project, cx| {
23978 project.worktrees(cx).next().unwrap().read(cx).id()
23979 })
23980 });
23981
23982 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23983 let editor = workspace
23984 .update_in(cx, |workspace, window, cx| {
23985 workspace.open_path(
23986 (worktree_id, rel_path("main.rs")),
23987 Some(pane.downgrade()),
23988 true,
23989 window,
23990 cx,
23991 )
23992 })
23993 .unwrap()
23994 .await
23995 .downcast::<Editor>()
23996 .unwrap();
23997 pane.update(cx, |pane, cx| {
23998 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23999 open_editor.update(cx, |editor, cx| {
24000 assert_eq!(
24001 editor.display_text(cx),
24002 main_text,
24003 "Original main.rs text on initial open",
24004 );
24005 })
24006 });
24007 editor.update_in(cx, |editor, window, cx| {
24008 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
24009 });
24010
24011 cx.update_global(|store: &mut SettingsStore, cx| {
24012 store.update_user_settings(cx, |s| {
24013 s.workspace.restore_on_file_reopen = Some(false);
24014 });
24015 });
24016 editor.update_in(cx, |editor, window, cx| {
24017 editor.fold_ranges(
24018 vec![
24019 Point::new(1, 0)..Point::new(1, 1),
24020 Point::new(2, 0)..Point::new(2, 2),
24021 Point::new(3, 0)..Point::new(3, 3),
24022 ],
24023 false,
24024 window,
24025 cx,
24026 );
24027 });
24028 pane.update_in(cx, |pane, window, cx| {
24029 pane.close_all_items(&CloseAllItems::default(), window, cx)
24030 })
24031 .await
24032 .unwrap();
24033 pane.update(cx, |pane, _| {
24034 assert!(pane.active_item().is_none());
24035 });
24036 cx.update_global(|store: &mut SettingsStore, cx| {
24037 store.update_user_settings(cx, |s| {
24038 s.workspace.restore_on_file_reopen = Some(true);
24039 });
24040 });
24041
24042 let _editor_reopened = workspace
24043 .update_in(cx, |workspace, window, cx| {
24044 workspace.open_path(
24045 (worktree_id, rel_path("main.rs")),
24046 Some(pane.downgrade()),
24047 true,
24048 window,
24049 cx,
24050 )
24051 })
24052 .unwrap()
24053 .await
24054 .downcast::<Editor>()
24055 .unwrap();
24056 pane.update(cx, |pane, cx| {
24057 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24058 open_editor.update(cx, |editor, cx| {
24059 assert_eq!(
24060 editor.display_text(cx),
24061 main_text,
24062 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
24063 );
24064 })
24065 });
24066}
24067
24068#[gpui::test]
24069async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
24070 struct EmptyModalView {
24071 focus_handle: gpui::FocusHandle,
24072 }
24073 impl EventEmitter<DismissEvent> for EmptyModalView {}
24074 impl Render for EmptyModalView {
24075 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
24076 div()
24077 }
24078 }
24079 impl Focusable for EmptyModalView {
24080 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
24081 self.focus_handle.clone()
24082 }
24083 }
24084 impl workspace::ModalView for EmptyModalView {}
24085 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
24086 EmptyModalView {
24087 focus_handle: cx.focus_handle(),
24088 }
24089 }
24090
24091 init_test(cx, |_| {});
24092
24093 let fs = FakeFs::new(cx.executor());
24094 let project = Project::test(fs, [], cx).await;
24095 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24096 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
24097 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24098 let editor = cx.new_window_entity(|window, cx| {
24099 Editor::new(
24100 EditorMode::full(),
24101 buffer,
24102 Some(project.clone()),
24103 window,
24104 cx,
24105 )
24106 });
24107 workspace
24108 .update(cx, |workspace, window, cx| {
24109 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
24110 })
24111 .unwrap();
24112 editor.update_in(cx, |editor, window, cx| {
24113 editor.open_context_menu(&OpenContextMenu, window, cx);
24114 assert!(editor.mouse_context_menu.is_some());
24115 });
24116 workspace
24117 .update(cx, |workspace, window, cx| {
24118 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
24119 })
24120 .unwrap();
24121 cx.read(|cx| {
24122 assert!(editor.read(cx).mouse_context_menu.is_none());
24123 });
24124}
24125
24126fn set_linked_edit_ranges(
24127 opening: (Point, Point),
24128 closing: (Point, Point),
24129 editor: &mut Editor,
24130 cx: &mut Context<Editor>,
24131) {
24132 let Some((buffer, _)) = editor
24133 .buffer
24134 .read(cx)
24135 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
24136 else {
24137 panic!("Failed to get buffer for selection position");
24138 };
24139 let buffer = buffer.read(cx);
24140 let buffer_id = buffer.remote_id();
24141 let opening_range = buffer.anchor_before(opening.0)..buffer.anchor_after(opening.1);
24142 let closing_range = buffer.anchor_before(closing.0)..buffer.anchor_after(closing.1);
24143 let mut linked_ranges = HashMap::default();
24144 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
24145 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
24146}
24147
24148#[gpui::test]
24149async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
24150 init_test(cx, |_| {});
24151
24152 let fs = FakeFs::new(cx.executor());
24153 fs.insert_file(path!("/file.html"), Default::default())
24154 .await;
24155
24156 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
24157
24158 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24159 let html_language = Arc::new(Language::new(
24160 LanguageConfig {
24161 name: "HTML".into(),
24162 matcher: LanguageMatcher {
24163 path_suffixes: vec!["html".to_string()],
24164 ..LanguageMatcher::default()
24165 },
24166 brackets: BracketPairConfig {
24167 pairs: vec![BracketPair {
24168 start: "<".into(),
24169 end: ">".into(),
24170 close: true,
24171 ..Default::default()
24172 }],
24173 ..Default::default()
24174 },
24175 ..Default::default()
24176 },
24177 Some(tree_sitter_html::LANGUAGE.into()),
24178 ));
24179 language_registry.add(html_language);
24180 let mut fake_servers = language_registry.register_fake_lsp(
24181 "HTML",
24182 FakeLspAdapter {
24183 capabilities: lsp::ServerCapabilities {
24184 completion_provider: Some(lsp::CompletionOptions {
24185 resolve_provider: Some(true),
24186 ..Default::default()
24187 }),
24188 ..Default::default()
24189 },
24190 ..Default::default()
24191 },
24192 );
24193
24194 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24195 let cx = &mut VisualTestContext::from_window(*workspace, cx);
24196
24197 let worktree_id = workspace
24198 .update(cx, |workspace, _window, cx| {
24199 workspace.project().update(cx, |project, cx| {
24200 project.worktrees(cx).next().unwrap().read(cx).id()
24201 })
24202 })
24203 .unwrap();
24204 project
24205 .update(cx, |project, cx| {
24206 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
24207 })
24208 .await
24209 .unwrap();
24210 let editor = workspace
24211 .update(cx, |workspace, window, cx| {
24212 workspace.open_path((worktree_id, rel_path("file.html")), None, true, window, cx)
24213 })
24214 .unwrap()
24215 .await
24216 .unwrap()
24217 .downcast::<Editor>()
24218 .unwrap();
24219
24220 let fake_server = fake_servers.next().await.unwrap();
24221 editor.update_in(cx, |editor, window, cx| {
24222 editor.set_text("<ad></ad>", window, cx);
24223 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
24224 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
24225 });
24226 set_linked_edit_ranges(
24227 (Point::new(0, 1), Point::new(0, 3)),
24228 (Point::new(0, 6), Point::new(0, 8)),
24229 editor,
24230 cx,
24231 );
24232 });
24233 let mut completion_handle =
24234 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
24235 Ok(Some(lsp::CompletionResponse::Array(vec![
24236 lsp::CompletionItem {
24237 label: "head".to_string(),
24238 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
24239 lsp::InsertReplaceEdit {
24240 new_text: "head".to_string(),
24241 insert: lsp::Range::new(
24242 lsp::Position::new(0, 1),
24243 lsp::Position::new(0, 3),
24244 ),
24245 replace: lsp::Range::new(
24246 lsp::Position::new(0, 1),
24247 lsp::Position::new(0, 3),
24248 ),
24249 },
24250 )),
24251 ..Default::default()
24252 },
24253 ])))
24254 });
24255 editor.update_in(cx, |editor, window, cx| {
24256 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
24257 });
24258 cx.run_until_parked();
24259 completion_handle.next().await.unwrap();
24260 editor.update(cx, |editor, _| {
24261 assert!(
24262 editor.context_menu_visible(),
24263 "Completion menu should be visible"
24264 );
24265 });
24266 editor.update_in(cx, |editor, window, cx| {
24267 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
24268 });
24269 cx.executor().run_until_parked();
24270 editor.update(cx, |editor, cx| {
24271 assert_eq!(editor.text(cx), "<head></head>");
24272 });
24273}
24274
24275#[gpui::test]
24276async fn test_linked_edits_on_typing_punctuation(cx: &mut TestAppContext) {
24277 init_test(cx, |_| {});
24278
24279 let mut cx = EditorTestContext::new(cx).await;
24280 let language = Arc::new(Language::new(
24281 LanguageConfig {
24282 name: "TSX".into(),
24283 matcher: LanguageMatcher {
24284 path_suffixes: vec!["tsx".to_string()],
24285 ..LanguageMatcher::default()
24286 },
24287 brackets: BracketPairConfig {
24288 pairs: vec![BracketPair {
24289 start: "<".into(),
24290 end: ">".into(),
24291 close: true,
24292 ..Default::default()
24293 }],
24294 ..Default::default()
24295 },
24296 linked_edit_characters: HashSet::from_iter(['.']),
24297 ..Default::default()
24298 },
24299 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
24300 ));
24301 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24302
24303 // Test typing > does not extend linked pair
24304 cx.set_state("<divˇ<div></div>");
24305 cx.update_editor(|editor, _, cx| {
24306 set_linked_edit_ranges(
24307 (Point::new(0, 1), Point::new(0, 4)),
24308 (Point::new(0, 11), Point::new(0, 14)),
24309 editor,
24310 cx,
24311 );
24312 });
24313 cx.update_editor(|editor, window, cx| {
24314 editor.handle_input(">", window, cx);
24315 });
24316 cx.assert_editor_state("<div>ˇ<div></div>");
24317
24318 // Test typing . do extend linked pair
24319 cx.set_state("<Animatedˇ></Animated>");
24320 cx.update_editor(|editor, _, cx| {
24321 set_linked_edit_ranges(
24322 (Point::new(0, 1), Point::new(0, 9)),
24323 (Point::new(0, 12), Point::new(0, 20)),
24324 editor,
24325 cx,
24326 );
24327 });
24328 cx.update_editor(|editor, window, cx| {
24329 editor.handle_input(".", window, cx);
24330 });
24331 cx.assert_editor_state("<Animated.ˇ></Animated.>");
24332 cx.update_editor(|editor, _, cx| {
24333 set_linked_edit_ranges(
24334 (Point::new(0, 1), Point::new(0, 10)),
24335 (Point::new(0, 13), Point::new(0, 21)),
24336 editor,
24337 cx,
24338 );
24339 });
24340 cx.update_editor(|editor, window, cx| {
24341 editor.handle_input("V", window, cx);
24342 });
24343 cx.assert_editor_state("<Animated.Vˇ></Animated.V>");
24344}
24345
24346#[gpui::test]
24347async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
24348 init_test(cx, |_| {});
24349
24350 let fs = FakeFs::new(cx.executor());
24351 fs.insert_tree(
24352 path!("/root"),
24353 json!({
24354 "a": {
24355 "main.rs": "fn main() {}",
24356 },
24357 "foo": {
24358 "bar": {
24359 "external_file.rs": "pub mod external {}",
24360 }
24361 }
24362 }),
24363 )
24364 .await;
24365
24366 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
24367 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24368 language_registry.add(rust_lang());
24369 let _fake_servers = language_registry.register_fake_lsp(
24370 "Rust",
24371 FakeLspAdapter {
24372 ..FakeLspAdapter::default()
24373 },
24374 );
24375 let (workspace, cx) =
24376 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24377 let worktree_id = workspace.update(cx, |workspace, cx| {
24378 workspace.project().update(cx, |project, cx| {
24379 project.worktrees(cx).next().unwrap().read(cx).id()
24380 })
24381 });
24382
24383 let assert_language_servers_count =
24384 |expected: usize, context: &str, cx: &mut VisualTestContext| {
24385 project.update(cx, |project, cx| {
24386 let current = project
24387 .lsp_store()
24388 .read(cx)
24389 .as_local()
24390 .unwrap()
24391 .language_servers
24392 .len();
24393 assert_eq!(expected, current, "{context}");
24394 });
24395 };
24396
24397 assert_language_servers_count(
24398 0,
24399 "No servers should be running before any file is open",
24400 cx,
24401 );
24402 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24403 let main_editor = workspace
24404 .update_in(cx, |workspace, window, cx| {
24405 workspace.open_path(
24406 (worktree_id, rel_path("main.rs")),
24407 Some(pane.downgrade()),
24408 true,
24409 window,
24410 cx,
24411 )
24412 })
24413 .unwrap()
24414 .await
24415 .downcast::<Editor>()
24416 .unwrap();
24417 pane.update(cx, |pane, cx| {
24418 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24419 open_editor.update(cx, |editor, cx| {
24420 assert_eq!(
24421 editor.display_text(cx),
24422 "fn main() {}",
24423 "Original main.rs text on initial open",
24424 );
24425 });
24426 assert_eq!(open_editor, main_editor);
24427 });
24428 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
24429
24430 let external_editor = workspace
24431 .update_in(cx, |workspace, window, cx| {
24432 workspace.open_abs_path(
24433 PathBuf::from("/root/foo/bar/external_file.rs"),
24434 OpenOptions::default(),
24435 window,
24436 cx,
24437 )
24438 })
24439 .await
24440 .expect("opening external file")
24441 .downcast::<Editor>()
24442 .expect("downcasted external file's open element to editor");
24443 pane.update(cx, |pane, cx| {
24444 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24445 open_editor.update(cx, |editor, cx| {
24446 assert_eq!(
24447 editor.display_text(cx),
24448 "pub mod external {}",
24449 "External file is open now",
24450 );
24451 });
24452 assert_eq!(open_editor, external_editor);
24453 });
24454 assert_language_servers_count(
24455 1,
24456 "Second, external, *.rs file should join the existing server",
24457 cx,
24458 );
24459
24460 pane.update_in(cx, |pane, window, cx| {
24461 pane.close_active_item(&CloseActiveItem::default(), window, cx)
24462 })
24463 .await
24464 .unwrap();
24465 pane.update_in(cx, |pane, window, cx| {
24466 pane.navigate_backward(&Default::default(), window, cx);
24467 });
24468 cx.run_until_parked();
24469 pane.update(cx, |pane, cx| {
24470 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24471 open_editor.update(cx, |editor, cx| {
24472 assert_eq!(
24473 editor.display_text(cx),
24474 "pub mod external {}",
24475 "External file is open now",
24476 );
24477 });
24478 });
24479 assert_language_servers_count(
24480 1,
24481 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
24482 cx,
24483 );
24484
24485 cx.update(|_, cx| {
24486 workspace::reload(cx);
24487 });
24488 assert_language_servers_count(
24489 1,
24490 "After reloading the worktree with local and external files opened, only one project should be started",
24491 cx,
24492 );
24493}
24494
24495#[gpui::test]
24496async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
24497 init_test(cx, |_| {});
24498
24499 let mut cx = EditorTestContext::new(cx).await;
24500 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24501 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24502
24503 // test cursor move to start of each line on tab
24504 // for `if`, `elif`, `else`, `while`, `with` and `for`
24505 cx.set_state(indoc! {"
24506 def main():
24507 ˇ for item in items:
24508 ˇ while item.active:
24509 ˇ if item.value > 10:
24510 ˇ continue
24511 ˇ elif item.value < 0:
24512 ˇ break
24513 ˇ else:
24514 ˇ with item.context() as ctx:
24515 ˇ yield count
24516 ˇ else:
24517 ˇ log('while else')
24518 ˇ else:
24519 ˇ log('for else')
24520 "});
24521 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24522 cx.assert_editor_state(indoc! {"
24523 def main():
24524 ˇfor item in items:
24525 ˇwhile item.active:
24526 ˇif item.value > 10:
24527 ˇcontinue
24528 ˇelif item.value < 0:
24529 ˇbreak
24530 ˇelse:
24531 ˇwith item.context() as ctx:
24532 ˇyield count
24533 ˇelse:
24534 ˇlog('while else')
24535 ˇelse:
24536 ˇlog('for else')
24537 "});
24538 // test relative indent is preserved when tab
24539 // for `if`, `elif`, `else`, `while`, `with` and `for`
24540 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24541 cx.assert_editor_state(indoc! {"
24542 def main():
24543 ˇfor item in items:
24544 ˇwhile item.active:
24545 ˇif item.value > 10:
24546 ˇcontinue
24547 ˇelif item.value < 0:
24548 ˇbreak
24549 ˇelse:
24550 ˇwith item.context() as ctx:
24551 ˇyield count
24552 ˇelse:
24553 ˇlog('while else')
24554 ˇelse:
24555 ˇlog('for else')
24556 "});
24557
24558 // test cursor move to start of each line on tab
24559 // for `try`, `except`, `else`, `finally`, `match` and `def`
24560 cx.set_state(indoc! {"
24561 def main():
24562 ˇ try:
24563 ˇ fetch()
24564 ˇ except ValueError:
24565 ˇ handle_error()
24566 ˇ else:
24567 ˇ match value:
24568 ˇ case _:
24569 ˇ finally:
24570 ˇ def status():
24571 ˇ return 0
24572 "});
24573 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24574 cx.assert_editor_state(indoc! {"
24575 def main():
24576 ˇtry:
24577 ˇfetch()
24578 ˇexcept ValueError:
24579 ˇhandle_error()
24580 ˇelse:
24581 ˇmatch value:
24582 ˇcase _:
24583 ˇfinally:
24584 ˇdef status():
24585 ˇreturn 0
24586 "});
24587 // test relative indent is preserved when tab
24588 // for `try`, `except`, `else`, `finally`, `match` and `def`
24589 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24590 cx.assert_editor_state(indoc! {"
24591 def main():
24592 ˇtry:
24593 ˇfetch()
24594 ˇexcept ValueError:
24595 ˇhandle_error()
24596 ˇelse:
24597 ˇmatch value:
24598 ˇcase _:
24599 ˇfinally:
24600 ˇdef status():
24601 ˇreturn 0
24602 "});
24603}
24604
24605#[gpui::test]
24606async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
24607 init_test(cx, |_| {});
24608
24609 let mut cx = EditorTestContext::new(cx).await;
24610 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24611 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24612
24613 // test `else` auto outdents when typed inside `if` block
24614 cx.set_state(indoc! {"
24615 def main():
24616 if i == 2:
24617 return
24618 ˇ
24619 "});
24620 cx.update_editor(|editor, window, cx| {
24621 editor.handle_input("else:", window, cx);
24622 });
24623 cx.assert_editor_state(indoc! {"
24624 def main():
24625 if i == 2:
24626 return
24627 else:ˇ
24628 "});
24629
24630 // test `except` auto outdents when typed inside `try` block
24631 cx.set_state(indoc! {"
24632 def main():
24633 try:
24634 i = 2
24635 ˇ
24636 "});
24637 cx.update_editor(|editor, window, cx| {
24638 editor.handle_input("except:", window, cx);
24639 });
24640 cx.assert_editor_state(indoc! {"
24641 def main():
24642 try:
24643 i = 2
24644 except:ˇ
24645 "});
24646
24647 // test `else` auto outdents when typed inside `except` block
24648 cx.set_state(indoc! {"
24649 def main():
24650 try:
24651 i = 2
24652 except:
24653 j = 2
24654 ˇ
24655 "});
24656 cx.update_editor(|editor, window, cx| {
24657 editor.handle_input("else:", window, cx);
24658 });
24659 cx.assert_editor_state(indoc! {"
24660 def main():
24661 try:
24662 i = 2
24663 except:
24664 j = 2
24665 else:ˇ
24666 "});
24667
24668 // test `finally` auto outdents when typed inside `else` block
24669 cx.set_state(indoc! {"
24670 def main():
24671 try:
24672 i = 2
24673 except:
24674 j = 2
24675 else:
24676 k = 2
24677 ˇ
24678 "});
24679 cx.update_editor(|editor, window, cx| {
24680 editor.handle_input("finally:", window, cx);
24681 });
24682 cx.assert_editor_state(indoc! {"
24683 def main():
24684 try:
24685 i = 2
24686 except:
24687 j = 2
24688 else:
24689 k = 2
24690 finally:ˇ
24691 "});
24692
24693 // test `else` does not outdents when typed inside `except` block right after for block
24694 cx.set_state(indoc! {"
24695 def main():
24696 try:
24697 i = 2
24698 except:
24699 for i in range(n):
24700 pass
24701 ˇ
24702 "});
24703 cx.update_editor(|editor, window, cx| {
24704 editor.handle_input("else:", window, cx);
24705 });
24706 cx.assert_editor_state(indoc! {"
24707 def main():
24708 try:
24709 i = 2
24710 except:
24711 for i in range(n):
24712 pass
24713 else:ˇ
24714 "});
24715
24716 // test `finally` auto outdents when typed inside `else` block right after for block
24717 cx.set_state(indoc! {"
24718 def main():
24719 try:
24720 i = 2
24721 except:
24722 j = 2
24723 else:
24724 for i in range(n):
24725 pass
24726 ˇ
24727 "});
24728 cx.update_editor(|editor, window, cx| {
24729 editor.handle_input("finally:", window, cx);
24730 });
24731 cx.assert_editor_state(indoc! {"
24732 def main():
24733 try:
24734 i = 2
24735 except:
24736 j = 2
24737 else:
24738 for i in range(n):
24739 pass
24740 finally:ˇ
24741 "});
24742
24743 // test `except` outdents to inner "try" block
24744 cx.set_state(indoc! {"
24745 def main():
24746 try:
24747 i = 2
24748 if i == 2:
24749 try:
24750 i = 3
24751 ˇ
24752 "});
24753 cx.update_editor(|editor, window, cx| {
24754 editor.handle_input("except:", window, cx);
24755 });
24756 cx.assert_editor_state(indoc! {"
24757 def main():
24758 try:
24759 i = 2
24760 if i == 2:
24761 try:
24762 i = 3
24763 except:ˇ
24764 "});
24765
24766 // test `except` outdents to outer "try" block
24767 cx.set_state(indoc! {"
24768 def main():
24769 try:
24770 i = 2
24771 if i == 2:
24772 try:
24773 i = 3
24774 ˇ
24775 "});
24776 cx.update_editor(|editor, window, cx| {
24777 editor.handle_input("except:", window, cx);
24778 });
24779 cx.assert_editor_state(indoc! {"
24780 def main():
24781 try:
24782 i = 2
24783 if i == 2:
24784 try:
24785 i = 3
24786 except:ˇ
24787 "});
24788
24789 // test `else` stays at correct indent when typed after `for` block
24790 cx.set_state(indoc! {"
24791 def main():
24792 for i in range(10):
24793 if i == 3:
24794 break
24795 ˇ
24796 "});
24797 cx.update_editor(|editor, window, cx| {
24798 editor.handle_input("else:", window, cx);
24799 });
24800 cx.assert_editor_state(indoc! {"
24801 def main():
24802 for i in range(10):
24803 if i == 3:
24804 break
24805 else:ˇ
24806 "});
24807
24808 // test does not outdent on typing after line with square brackets
24809 cx.set_state(indoc! {"
24810 def f() -> list[str]:
24811 ˇ
24812 "});
24813 cx.update_editor(|editor, window, cx| {
24814 editor.handle_input("a", window, cx);
24815 });
24816 cx.assert_editor_state(indoc! {"
24817 def f() -> list[str]:
24818 aˇ
24819 "});
24820
24821 // test does not outdent on typing : after case keyword
24822 cx.set_state(indoc! {"
24823 match 1:
24824 caseˇ
24825 "});
24826 cx.update_editor(|editor, window, cx| {
24827 editor.handle_input(":", window, cx);
24828 });
24829 cx.assert_editor_state(indoc! {"
24830 match 1:
24831 case:ˇ
24832 "});
24833}
24834
24835#[gpui::test]
24836async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
24837 init_test(cx, |_| {});
24838 update_test_language_settings(cx, |settings| {
24839 settings.defaults.extend_comment_on_newline = Some(false);
24840 });
24841 let mut cx = EditorTestContext::new(cx).await;
24842 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24843 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24844
24845 // test correct indent after newline on comment
24846 cx.set_state(indoc! {"
24847 # COMMENT:ˇ
24848 "});
24849 cx.update_editor(|editor, window, cx| {
24850 editor.newline(&Newline, window, cx);
24851 });
24852 cx.assert_editor_state(indoc! {"
24853 # COMMENT:
24854 ˇ
24855 "});
24856
24857 // test correct indent after newline in brackets
24858 cx.set_state(indoc! {"
24859 {ˇ}
24860 "});
24861 cx.update_editor(|editor, window, cx| {
24862 editor.newline(&Newline, window, cx);
24863 });
24864 cx.run_until_parked();
24865 cx.assert_editor_state(indoc! {"
24866 {
24867 ˇ
24868 }
24869 "});
24870
24871 cx.set_state(indoc! {"
24872 (ˇ)
24873 "});
24874 cx.update_editor(|editor, window, cx| {
24875 editor.newline(&Newline, window, cx);
24876 });
24877 cx.run_until_parked();
24878 cx.assert_editor_state(indoc! {"
24879 (
24880 ˇ
24881 )
24882 "});
24883
24884 // do not indent after empty lists or dictionaries
24885 cx.set_state(indoc! {"
24886 a = []ˇ
24887 "});
24888 cx.update_editor(|editor, window, cx| {
24889 editor.newline(&Newline, window, cx);
24890 });
24891 cx.run_until_parked();
24892 cx.assert_editor_state(indoc! {"
24893 a = []
24894 ˇ
24895 "});
24896}
24897
24898#[gpui::test]
24899async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
24900 init_test(cx, |_| {});
24901
24902 let mut cx = EditorTestContext::new(cx).await;
24903 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24904 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24905
24906 // test cursor move to start of each line on tab
24907 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
24908 cx.set_state(indoc! {"
24909 function main() {
24910 ˇ for item in $items; do
24911 ˇ while [ -n \"$item\" ]; do
24912 ˇ if [ \"$value\" -gt 10 ]; then
24913 ˇ continue
24914 ˇ elif [ \"$value\" -lt 0 ]; then
24915 ˇ break
24916 ˇ else
24917 ˇ echo \"$item\"
24918 ˇ fi
24919 ˇ done
24920 ˇ done
24921 ˇ}
24922 "});
24923 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24924 cx.assert_editor_state(indoc! {"
24925 function main() {
24926 ˇfor item in $items; do
24927 ˇwhile [ -n \"$item\" ]; do
24928 ˇif [ \"$value\" -gt 10 ]; then
24929 ˇcontinue
24930 ˇelif [ \"$value\" -lt 0 ]; then
24931 ˇbreak
24932 ˇelse
24933 ˇecho \"$item\"
24934 ˇfi
24935 ˇdone
24936 ˇdone
24937 ˇ}
24938 "});
24939 // test relative indent is preserved when tab
24940 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24941 cx.assert_editor_state(indoc! {"
24942 function main() {
24943 ˇfor item in $items; do
24944 ˇwhile [ -n \"$item\" ]; do
24945 ˇif [ \"$value\" -gt 10 ]; then
24946 ˇcontinue
24947 ˇelif [ \"$value\" -lt 0 ]; then
24948 ˇbreak
24949 ˇelse
24950 ˇecho \"$item\"
24951 ˇfi
24952 ˇdone
24953 ˇdone
24954 ˇ}
24955 "});
24956
24957 // test cursor move to start of each line on tab
24958 // for `case` statement with patterns
24959 cx.set_state(indoc! {"
24960 function handle() {
24961 ˇ case \"$1\" in
24962 ˇ start)
24963 ˇ echo \"a\"
24964 ˇ ;;
24965 ˇ stop)
24966 ˇ echo \"b\"
24967 ˇ ;;
24968 ˇ *)
24969 ˇ echo \"c\"
24970 ˇ ;;
24971 ˇ esac
24972 ˇ}
24973 "});
24974 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24975 cx.assert_editor_state(indoc! {"
24976 function handle() {
24977 ˇcase \"$1\" in
24978 ˇstart)
24979 ˇecho \"a\"
24980 ˇ;;
24981 ˇstop)
24982 ˇecho \"b\"
24983 ˇ;;
24984 ˇ*)
24985 ˇecho \"c\"
24986 ˇ;;
24987 ˇesac
24988 ˇ}
24989 "});
24990}
24991
24992#[gpui::test]
24993async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
24994 init_test(cx, |_| {});
24995
24996 let mut cx = EditorTestContext::new(cx).await;
24997 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24998 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24999
25000 // test indents on comment insert
25001 cx.set_state(indoc! {"
25002 function main() {
25003 ˇ for item in $items; do
25004 ˇ while [ -n \"$item\" ]; do
25005 ˇ if [ \"$value\" -gt 10 ]; then
25006 ˇ continue
25007 ˇ elif [ \"$value\" -lt 0 ]; then
25008 ˇ break
25009 ˇ else
25010 ˇ echo \"$item\"
25011 ˇ fi
25012 ˇ done
25013 ˇ done
25014 ˇ}
25015 "});
25016 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
25017 cx.assert_editor_state(indoc! {"
25018 function main() {
25019 #ˇ for item in $items; do
25020 #ˇ while [ -n \"$item\" ]; do
25021 #ˇ if [ \"$value\" -gt 10 ]; then
25022 #ˇ continue
25023 #ˇ elif [ \"$value\" -lt 0 ]; then
25024 #ˇ break
25025 #ˇ else
25026 #ˇ echo \"$item\"
25027 #ˇ fi
25028 #ˇ done
25029 #ˇ done
25030 #ˇ}
25031 "});
25032}
25033
25034#[gpui::test]
25035async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
25036 init_test(cx, |_| {});
25037
25038 let mut cx = EditorTestContext::new(cx).await;
25039 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25040 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25041
25042 // test `else` auto outdents when typed inside `if` block
25043 cx.set_state(indoc! {"
25044 if [ \"$1\" = \"test\" ]; then
25045 echo \"foo bar\"
25046 ˇ
25047 "});
25048 cx.update_editor(|editor, window, cx| {
25049 editor.handle_input("else", window, cx);
25050 });
25051 cx.assert_editor_state(indoc! {"
25052 if [ \"$1\" = \"test\" ]; then
25053 echo \"foo bar\"
25054 elseˇ
25055 "});
25056
25057 // test `elif` auto outdents when typed inside `if` block
25058 cx.set_state(indoc! {"
25059 if [ \"$1\" = \"test\" ]; then
25060 echo \"foo bar\"
25061 ˇ
25062 "});
25063 cx.update_editor(|editor, window, cx| {
25064 editor.handle_input("elif", window, cx);
25065 });
25066 cx.assert_editor_state(indoc! {"
25067 if [ \"$1\" = \"test\" ]; then
25068 echo \"foo bar\"
25069 elifˇ
25070 "});
25071
25072 // test `fi` auto outdents when typed inside `else` block
25073 cx.set_state(indoc! {"
25074 if [ \"$1\" = \"test\" ]; then
25075 echo \"foo bar\"
25076 else
25077 echo \"bar baz\"
25078 ˇ
25079 "});
25080 cx.update_editor(|editor, window, cx| {
25081 editor.handle_input("fi", window, cx);
25082 });
25083 cx.assert_editor_state(indoc! {"
25084 if [ \"$1\" = \"test\" ]; then
25085 echo \"foo bar\"
25086 else
25087 echo \"bar baz\"
25088 fiˇ
25089 "});
25090
25091 // test `done` auto outdents when typed inside `while` block
25092 cx.set_state(indoc! {"
25093 while read line; do
25094 echo \"$line\"
25095 ˇ
25096 "});
25097 cx.update_editor(|editor, window, cx| {
25098 editor.handle_input("done", window, cx);
25099 });
25100 cx.assert_editor_state(indoc! {"
25101 while read line; do
25102 echo \"$line\"
25103 doneˇ
25104 "});
25105
25106 // test `done` auto outdents when typed inside `for` block
25107 cx.set_state(indoc! {"
25108 for file in *.txt; do
25109 cat \"$file\"
25110 ˇ
25111 "});
25112 cx.update_editor(|editor, window, cx| {
25113 editor.handle_input("done", window, cx);
25114 });
25115 cx.assert_editor_state(indoc! {"
25116 for file in *.txt; do
25117 cat \"$file\"
25118 doneˇ
25119 "});
25120
25121 // test `esac` auto outdents when typed inside `case` block
25122 cx.set_state(indoc! {"
25123 case \"$1\" in
25124 start)
25125 echo \"foo bar\"
25126 ;;
25127 stop)
25128 echo \"bar baz\"
25129 ;;
25130 ˇ
25131 "});
25132 cx.update_editor(|editor, window, cx| {
25133 editor.handle_input("esac", window, cx);
25134 });
25135 cx.assert_editor_state(indoc! {"
25136 case \"$1\" in
25137 start)
25138 echo \"foo bar\"
25139 ;;
25140 stop)
25141 echo \"bar baz\"
25142 ;;
25143 esacˇ
25144 "});
25145
25146 // test `*)` auto outdents when typed inside `case` block
25147 cx.set_state(indoc! {"
25148 case \"$1\" in
25149 start)
25150 echo \"foo bar\"
25151 ;;
25152 ˇ
25153 "});
25154 cx.update_editor(|editor, window, cx| {
25155 editor.handle_input("*)", window, cx);
25156 });
25157 cx.assert_editor_state(indoc! {"
25158 case \"$1\" in
25159 start)
25160 echo \"foo bar\"
25161 ;;
25162 *)ˇ
25163 "});
25164
25165 // test `fi` outdents to correct level with nested if blocks
25166 cx.set_state(indoc! {"
25167 if [ \"$1\" = \"test\" ]; then
25168 echo \"outer if\"
25169 if [ \"$2\" = \"debug\" ]; then
25170 echo \"inner if\"
25171 ˇ
25172 "});
25173 cx.update_editor(|editor, window, cx| {
25174 editor.handle_input("fi", window, cx);
25175 });
25176 cx.assert_editor_state(indoc! {"
25177 if [ \"$1\" = \"test\" ]; then
25178 echo \"outer if\"
25179 if [ \"$2\" = \"debug\" ]; then
25180 echo \"inner if\"
25181 fiˇ
25182 "});
25183}
25184
25185#[gpui::test]
25186async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
25187 init_test(cx, |_| {});
25188 update_test_language_settings(cx, |settings| {
25189 settings.defaults.extend_comment_on_newline = Some(false);
25190 });
25191 let mut cx = EditorTestContext::new(cx).await;
25192 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25193 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25194
25195 // test correct indent after newline on comment
25196 cx.set_state(indoc! {"
25197 # COMMENT:ˇ
25198 "});
25199 cx.update_editor(|editor, window, cx| {
25200 editor.newline(&Newline, window, cx);
25201 });
25202 cx.assert_editor_state(indoc! {"
25203 # COMMENT:
25204 ˇ
25205 "});
25206
25207 // test correct indent after newline after `then`
25208 cx.set_state(indoc! {"
25209
25210 if [ \"$1\" = \"test\" ]; thenˇ
25211 "});
25212 cx.update_editor(|editor, window, cx| {
25213 editor.newline(&Newline, window, cx);
25214 });
25215 cx.run_until_parked();
25216 cx.assert_editor_state(indoc! {"
25217
25218 if [ \"$1\" = \"test\" ]; then
25219 ˇ
25220 "});
25221
25222 // test correct indent after newline after `else`
25223 cx.set_state(indoc! {"
25224 if [ \"$1\" = \"test\" ]; then
25225 elseˇ
25226 "});
25227 cx.update_editor(|editor, window, cx| {
25228 editor.newline(&Newline, window, cx);
25229 });
25230 cx.run_until_parked();
25231 cx.assert_editor_state(indoc! {"
25232 if [ \"$1\" = \"test\" ]; then
25233 else
25234 ˇ
25235 "});
25236
25237 // test correct indent after newline after `elif`
25238 cx.set_state(indoc! {"
25239 if [ \"$1\" = \"test\" ]; then
25240 elifˇ
25241 "});
25242 cx.update_editor(|editor, window, cx| {
25243 editor.newline(&Newline, window, cx);
25244 });
25245 cx.run_until_parked();
25246 cx.assert_editor_state(indoc! {"
25247 if [ \"$1\" = \"test\" ]; then
25248 elif
25249 ˇ
25250 "});
25251
25252 // test correct indent after newline after `do`
25253 cx.set_state(indoc! {"
25254 for file in *.txt; doˇ
25255 "});
25256 cx.update_editor(|editor, window, cx| {
25257 editor.newline(&Newline, window, cx);
25258 });
25259 cx.run_until_parked();
25260 cx.assert_editor_state(indoc! {"
25261 for file in *.txt; do
25262 ˇ
25263 "});
25264
25265 // test correct indent after newline after case pattern
25266 cx.set_state(indoc! {"
25267 case \"$1\" in
25268 start)ˇ
25269 "});
25270 cx.update_editor(|editor, window, cx| {
25271 editor.newline(&Newline, window, cx);
25272 });
25273 cx.run_until_parked();
25274 cx.assert_editor_state(indoc! {"
25275 case \"$1\" in
25276 start)
25277 ˇ
25278 "});
25279
25280 // test correct indent after newline after case pattern
25281 cx.set_state(indoc! {"
25282 case \"$1\" in
25283 start)
25284 ;;
25285 *)ˇ
25286 "});
25287 cx.update_editor(|editor, window, cx| {
25288 editor.newline(&Newline, window, cx);
25289 });
25290 cx.run_until_parked();
25291 cx.assert_editor_state(indoc! {"
25292 case \"$1\" in
25293 start)
25294 ;;
25295 *)
25296 ˇ
25297 "});
25298
25299 // test correct indent after newline after function opening brace
25300 cx.set_state(indoc! {"
25301 function test() {ˇ}
25302 "});
25303 cx.update_editor(|editor, window, cx| {
25304 editor.newline(&Newline, window, cx);
25305 });
25306 cx.run_until_parked();
25307 cx.assert_editor_state(indoc! {"
25308 function test() {
25309 ˇ
25310 }
25311 "});
25312
25313 // test no extra indent after semicolon on same line
25314 cx.set_state(indoc! {"
25315 echo \"test\";ˇ
25316 "});
25317 cx.update_editor(|editor, window, cx| {
25318 editor.newline(&Newline, window, cx);
25319 });
25320 cx.run_until_parked();
25321 cx.assert_editor_state(indoc! {"
25322 echo \"test\";
25323 ˇ
25324 "});
25325}
25326
25327fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
25328 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
25329 point..point
25330}
25331
25332#[track_caller]
25333fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
25334 let (text, ranges) = marked_text_ranges(marked_text, true);
25335 assert_eq!(editor.text(cx), text);
25336 assert_eq!(
25337 editor.selections.ranges(&editor.display_snapshot(cx)),
25338 ranges,
25339 "Assert selections are {}",
25340 marked_text
25341 );
25342}
25343
25344pub fn handle_signature_help_request(
25345 cx: &mut EditorLspTestContext,
25346 mocked_response: lsp::SignatureHelp,
25347) -> impl Future<Output = ()> + use<> {
25348 let mut request =
25349 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
25350 let mocked_response = mocked_response.clone();
25351 async move { Ok(Some(mocked_response)) }
25352 });
25353
25354 async move {
25355 request.next().await;
25356 }
25357}
25358
25359#[track_caller]
25360pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
25361 cx.update_editor(|editor, _, _| {
25362 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
25363 let entries = menu.entries.borrow();
25364 let entries = entries
25365 .iter()
25366 .map(|entry| entry.string.as_str())
25367 .collect::<Vec<_>>();
25368 assert_eq!(entries, expected);
25369 } else {
25370 panic!("Expected completions menu");
25371 }
25372 });
25373}
25374
25375/// Handle completion request passing a marked string specifying where the completion
25376/// should be triggered from using '|' character, what range should be replaced, and what completions
25377/// should be returned using '<' and '>' to delimit the range.
25378///
25379/// Also see `handle_completion_request_with_insert_and_replace`.
25380#[track_caller]
25381pub fn handle_completion_request(
25382 marked_string: &str,
25383 completions: Vec<&'static str>,
25384 is_incomplete: bool,
25385 counter: Arc<AtomicUsize>,
25386 cx: &mut EditorLspTestContext,
25387) -> impl Future<Output = ()> {
25388 let complete_from_marker: TextRangeMarker = '|'.into();
25389 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25390 let (_, mut marked_ranges) = marked_text_ranges_by(
25391 marked_string,
25392 vec![complete_from_marker.clone(), replace_range_marker.clone()],
25393 );
25394
25395 let complete_from_position =
25396 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25397 let replace_range =
25398 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25399
25400 let mut request =
25401 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25402 let completions = completions.clone();
25403 counter.fetch_add(1, atomic::Ordering::Release);
25404 async move {
25405 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25406 assert_eq!(
25407 params.text_document_position.position,
25408 complete_from_position
25409 );
25410 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
25411 is_incomplete,
25412 item_defaults: None,
25413 items: completions
25414 .iter()
25415 .map(|completion_text| lsp::CompletionItem {
25416 label: completion_text.to_string(),
25417 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
25418 range: replace_range,
25419 new_text: completion_text.to_string(),
25420 })),
25421 ..Default::default()
25422 })
25423 .collect(),
25424 })))
25425 }
25426 });
25427
25428 async move {
25429 request.next().await;
25430 }
25431}
25432
25433/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
25434/// given instead, which also contains an `insert` range.
25435///
25436/// This function uses markers to define ranges:
25437/// - `|` marks the cursor position
25438/// - `<>` marks the replace range
25439/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
25440pub fn handle_completion_request_with_insert_and_replace(
25441 cx: &mut EditorLspTestContext,
25442 marked_string: &str,
25443 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
25444 counter: Arc<AtomicUsize>,
25445) -> impl Future<Output = ()> {
25446 let complete_from_marker: TextRangeMarker = '|'.into();
25447 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25448 let insert_range_marker: TextRangeMarker = ('{', '}').into();
25449
25450 let (_, mut marked_ranges) = marked_text_ranges_by(
25451 marked_string,
25452 vec![
25453 complete_from_marker.clone(),
25454 replace_range_marker.clone(),
25455 insert_range_marker.clone(),
25456 ],
25457 );
25458
25459 let complete_from_position =
25460 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25461 let replace_range =
25462 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25463
25464 let insert_range = match marked_ranges.remove(&insert_range_marker) {
25465 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
25466 _ => lsp::Range {
25467 start: replace_range.start,
25468 end: complete_from_position,
25469 },
25470 };
25471
25472 let mut request =
25473 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25474 let completions = completions.clone();
25475 counter.fetch_add(1, atomic::Ordering::Release);
25476 async move {
25477 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25478 assert_eq!(
25479 params.text_document_position.position, complete_from_position,
25480 "marker `|` position doesn't match",
25481 );
25482 Ok(Some(lsp::CompletionResponse::Array(
25483 completions
25484 .iter()
25485 .map(|(label, new_text)| lsp::CompletionItem {
25486 label: label.to_string(),
25487 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
25488 lsp::InsertReplaceEdit {
25489 insert: insert_range,
25490 replace: replace_range,
25491 new_text: new_text.to_string(),
25492 },
25493 )),
25494 ..Default::default()
25495 })
25496 .collect(),
25497 )))
25498 }
25499 });
25500
25501 async move {
25502 request.next().await;
25503 }
25504}
25505
25506fn handle_resolve_completion_request(
25507 cx: &mut EditorLspTestContext,
25508 edits: Option<Vec<(&'static str, &'static str)>>,
25509) -> impl Future<Output = ()> {
25510 let edits = edits.map(|edits| {
25511 edits
25512 .iter()
25513 .map(|(marked_string, new_text)| {
25514 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
25515 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
25516 lsp::TextEdit::new(replace_range, new_text.to_string())
25517 })
25518 .collect::<Vec<_>>()
25519 });
25520
25521 let mut request =
25522 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
25523 let edits = edits.clone();
25524 async move {
25525 Ok(lsp::CompletionItem {
25526 additional_text_edits: edits,
25527 ..Default::default()
25528 })
25529 }
25530 });
25531
25532 async move {
25533 request.next().await;
25534 }
25535}
25536
25537pub(crate) fn update_test_language_settings(
25538 cx: &mut TestAppContext,
25539 f: impl Fn(&mut AllLanguageSettingsContent),
25540) {
25541 cx.update(|cx| {
25542 SettingsStore::update_global(cx, |store, cx| {
25543 store.update_user_settings(cx, |settings| f(&mut settings.project.all_languages));
25544 });
25545 });
25546}
25547
25548pub(crate) fn update_test_project_settings(
25549 cx: &mut TestAppContext,
25550 f: impl Fn(&mut ProjectSettingsContent),
25551) {
25552 cx.update(|cx| {
25553 SettingsStore::update_global(cx, |store, cx| {
25554 store.update_user_settings(cx, |settings| f(&mut settings.project));
25555 });
25556 });
25557}
25558
25559pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
25560 cx.update(|cx| {
25561 assets::Assets.load_test_fonts(cx);
25562 let store = SettingsStore::test(cx);
25563 cx.set_global(store);
25564 theme::init(theme::LoadThemes::JustBase, cx);
25565 release_channel::init(SemanticVersion::default(), cx);
25566 client::init_settings(cx);
25567 language::init(cx);
25568 Project::init_settings(cx);
25569 workspace::init_settings(cx);
25570 crate::init(cx);
25571 });
25572 zlog::init_test();
25573 update_test_language_settings(cx, f);
25574}
25575
25576#[track_caller]
25577fn assert_hunk_revert(
25578 not_reverted_text_with_selections: &str,
25579 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
25580 expected_reverted_text_with_selections: &str,
25581 base_text: &str,
25582 cx: &mut EditorLspTestContext,
25583) {
25584 cx.set_state(not_reverted_text_with_selections);
25585 cx.set_head_text(base_text);
25586 cx.executor().run_until_parked();
25587
25588 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
25589 let snapshot = editor.snapshot(window, cx);
25590 let reverted_hunk_statuses = snapshot
25591 .buffer_snapshot()
25592 .diff_hunks_in_range(0..snapshot.buffer_snapshot().len())
25593 .map(|hunk| hunk.status().kind)
25594 .collect::<Vec<_>>();
25595
25596 editor.git_restore(&Default::default(), window, cx);
25597 reverted_hunk_statuses
25598 });
25599 cx.executor().run_until_parked();
25600 cx.assert_editor_state(expected_reverted_text_with_selections);
25601 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
25602}
25603
25604#[gpui::test(iterations = 10)]
25605async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
25606 init_test(cx, |_| {});
25607
25608 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
25609 let counter = diagnostic_requests.clone();
25610
25611 let fs = FakeFs::new(cx.executor());
25612 fs.insert_tree(
25613 path!("/a"),
25614 json!({
25615 "first.rs": "fn main() { let a = 5; }",
25616 "second.rs": "// Test file",
25617 }),
25618 )
25619 .await;
25620
25621 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25622 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25623 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25624
25625 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25626 language_registry.add(rust_lang());
25627 let mut fake_servers = language_registry.register_fake_lsp(
25628 "Rust",
25629 FakeLspAdapter {
25630 capabilities: lsp::ServerCapabilities {
25631 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
25632 lsp::DiagnosticOptions {
25633 identifier: None,
25634 inter_file_dependencies: true,
25635 workspace_diagnostics: true,
25636 work_done_progress_options: Default::default(),
25637 },
25638 )),
25639 ..Default::default()
25640 },
25641 ..Default::default()
25642 },
25643 );
25644
25645 let editor = workspace
25646 .update(cx, |workspace, window, cx| {
25647 workspace.open_abs_path(
25648 PathBuf::from(path!("/a/first.rs")),
25649 OpenOptions::default(),
25650 window,
25651 cx,
25652 )
25653 })
25654 .unwrap()
25655 .await
25656 .unwrap()
25657 .downcast::<Editor>()
25658 .unwrap();
25659 let fake_server = fake_servers.next().await.unwrap();
25660 let server_id = fake_server.server.server_id();
25661 let mut first_request = fake_server
25662 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
25663 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
25664 let result_id = Some(new_result_id.to_string());
25665 assert_eq!(
25666 params.text_document.uri,
25667 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25668 );
25669 async move {
25670 Ok(lsp::DocumentDiagnosticReportResult::Report(
25671 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
25672 related_documents: None,
25673 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
25674 items: Vec::new(),
25675 result_id,
25676 },
25677 }),
25678 ))
25679 }
25680 });
25681
25682 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
25683 project.update(cx, |project, cx| {
25684 let buffer_id = editor
25685 .read(cx)
25686 .buffer()
25687 .read(cx)
25688 .as_singleton()
25689 .expect("created a singleton buffer")
25690 .read(cx)
25691 .remote_id();
25692 let buffer_result_id = project
25693 .lsp_store()
25694 .read(cx)
25695 .result_id(server_id, buffer_id, cx);
25696 assert_eq!(expected, buffer_result_id);
25697 });
25698 };
25699
25700 ensure_result_id(None, cx);
25701 cx.executor().advance_clock(Duration::from_millis(60));
25702 cx.executor().run_until_parked();
25703 assert_eq!(
25704 diagnostic_requests.load(atomic::Ordering::Acquire),
25705 1,
25706 "Opening file should trigger diagnostic request"
25707 );
25708 first_request
25709 .next()
25710 .await
25711 .expect("should have sent the first diagnostics pull request");
25712 ensure_result_id(Some("1".to_string()), cx);
25713
25714 // Editing should trigger diagnostics
25715 editor.update_in(cx, |editor, window, cx| {
25716 editor.handle_input("2", window, cx)
25717 });
25718 cx.executor().advance_clock(Duration::from_millis(60));
25719 cx.executor().run_until_parked();
25720 assert_eq!(
25721 diagnostic_requests.load(atomic::Ordering::Acquire),
25722 2,
25723 "Editing should trigger diagnostic request"
25724 );
25725 ensure_result_id(Some("2".to_string()), cx);
25726
25727 // Moving cursor should not trigger diagnostic request
25728 editor.update_in(cx, |editor, window, cx| {
25729 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25730 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
25731 });
25732 });
25733 cx.executor().advance_clock(Duration::from_millis(60));
25734 cx.executor().run_until_parked();
25735 assert_eq!(
25736 diagnostic_requests.load(atomic::Ordering::Acquire),
25737 2,
25738 "Cursor movement should not trigger diagnostic request"
25739 );
25740 ensure_result_id(Some("2".to_string()), cx);
25741 // Multiple rapid edits should be debounced
25742 for _ in 0..5 {
25743 editor.update_in(cx, |editor, window, cx| {
25744 editor.handle_input("x", window, cx)
25745 });
25746 }
25747 cx.executor().advance_clock(Duration::from_millis(60));
25748 cx.executor().run_until_parked();
25749
25750 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
25751 assert!(
25752 final_requests <= 4,
25753 "Multiple rapid edits should be debounced (got {final_requests} requests)",
25754 );
25755 ensure_result_id(Some(final_requests.to_string()), cx);
25756}
25757
25758#[gpui::test]
25759async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
25760 // Regression test for issue #11671
25761 // Previously, adding a cursor after moving multiple cursors would reset
25762 // the cursor count instead of adding to the existing cursors.
25763 init_test(cx, |_| {});
25764 let mut cx = EditorTestContext::new(cx).await;
25765
25766 // Create a simple buffer with cursor at start
25767 cx.set_state(indoc! {"
25768 ˇaaaa
25769 bbbb
25770 cccc
25771 dddd
25772 eeee
25773 ffff
25774 gggg
25775 hhhh"});
25776
25777 // Add 2 cursors below (so we have 3 total)
25778 cx.update_editor(|editor, window, cx| {
25779 editor.add_selection_below(&Default::default(), window, cx);
25780 editor.add_selection_below(&Default::default(), window, cx);
25781 });
25782
25783 // Verify we have 3 cursors
25784 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
25785 assert_eq!(
25786 initial_count, 3,
25787 "Should have 3 cursors after adding 2 below"
25788 );
25789
25790 // Move down one line
25791 cx.update_editor(|editor, window, cx| {
25792 editor.move_down(&MoveDown, window, cx);
25793 });
25794
25795 // Add another cursor below
25796 cx.update_editor(|editor, window, cx| {
25797 editor.add_selection_below(&Default::default(), window, cx);
25798 });
25799
25800 // Should now have 4 cursors (3 original + 1 new)
25801 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
25802 assert_eq!(
25803 final_count, 4,
25804 "Should have 4 cursors after moving and adding another"
25805 );
25806}
25807
25808#[gpui::test]
25809async fn test_add_selection_skip_soft_wrap_option(cx: &mut TestAppContext) {
25810 init_test(cx, |_| {});
25811
25812 let mut cx = EditorTestContext::new(cx).await;
25813
25814 cx.set_state(indoc!(
25815 r#"ˇThis is a very long line that will be wrapped when soft wrapping is enabled
25816 Second line here"#
25817 ));
25818
25819 cx.update_editor(|editor, window, cx| {
25820 // Enable soft wrapping with a narrow width to force soft wrapping and
25821 // confirm that more than 2 rows are being displayed.
25822 editor.set_wrap_width(Some(100.0.into()), cx);
25823 assert!(editor.display_text(cx).lines().count() > 2);
25824
25825 editor.add_selection_below(
25826 &AddSelectionBelow {
25827 skip_soft_wrap: true,
25828 },
25829 window,
25830 cx,
25831 );
25832
25833 assert_eq!(
25834 editor.selections.display_ranges(cx),
25835 &[
25836 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
25837 DisplayPoint::new(DisplayRow(8), 0)..DisplayPoint::new(DisplayRow(8), 0),
25838 ]
25839 );
25840
25841 editor.add_selection_above(
25842 &AddSelectionAbove {
25843 skip_soft_wrap: true,
25844 },
25845 window,
25846 cx,
25847 );
25848
25849 assert_eq!(
25850 editor.selections.display_ranges(cx),
25851 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
25852 );
25853
25854 editor.add_selection_below(
25855 &AddSelectionBelow {
25856 skip_soft_wrap: false,
25857 },
25858 window,
25859 cx,
25860 );
25861
25862 assert_eq!(
25863 editor.selections.display_ranges(cx),
25864 &[
25865 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
25866 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
25867 ]
25868 );
25869
25870 editor.add_selection_above(
25871 &AddSelectionAbove {
25872 skip_soft_wrap: false,
25873 },
25874 window,
25875 cx,
25876 );
25877
25878 assert_eq!(
25879 editor.selections.display_ranges(cx),
25880 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
25881 );
25882 });
25883}
25884
25885#[gpui::test(iterations = 10)]
25886async fn test_document_colors(cx: &mut TestAppContext) {
25887 let expected_color = Rgba {
25888 r: 0.33,
25889 g: 0.33,
25890 b: 0.33,
25891 a: 0.33,
25892 };
25893
25894 init_test(cx, |_| {});
25895
25896 let fs = FakeFs::new(cx.executor());
25897 fs.insert_tree(
25898 path!("/a"),
25899 json!({
25900 "first.rs": "fn main() { let a = 5; }",
25901 }),
25902 )
25903 .await;
25904
25905 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25906 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25907 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25908
25909 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25910 language_registry.add(rust_lang());
25911 let mut fake_servers = language_registry.register_fake_lsp(
25912 "Rust",
25913 FakeLspAdapter {
25914 capabilities: lsp::ServerCapabilities {
25915 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
25916 ..lsp::ServerCapabilities::default()
25917 },
25918 name: "rust-analyzer",
25919 ..FakeLspAdapter::default()
25920 },
25921 );
25922 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
25923 "Rust",
25924 FakeLspAdapter {
25925 capabilities: lsp::ServerCapabilities {
25926 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
25927 ..lsp::ServerCapabilities::default()
25928 },
25929 name: "not-rust-analyzer",
25930 ..FakeLspAdapter::default()
25931 },
25932 );
25933
25934 let editor = workspace
25935 .update(cx, |workspace, window, cx| {
25936 workspace.open_abs_path(
25937 PathBuf::from(path!("/a/first.rs")),
25938 OpenOptions::default(),
25939 window,
25940 cx,
25941 )
25942 })
25943 .unwrap()
25944 .await
25945 .unwrap()
25946 .downcast::<Editor>()
25947 .unwrap();
25948 let fake_language_server = fake_servers.next().await.unwrap();
25949 let fake_language_server_without_capabilities =
25950 fake_servers_without_capabilities.next().await.unwrap();
25951 let requests_made = Arc::new(AtomicUsize::new(0));
25952 let closure_requests_made = Arc::clone(&requests_made);
25953 let mut color_request_handle = fake_language_server
25954 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
25955 let requests_made = Arc::clone(&closure_requests_made);
25956 async move {
25957 assert_eq!(
25958 params.text_document.uri,
25959 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25960 );
25961 requests_made.fetch_add(1, atomic::Ordering::Release);
25962 Ok(vec![
25963 lsp::ColorInformation {
25964 range: lsp::Range {
25965 start: lsp::Position {
25966 line: 0,
25967 character: 0,
25968 },
25969 end: lsp::Position {
25970 line: 0,
25971 character: 1,
25972 },
25973 },
25974 color: lsp::Color {
25975 red: 0.33,
25976 green: 0.33,
25977 blue: 0.33,
25978 alpha: 0.33,
25979 },
25980 },
25981 lsp::ColorInformation {
25982 range: lsp::Range {
25983 start: lsp::Position {
25984 line: 0,
25985 character: 0,
25986 },
25987 end: lsp::Position {
25988 line: 0,
25989 character: 1,
25990 },
25991 },
25992 color: lsp::Color {
25993 red: 0.33,
25994 green: 0.33,
25995 blue: 0.33,
25996 alpha: 0.33,
25997 },
25998 },
25999 ])
26000 }
26001 });
26002
26003 let _handle = fake_language_server_without_capabilities
26004 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
26005 panic!("Should not be called");
26006 });
26007 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26008 color_request_handle.next().await.unwrap();
26009 cx.run_until_parked();
26010 assert_eq!(
26011 1,
26012 requests_made.load(atomic::Ordering::Acquire),
26013 "Should query for colors once per editor open"
26014 );
26015 editor.update_in(cx, |editor, _, cx| {
26016 assert_eq!(
26017 vec![expected_color],
26018 extract_color_inlays(editor, cx),
26019 "Should have an initial inlay"
26020 );
26021 });
26022
26023 // opening another file in a split should not influence the LSP query counter
26024 workspace
26025 .update(cx, |workspace, window, cx| {
26026 assert_eq!(
26027 workspace.panes().len(),
26028 1,
26029 "Should have one pane with one editor"
26030 );
26031 workspace.move_item_to_pane_in_direction(
26032 &MoveItemToPaneInDirection {
26033 direction: SplitDirection::Right,
26034 focus: false,
26035 clone: true,
26036 },
26037 window,
26038 cx,
26039 );
26040 })
26041 .unwrap();
26042 cx.run_until_parked();
26043 workspace
26044 .update(cx, |workspace, _, cx| {
26045 let panes = workspace.panes();
26046 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
26047 for pane in panes {
26048 let editor = pane
26049 .read(cx)
26050 .active_item()
26051 .and_then(|item| item.downcast::<Editor>())
26052 .expect("Should have opened an editor in each split");
26053 let editor_file = editor
26054 .read(cx)
26055 .buffer()
26056 .read(cx)
26057 .as_singleton()
26058 .expect("test deals with singleton buffers")
26059 .read(cx)
26060 .file()
26061 .expect("test buffese should have a file")
26062 .path();
26063 assert_eq!(
26064 editor_file.as_ref(),
26065 rel_path("first.rs"),
26066 "Both editors should be opened for the same file"
26067 )
26068 }
26069 })
26070 .unwrap();
26071
26072 cx.executor().advance_clock(Duration::from_millis(500));
26073 let save = editor.update_in(cx, |editor, window, cx| {
26074 editor.move_to_end(&MoveToEnd, window, cx);
26075 editor.handle_input("dirty", window, cx);
26076 editor.save(
26077 SaveOptions {
26078 format: true,
26079 autosave: true,
26080 },
26081 project.clone(),
26082 window,
26083 cx,
26084 )
26085 });
26086 save.await.unwrap();
26087
26088 color_request_handle.next().await.unwrap();
26089 cx.run_until_parked();
26090 assert_eq!(
26091 2,
26092 requests_made.load(atomic::Ordering::Acquire),
26093 "Should query for colors once per save (deduplicated) and once per formatting after save"
26094 );
26095
26096 drop(editor);
26097 let close = workspace
26098 .update(cx, |workspace, window, cx| {
26099 workspace.active_pane().update(cx, |pane, cx| {
26100 pane.close_active_item(&CloseActiveItem::default(), window, cx)
26101 })
26102 })
26103 .unwrap();
26104 close.await.unwrap();
26105 let close = workspace
26106 .update(cx, |workspace, window, cx| {
26107 workspace.active_pane().update(cx, |pane, cx| {
26108 pane.close_active_item(&CloseActiveItem::default(), window, cx)
26109 })
26110 })
26111 .unwrap();
26112 close.await.unwrap();
26113 assert_eq!(
26114 2,
26115 requests_made.load(atomic::Ordering::Acquire),
26116 "After saving and closing all editors, no extra requests should be made"
26117 );
26118 workspace
26119 .update(cx, |workspace, _, cx| {
26120 assert!(
26121 workspace.active_item(cx).is_none(),
26122 "Should close all editors"
26123 )
26124 })
26125 .unwrap();
26126
26127 workspace
26128 .update(cx, |workspace, window, cx| {
26129 workspace.active_pane().update(cx, |pane, cx| {
26130 pane.navigate_backward(&workspace::GoBack, window, cx);
26131 })
26132 })
26133 .unwrap();
26134 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26135 cx.run_until_parked();
26136 let editor = workspace
26137 .update(cx, |workspace, _, cx| {
26138 workspace
26139 .active_item(cx)
26140 .expect("Should have reopened the editor again after navigating back")
26141 .downcast::<Editor>()
26142 .expect("Should be an editor")
26143 })
26144 .unwrap();
26145
26146 assert_eq!(
26147 2,
26148 requests_made.load(atomic::Ordering::Acquire),
26149 "Cache should be reused on buffer close and reopen"
26150 );
26151 editor.update(cx, |editor, cx| {
26152 assert_eq!(
26153 vec![expected_color],
26154 extract_color_inlays(editor, cx),
26155 "Should have an initial inlay"
26156 );
26157 });
26158
26159 drop(color_request_handle);
26160 let closure_requests_made = Arc::clone(&requests_made);
26161 let mut empty_color_request_handle = fake_language_server
26162 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
26163 let requests_made = Arc::clone(&closure_requests_made);
26164 async move {
26165 assert_eq!(
26166 params.text_document.uri,
26167 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
26168 );
26169 requests_made.fetch_add(1, atomic::Ordering::Release);
26170 Ok(Vec::new())
26171 }
26172 });
26173 let save = editor.update_in(cx, |editor, window, cx| {
26174 editor.move_to_end(&MoveToEnd, window, cx);
26175 editor.handle_input("dirty_again", window, cx);
26176 editor.save(
26177 SaveOptions {
26178 format: false,
26179 autosave: true,
26180 },
26181 project.clone(),
26182 window,
26183 cx,
26184 )
26185 });
26186 save.await.unwrap();
26187
26188 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26189 empty_color_request_handle.next().await.unwrap();
26190 cx.run_until_parked();
26191 assert_eq!(
26192 3,
26193 requests_made.load(atomic::Ordering::Acquire),
26194 "Should query for colors once per save only, as formatting was not requested"
26195 );
26196 editor.update(cx, |editor, cx| {
26197 assert_eq!(
26198 Vec::<Rgba>::new(),
26199 extract_color_inlays(editor, cx),
26200 "Should clear all colors when the server returns an empty response"
26201 );
26202 });
26203}
26204
26205#[gpui::test]
26206async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
26207 init_test(cx, |_| {});
26208 let (editor, cx) = cx.add_window_view(Editor::single_line);
26209 editor.update_in(cx, |editor, window, cx| {
26210 editor.set_text("oops\n\nwow\n", window, cx)
26211 });
26212 cx.run_until_parked();
26213 editor.update(cx, |editor, cx| {
26214 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
26215 });
26216 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
26217 cx.run_until_parked();
26218 editor.update(cx, |editor, cx| {
26219 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
26220 });
26221}
26222
26223#[gpui::test]
26224async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
26225 init_test(cx, |_| {});
26226
26227 cx.update(|cx| {
26228 register_project_item::<Editor>(cx);
26229 });
26230
26231 let fs = FakeFs::new(cx.executor());
26232 fs.insert_tree("/root1", json!({})).await;
26233 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
26234 .await;
26235
26236 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
26237 let (workspace, cx) =
26238 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
26239
26240 let worktree_id = project.update(cx, |project, cx| {
26241 project.worktrees(cx).next().unwrap().read(cx).id()
26242 });
26243
26244 let handle = workspace
26245 .update_in(cx, |workspace, window, cx| {
26246 let project_path = (worktree_id, rel_path("one.pdf"));
26247 workspace.open_path(project_path, None, true, window, cx)
26248 })
26249 .await
26250 .unwrap();
26251
26252 assert_eq!(
26253 handle.to_any().entity_type(),
26254 TypeId::of::<InvalidBufferView>()
26255 );
26256}
26257
26258#[gpui::test]
26259async fn test_select_next_prev_syntax_node(cx: &mut TestAppContext) {
26260 init_test(cx, |_| {});
26261
26262 let language = Arc::new(Language::new(
26263 LanguageConfig::default(),
26264 Some(tree_sitter_rust::LANGUAGE.into()),
26265 ));
26266
26267 // Test hierarchical sibling navigation
26268 let text = r#"
26269 fn outer() {
26270 if condition {
26271 let a = 1;
26272 }
26273 let b = 2;
26274 }
26275
26276 fn another() {
26277 let c = 3;
26278 }
26279 "#;
26280
26281 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
26282 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
26283 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
26284
26285 // Wait for parsing to complete
26286 editor
26287 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
26288 .await;
26289
26290 editor.update_in(cx, |editor, window, cx| {
26291 // Start by selecting "let a = 1;" inside the if block
26292 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26293 s.select_display_ranges([
26294 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 26)
26295 ]);
26296 });
26297
26298 let initial_selection = editor.selections.display_ranges(cx);
26299 assert_eq!(initial_selection.len(), 1, "Should have one selection");
26300
26301 // Test select next sibling - should move up levels to find the next sibling
26302 // Since "let a = 1;" has no siblings in the if block, it should move up
26303 // to find "let b = 2;" which is a sibling of the if block
26304 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
26305 let next_selection = editor.selections.display_ranges(cx);
26306
26307 // Should have a selection and it should be different from the initial
26308 assert_eq!(
26309 next_selection.len(),
26310 1,
26311 "Should have one selection after next"
26312 );
26313 assert_ne!(
26314 next_selection[0], initial_selection[0],
26315 "Next sibling selection should be different"
26316 );
26317
26318 // Test hierarchical navigation by going to the end of the current function
26319 // and trying to navigate to the next function
26320 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26321 s.select_display_ranges([
26322 DisplayPoint::new(DisplayRow(5), 12)..DisplayPoint::new(DisplayRow(5), 22)
26323 ]);
26324 });
26325
26326 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
26327 let function_next_selection = editor.selections.display_ranges(cx);
26328
26329 // Should move to the next function
26330 assert_eq!(
26331 function_next_selection.len(),
26332 1,
26333 "Should have one selection after function next"
26334 );
26335
26336 // Test select previous sibling navigation
26337 editor.select_prev_syntax_node(&SelectPreviousSyntaxNode, window, cx);
26338 let prev_selection = editor.selections.display_ranges(cx);
26339
26340 // Should have a selection and it should be different
26341 assert_eq!(
26342 prev_selection.len(),
26343 1,
26344 "Should have one selection after prev"
26345 );
26346 assert_ne!(
26347 prev_selection[0], function_next_selection[0],
26348 "Previous sibling selection should be different from next"
26349 );
26350 });
26351}
26352
26353#[gpui::test]
26354async fn test_next_prev_document_highlight(cx: &mut TestAppContext) {
26355 init_test(cx, |_| {});
26356
26357 let mut cx = EditorTestContext::new(cx).await;
26358 cx.set_state(
26359 "let ˇvariable = 42;
26360let another = variable + 1;
26361let result = variable * 2;",
26362 );
26363
26364 // Set up document highlights manually (simulating LSP response)
26365 cx.update_editor(|editor, _window, cx| {
26366 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
26367
26368 // Create highlights for "variable" occurrences
26369 let highlight_ranges = [
26370 Point::new(0, 4)..Point::new(0, 12), // First "variable"
26371 Point::new(1, 14)..Point::new(1, 22), // Second "variable"
26372 Point::new(2, 13)..Point::new(2, 21), // Third "variable"
26373 ];
26374
26375 let anchor_ranges: Vec<_> = highlight_ranges
26376 .iter()
26377 .map(|range| range.clone().to_anchors(&buffer_snapshot))
26378 .collect();
26379
26380 editor.highlight_background::<DocumentHighlightRead>(
26381 &anchor_ranges,
26382 |theme| theme.colors().editor_document_highlight_read_background,
26383 cx,
26384 );
26385 });
26386
26387 // Go to next highlight - should move to second "variable"
26388 cx.update_editor(|editor, window, cx| {
26389 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26390 });
26391 cx.assert_editor_state(
26392 "let variable = 42;
26393let another = ˇvariable + 1;
26394let result = variable * 2;",
26395 );
26396
26397 // Go to next highlight - should move to third "variable"
26398 cx.update_editor(|editor, window, cx| {
26399 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26400 });
26401 cx.assert_editor_state(
26402 "let variable = 42;
26403let another = variable + 1;
26404let result = ˇvariable * 2;",
26405 );
26406
26407 // Go to next highlight - should stay at third "variable" (no wrap-around)
26408 cx.update_editor(|editor, window, cx| {
26409 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26410 });
26411 cx.assert_editor_state(
26412 "let variable = 42;
26413let another = variable + 1;
26414let result = ˇvariable * 2;",
26415 );
26416
26417 // Now test going backwards from third position
26418 cx.update_editor(|editor, window, cx| {
26419 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26420 });
26421 cx.assert_editor_state(
26422 "let variable = 42;
26423let another = ˇvariable + 1;
26424let result = variable * 2;",
26425 );
26426
26427 // Go to previous highlight - should move to first "variable"
26428 cx.update_editor(|editor, window, cx| {
26429 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26430 });
26431 cx.assert_editor_state(
26432 "let ˇvariable = 42;
26433let another = variable + 1;
26434let result = variable * 2;",
26435 );
26436
26437 // Go to previous highlight - should stay on first "variable"
26438 cx.update_editor(|editor, window, cx| {
26439 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26440 });
26441 cx.assert_editor_state(
26442 "let ˇvariable = 42;
26443let another = variable + 1;
26444let result = variable * 2;",
26445 );
26446}
26447
26448#[gpui::test]
26449async fn test_paste_url_from_other_app_creates_markdown_link_over_selected_text(
26450 cx: &mut gpui::TestAppContext,
26451) {
26452 init_test(cx, |_| {});
26453
26454 let url = "https://zed.dev";
26455
26456 let markdown_language = Arc::new(Language::new(
26457 LanguageConfig {
26458 name: "Markdown".into(),
26459 ..LanguageConfig::default()
26460 },
26461 None,
26462 ));
26463
26464 let mut cx = EditorTestContext::new(cx).await;
26465 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26466 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)");
26467
26468 cx.update_editor(|editor, window, cx| {
26469 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26470 editor.paste(&Paste, window, cx);
26471 });
26472
26473 cx.assert_editor_state(&format!(
26474 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)"
26475 ));
26476}
26477
26478#[gpui::test]
26479async fn test_paste_url_from_zed_copy_creates_markdown_link_over_selected_text(
26480 cx: &mut gpui::TestAppContext,
26481) {
26482 init_test(cx, |_| {});
26483
26484 let url = "https://zed.dev";
26485
26486 let markdown_language = Arc::new(Language::new(
26487 LanguageConfig {
26488 name: "Markdown".into(),
26489 ..LanguageConfig::default()
26490 },
26491 None,
26492 ));
26493
26494 let mut cx = EditorTestContext::new(cx).await;
26495 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26496 cx.set_state(&format!(
26497 "Hello, editor.\nZed is great (see this link: )\n«{url}ˇ»"
26498 ));
26499
26500 cx.update_editor(|editor, window, cx| {
26501 editor.copy(&Copy, window, cx);
26502 });
26503
26504 cx.set_state(&format!(
26505 "Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)\n{url}"
26506 ));
26507
26508 cx.update_editor(|editor, window, cx| {
26509 editor.paste(&Paste, window, cx);
26510 });
26511
26512 cx.assert_editor_state(&format!(
26513 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)\n{url}"
26514 ));
26515}
26516
26517#[gpui::test]
26518async fn test_paste_url_from_other_app_replaces_existing_url_without_creating_markdown_link(
26519 cx: &mut gpui::TestAppContext,
26520) {
26521 init_test(cx, |_| {});
26522
26523 let url = "https://zed.dev";
26524
26525 let markdown_language = Arc::new(Language::new(
26526 LanguageConfig {
26527 name: "Markdown".into(),
26528 ..LanguageConfig::default()
26529 },
26530 None,
26531 ));
26532
26533 let mut cx = EditorTestContext::new(cx).await;
26534 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26535 cx.set_state("Please visit zed's homepage: «https://www.apple.comˇ»");
26536
26537 cx.update_editor(|editor, window, cx| {
26538 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26539 editor.paste(&Paste, window, cx);
26540 });
26541
26542 cx.assert_editor_state(&format!("Please visit zed's homepage: {url}ˇ"));
26543}
26544
26545#[gpui::test]
26546async fn test_paste_plain_text_from_other_app_replaces_selection_without_creating_markdown_link(
26547 cx: &mut gpui::TestAppContext,
26548) {
26549 init_test(cx, |_| {});
26550
26551 let text = "Awesome";
26552
26553 let markdown_language = Arc::new(Language::new(
26554 LanguageConfig {
26555 name: "Markdown".into(),
26556 ..LanguageConfig::default()
26557 },
26558 None,
26559 ));
26560
26561 let mut cx = EditorTestContext::new(cx).await;
26562 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26563 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat»");
26564
26565 cx.update_editor(|editor, window, cx| {
26566 cx.write_to_clipboard(ClipboardItem::new_string(text.to_string()));
26567 editor.paste(&Paste, window, cx);
26568 });
26569
26570 cx.assert_editor_state(&format!("Hello, {text}ˇ.\nZed is {text}ˇ"));
26571}
26572
26573#[gpui::test]
26574async fn test_paste_url_from_other_app_without_creating_markdown_link_in_non_markdown_language(
26575 cx: &mut gpui::TestAppContext,
26576) {
26577 init_test(cx, |_| {});
26578
26579 let url = "https://zed.dev";
26580
26581 let markdown_language = Arc::new(Language::new(
26582 LanguageConfig {
26583 name: "Rust".into(),
26584 ..LanguageConfig::default()
26585 },
26586 None,
26587 ));
26588
26589 let mut cx = EditorTestContext::new(cx).await;
26590 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26591 cx.set_state("// Hello, «editorˇ».\n// Zed is «ˇgreat» (see this link: ˇ)");
26592
26593 cx.update_editor(|editor, window, cx| {
26594 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26595 editor.paste(&Paste, window, cx);
26596 });
26597
26598 cx.assert_editor_state(&format!(
26599 "// Hello, {url}ˇ.\n// Zed is {url}ˇ (see this link: {url}ˇ)"
26600 ));
26601}
26602
26603#[gpui::test]
26604async fn test_paste_url_from_other_app_creates_markdown_link_selectively_in_multi_buffer(
26605 cx: &mut TestAppContext,
26606) {
26607 init_test(cx, |_| {});
26608
26609 let url = "https://zed.dev";
26610
26611 let markdown_language = Arc::new(Language::new(
26612 LanguageConfig {
26613 name: "Markdown".into(),
26614 ..LanguageConfig::default()
26615 },
26616 None,
26617 ));
26618
26619 let (editor, cx) = cx.add_window_view(|window, cx| {
26620 let multi_buffer = MultiBuffer::build_multi(
26621 [
26622 ("this will embed -> link", vec![Point::row_range(0..1)]),
26623 ("this will replace -> link", vec![Point::row_range(0..1)]),
26624 ],
26625 cx,
26626 );
26627 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
26628 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26629 s.select_ranges(vec![
26630 Point::new(0, 19)..Point::new(0, 23),
26631 Point::new(1, 21)..Point::new(1, 25),
26632 ])
26633 });
26634 let first_buffer_id = multi_buffer
26635 .read(cx)
26636 .excerpt_buffer_ids()
26637 .into_iter()
26638 .next()
26639 .unwrap();
26640 let first_buffer = multi_buffer.read(cx).buffer(first_buffer_id).unwrap();
26641 first_buffer.update(cx, |buffer, cx| {
26642 buffer.set_language(Some(markdown_language.clone()), cx);
26643 });
26644
26645 editor
26646 });
26647 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
26648
26649 cx.update_editor(|editor, window, cx| {
26650 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26651 editor.paste(&Paste, window, cx);
26652 });
26653
26654 cx.assert_editor_state(&format!(
26655 "this will embed -> [link]({url})ˇ\nthis will replace -> {url}ˇ"
26656 ));
26657}
26658
26659#[gpui::test]
26660async fn test_race_in_multibuffer_save(cx: &mut TestAppContext) {
26661 init_test(cx, |_| {});
26662
26663 let fs = FakeFs::new(cx.executor());
26664 fs.insert_tree(
26665 path!("/project"),
26666 json!({
26667 "first.rs": "# First Document\nSome content here.",
26668 "second.rs": "Plain text content for second file.",
26669 }),
26670 )
26671 .await;
26672
26673 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
26674 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
26675 let cx = &mut VisualTestContext::from_window(*workspace, cx);
26676
26677 let language = rust_lang();
26678 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
26679 language_registry.add(language.clone());
26680 let mut fake_servers = language_registry.register_fake_lsp(
26681 "Rust",
26682 FakeLspAdapter {
26683 ..FakeLspAdapter::default()
26684 },
26685 );
26686
26687 let buffer1 = project
26688 .update(cx, |project, cx| {
26689 project.open_local_buffer(PathBuf::from(path!("/project/first.rs")), cx)
26690 })
26691 .await
26692 .unwrap();
26693 let buffer2 = project
26694 .update(cx, |project, cx| {
26695 project.open_local_buffer(PathBuf::from(path!("/project/second.rs")), cx)
26696 })
26697 .await
26698 .unwrap();
26699
26700 let multi_buffer = cx.new(|cx| {
26701 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
26702 multi_buffer.set_excerpts_for_path(
26703 PathKey::for_buffer(&buffer1, cx),
26704 buffer1.clone(),
26705 [Point::zero()..buffer1.read(cx).max_point()],
26706 3,
26707 cx,
26708 );
26709 multi_buffer.set_excerpts_for_path(
26710 PathKey::for_buffer(&buffer2, cx),
26711 buffer2.clone(),
26712 [Point::zero()..buffer1.read(cx).max_point()],
26713 3,
26714 cx,
26715 );
26716 multi_buffer
26717 });
26718
26719 let (editor, cx) = cx.add_window_view(|window, cx| {
26720 Editor::new(
26721 EditorMode::full(),
26722 multi_buffer,
26723 Some(project.clone()),
26724 window,
26725 cx,
26726 )
26727 });
26728
26729 let fake_language_server = fake_servers.next().await.unwrap();
26730
26731 buffer1.update(cx, |buffer, cx| buffer.edit([(0..0, "hello!")], None, cx));
26732
26733 let save = editor.update_in(cx, |editor, window, cx| {
26734 assert!(editor.is_dirty(cx));
26735
26736 editor.save(
26737 SaveOptions {
26738 format: true,
26739 autosave: true,
26740 },
26741 project,
26742 window,
26743 cx,
26744 )
26745 });
26746 let (start_edit_tx, start_edit_rx) = oneshot::channel();
26747 let (done_edit_tx, done_edit_rx) = oneshot::channel();
26748 let mut done_edit_rx = Some(done_edit_rx);
26749 let mut start_edit_tx = Some(start_edit_tx);
26750
26751 fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| {
26752 start_edit_tx.take().unwrap().send(()).unwrap();
26753 let done_edit_rx = done_edit_rx.take().unwrap();
26754 async move {
26755 done_edit_rx.await.unwrap();
26756 Ok(None)
26757 }
26758 });
26759
26760 start_edit_rx.await.unwrap();
26761 buffer2
26762 .update(cx, |buffer, cx| buffer.edit([(0..0, "world!")], None, cx))
26763 .unwrap();
26764
26765 done_edit_tx.send(()).unwrap();
26766
26767 save.await.unwrap();
26768 cx.update(|_, cx| assert!(editor.is_dirty(cx)));
26769}
26770
26771#[track_caller]
26772fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
26773 editor
26774 .all_inlays(cx)
26775 .into_iter()
26776 .filter_map(|inlay| inlay.get_color())
26777 .map(Rgba::from)
26778 .collect()
26779}
26780
26781#[gpui::test]
26782fn test_duplicate_line_up_on_last_line_without_newline(cx: &mut TestAppContext) {
26783 init_test(cx, |_| {});
26784
26785 let editor = cx.add_window(|window, cx| {
26786 let buffer = MultiBuffer::build_simple("line1\nline2", cx);
26787 build_editor(buffer, window, cx)
26788 });
26789
26790 editor
26791 .update(cx, |editor, window, cx| {
26792 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26793 s.select_display_ranges([
26794 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
26795 ])
26796 });
26797
26798 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
26799
26800 assert_eq!(
26801 editor.display_text(cx),
26802 "line1\nline2\nline2",
26803 "Duplicating last line upward should create duplicate above, not on same line"
26804 );
26805
26806 assert_eq!(
26807 editor.selections.display_ranges(cx),
26808 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)],
26809 "Selection should remain on the original line"
26810 );
26811 })
26812 .unwrap();
26813}
26814
26815#[gpui::test]
26816async fn test_copy_line_without_trailing_newline(cx: &mut TestAppContext) {
26817 init_test(cx, |_| {});
26818
26819 let mut cx = EditorTestContext::new(cx).await;
26820
26821 cx.set_state("line1\nline2ˇ");
26822
26823 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
26824
26825 let clipboard_text = cx
26826 .read_from_clipboard()
26827 .and_then(|item| item.text().as_deref().map(str::to_string));
26828
26829 assert_eq!(
26830 clipboard_text,
26831 Some("line2\n".to_string()),
26832 "Copying a line without trailing newline should include a newline"
26833 );
26834
26835 cx.set_state("line1\nˇ");
26836
26837 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
26838
26839 cx.assert_editor_state("line1\nline2\nˇ");
26840}
26841
26842#[gpui::test]
26843async fn test_end_of_editor_context(cx: &mut TestAppContext) {
26844 init_test(cx, |_| {});
26845
26846 let mut cx = EditorTestContext::new(cx).await;
26847
26848 cx.set_state("line1\nline2ˇ");
26849 cx.update_editor(|e, window, cx| {
26850 e.set_mode(EditorMode::SingleLine);
26851 assert!(e.key_context(window, cx).contains("end_of_input"));
26852 });
26853 cx.set_state("ˇline1\nline2");
26854 cx.update_editor(|e, window, cx| {
26855 assert!(!e.key_context(window, cx).contains("end_of_input"));
26856 });
26857 cx.set_state("line1ˇ\nline2");
26858 cx.update_editor(|e, window, cx| {
26859 assert!(!e.key_context(window, cx).contains("end_of_input"));
26860 });
26861}