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 languages::rust_lang;
35use lsp::CompletionParams;
36use multi_buffer::{IndentGuide, PathKey};
37use parking_lot::Mutex;
38use pretty_assertions::{assert_eq, assert_ne};
39use project::{
40 FakeFs,
41 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
42 project_settings::LspSettings,
43};
44use serde_json::{self, json};
45use settings::{
46 AllLanguageSettingsContent, IndentGuideBackgroundColoring, IndentGuideColoring,
47 ProjectSettingsContent,
48};
49use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
50use std::{
51 iter,
52 sync::atomic::{self, AtomicUsize},
53};
54use test::build_editor_with_project;
55use text::ToPoint as _;
56use unindent::Unindent;
57use util::{
58 assert_set_eq, path,
59 rel_path::rel_path,
60 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
61 uri,
62};
63use workspace::{
64 CloseActiveItem, CloseAllItems, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry,
65 OpenOptions, ViewId,
66 invalid_item_view::InvalidItemView,
67 item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
68 register_project_item,
69};
70
71fn display_ranges(editor: &Editor, cx: &mut Context<'_, Editor>) -> Vec<Range<DisplayPoint>> {
72 editor
73 .selections
74 .display_ranges(&editor.display_snapshot(cx))
75}
76
77#[gpui::test]
78fn test_edit_events(cx: &mut TestAppContext) {
79 init_test(cx, |_| {});
80
81 let buffer = cx.new(|cx| {
82 let mut buffer = language::Buffer::local("123456", cx);
83 buffer.set_group_interval(Duration::from_secs(1));
84 buffer
85 });
86
87 let events = Rc::new(RefCell::new(Vec::new()));
88 let editor1 = cx.add_window({
89 let events = events.clone();
90 |window, cx| {
91 let entity = cx.entity();
92 cx.subscribe_in(
93 &entity,
94 window,
95 move |_, _, event: &EditorEvent, _, _| match event {
96 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
97 EditorEvent::BufferEdited => {
98 events.borrow_mut().push(("editor1", "buffer edited"))
99 }
100 _ => {}
101 },
102 )
103 .detach();
104 Editor::for_buffer(buffer.clone(), None, window, cx)
105 }
106 });
107
108 let editor2 = cx.add_window({
109 let events = events.clone();
110 |window, cx| {
111 cx.subscribe_in(
112 &cx.entity(),
113 window,
114 move |_, _, event: &EditorEvent, _, _| match event {
115 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
116 EditorEvent::BufferEdited => {
117 events.borrow_mut().push(("editor2", "buffer edited"))
118 }
119 _ => {}
120 },
121 )
122 .detach();
123 Editor::for_buffer(buffer.clone(), None, window, cx)
124 }
125 });
126
127 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
128
129 // Mutating editor 1 will emit an `Edited` event only for that editor.
130 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
131 assert_eq!(
132 mem::take(&mut *events.borrow_mut()),
133 [
134 ("editor1", "edited"),
135 ("editor1", "buffer edited"),
136 ("editor2", "buffer edited"),
137 ]
138 );
139
140 // Mutating editor 2 will emit an `Edited` event only for that editor.
141 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
142 assert_eq!(
143 mem::take(&mut *events.borrow_mut()),
144 [
145 ("editor2", "edited"),
146 ("editor1", "buffer edited"),
147 ("editor2", "buffer edited"),
148 ]
149 );
150
151 // Undoing on editor 1 will emit an `Edited` event only for that editor.
152 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
153 assert_eq!(
154 mem::take(&mut *events.borrow_mut()),
155 [
156 ("editor1", "edited"),
157 ("editor1", "buffer edited"),
158 ("editor2", "buffer edited"),
159 ]
160 );
161
162 // Redoing on editor 1 will emit an `Edited` event only for that editor.
163 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
164 assert_eq!(
165 mem::take(&mut *events.borrow_mut()),
166 [
167 ("editor1", "edited"),
168 ("editor1", "buffer edited"),
169 ("editor2", "buffer edited"),
170 ]
171 );
172
173 // Undoing on editor 2 will emit an `Edited` event only for that editor.
174 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
175 assert_eq!(
176 mem::take(&mut *events.borrow_mut()),
177 [
178 ("editor2", "edited"),
179 ("editor1", "buffer edited"),
180 ("editor2", "buffer edited"),
181 ]
182 );
183
184 // Redoing on editor 2 will emit an `Edited` event only for that editor.
185 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
186 assert_eq!(
187 mem::take(&mut *events.borrow_mut()),
188 [
189 ("editor2", "edited"),
190 ("editor1", "buffer edited"),
191 ("editor2", "buffer edited"),
192 ]
193 );
194
195 // No event is emitted when the mutation is a no-op.
196 _ = editor2.update(cx, |editor, window, cx| {
197 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
198 s.select_ranges([0..0])
199 });
200
201 editor.backspace(&Backspace, window, cx);
202 });
203 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
204}
205
206#[gpui::test]
207fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
208 init_test(cx, |_| {});
209
210 let mut now = Instant::now();
211 let group_interval = Duration::from_millis(1);
212 let buffer = cx.new(|cx| {
213 let mut buf = language::Buffer::local("123456", cx);
214 buf.set_group_interval(group_interval);
215 buf
216 });
217 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
218 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
219
220 _ = editor.update(cx, |editor, window, cx| {
221 editor.start_transaction_at(now, window, cx);
222 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
223 s.select_ranges([2..4])
224 });
225
226 editor.insert("cd", window, cx);
227 editor.end_transaction_at(now, cx);
228 assert_eq!(editor.text(cx), "12cd56");
229 assert_eq!(
230 editor.selections.ranges(&editor.display_snapshot(cx)),
231 vec![4..4]
232 );
233
234 editor.start_transaction_at(now, window, cx);
235 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
236 s.select_ranges([4..5])
237 });
238 editor.insert("e", window, cx);
239 editor.end_transaction_at(now, cx);
240 assert_eq!(editor.text(cx), "12cde6");
241 assert_eq!(
242 editor.selections.ranges(&editor.display_snapshot(cx)),
243 vec![5..5]
244 );
245
246 now += group_interval + Duration::from_millis(1);
247 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
248 s.select_ranges([2..2])
249 });
250
251 // Simulate an edit in another editor
252 buffer.update(cx, |buffer, cx| {
253 buffer.start_transaction_at(now, cx);
254 buffer.edit([(0..1, "a")], None, cx);
255 buffer.edit([(1..1, "b")], None, cx);
256 buffer.end_transaction_at(now, cx);
257 });
258
259 assert_eq!(editor.text(cx), "ab2cde6");
260 assert_eq!(
261 editor.selections.ranges(&editor.display_snapshot(cx)),
262 vec![3..3]
263 );
264
265 // Last transaction happened past the group interval in a different editor.
266 // Undo it individually and don't restore selections.
267 editor.undo(&Undo, window, cx);
268 assert_eq!(editor.text(cx), "12cde6");
269 assert_eq!(
270 editor.selections.ranges(&editor.display_snapshot(cx)),
271 vec![2..2]
272 );
273
274 // First two transactions happened within the group interval in this editor.
275 // Undo them together and restore selections.
276 editor.undo(&Undo, window, cx);
277 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
278 assert_eq!(editor.text(cx), "123456");
279 assert_eq!(
280 editor.selections.ranges(&editor.display_snapshot(cx)),
281 vec![0..0]
282 );
283
284 // Redo the first two transactions together.
285 editor.redo(&Redo, window, cx);
286 assert_eq!(editor.text(cx), "12cde6");
287 assert_eq!(
288 editor.selections.ranges(&editor.display_snapshot(cx)),
289 vec![5..5]
290 );
291
292 // Redo the last transaction on its own.
293 editor.redo(&Redo, window, cx);
294 assert_eq!(editor.text(cx), "ab2cde6");
295 assert_eq!(
296 editor.selections.ranges(&editor.display_snapshot(cx)),
297 vec![6..6]
298 );
299
300 // Test empty transactions.
301 editor.start_transaction_at(now, window, cx);
302 editor.end_transaction_at(now, cx);
303 editor.undo(&Undo, window, cx);
304 assert_eq!(editor.text(cx), "12cde6");
305 });
306}
307
308#[gpui::test]
309fn test_ime_composition(cx: &mut TestAppContext) {
310 init_test(cx, |_| {});
311
312 let buffer = cx.new(|cx| {
313 let mut buffer = language::Buffer::local("abcde", cx);
314 // Ensure automatic grouping doesn't occur.
315 buffer.set_group_interval(Duration::ZERO);
316 buffer
317 });
318
319 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
320 cx.add_window(|window, cx| {
321 let mut editor = build_editor(buffer.clone(), window, cx);
322
323 // Start a new IME composition.
324 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
325 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
326 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
327 assert_eq!(editor.text(cx), "äbcde");
328 assert_eq!(
329 editor.marked_text_ranges(cx),
330 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
331 );
332
333 // Finalize IME composition.
334 editor.replace_text_in_range(None, "ā", window, cx);
335 assert_eq!(editor.text(cx), "ābcde");
336 assert_eq!(editor.marked_text_ranges(cx), None);
337
338 // IME composition edits are grouped and are undone/redone at once.
339 editor.undo(&Default::default(), window, cx);
340 assert_eq!(editor.text(cx), "abcde");
341 assert_eq!(editor.marked_text_ranges(cx), None);
342 editor.redo(&Default::default(), window, cx);
343 assert_eq!(editor.text(cx), "ābcde");
344 assert_eq!(editor.marked_text_ranges(cx), None);
345
346 // Start a new IME composition.
347 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
348 assert_eq!(
349 editor.marked_text_ranges(cx),
350 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
351 );
352
353 // Undoing during an IME composition cancels it.
354 editor.undo(&Default::default(), window, cx);
355 assert_eq!(editor.text(cx), "ābcde");
356 assert_eq!(editor.marked_text_ranges(cx), None);
357
358 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
359 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
360 assert_eq!(editor.text(cx), "ābcdè");
361 assert_eq!(
362 editor.marked_text_ranges(cx),
363 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
364 );
365
366 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
367 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
368 assert_eq!(editor.text(cx), "ābcdę");
369 assert_eq!(editor.marked_text_ranges(cx), None);
370
371 // Start a new IME composition with multiple cursors.
372 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
373 s.select_ranges([
374 OffsetUtf16(1)..OffsetUtf16(1),
375 OffsetUtf16(3)..OffsetUtf16(3),
376 OffsetUtf16(5)..OffsetUtf16(5),
377 ])
378 });
379 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
380 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
381 assert_eq!(
382 editor.marked_text_ranges(cx),
383 Some(vec![
384 OffsetUtf16(0)..OffsetUtf16(3),
385 OffsetUtf16(4)..OffsetUtf16(7),
386 OffsetUtf16(8)..OffsetUtf16(11)
387 ])
388 );
389
390 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
391 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
392 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
393 assert_eq!(
394 editor.marked_text_ranges(cx),
395 Some(vec![
396 OffsetUtf16(1)..OffsetUtf16(2),
397 OffsetUtf16(5)..OffsetUtf16(6),
398 OffsetUtf16(9)..OffsetUtf16(10)
399 ])
400 );
401
402 // Finalize IME composition with multiple cursors.
403 editor.replace_text_in_range(Some(9..10), "2", window, cx);
404 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
405 assert_eq!(editor.marked_text_ranges(cx), None);
406
407 editor
408 });
409}
410
411#[gpui::test]
412fn test_selection_with_mouse(cx: &mut TestAppContext) {
413 init_test(cx, |_| {});
414
415 let editor = cx.add_window(|window, cx| {
416 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
417 build_editor(buffer, window, cx)
418 });
419
420 _ = editor.update(cx, |editor, window, cx| {
421 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
422 });
423 assert_eq!(
424 editor
425 .update(cx, |editor, _, cx| display_ranges(editor, cx))
426 .unwrap(),
427 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
428 );
429
430 _ = editor.update(cx, |editor, window, cx| {
431 editor.update_selection(
432 DisplayPoint::new(DisplayRow(3), 3),
433 0,
434 gpui::Point::<f32>::default(),
435 window,
436 cx,
437 );
438 });
439
440 assert_eq!(
441 editor
442 .update(cx, |editor, _, cx| display_ranges(editor, cx))
443 .unwrap(),
444 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
445 );
446
447 _ = editor.update(cx, |editor, window, cx| {
448 editor.update_selection(
449 DisplayPoint::new(DisplayRow(1), 1),
450 0,
451 gpui::Point::<f32>::default(),
452 window,
453 cx,
454 );
455 });
456
457 assert_eq!(
458 editor
459 .update(cx, |editor, _, cx| display_ranges(editor, cx))
460 .unwrap(),
461 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
462 );
463
464 _ = editor.update(cx, |editor, window, cx| {
465 editor.end_selection(window, cx);
466 editor.update_selection(
467 DisplayPoint::new(DisplayRow(3), 3),
468 0,
469 gpui::Point::<f32>::default(),
470 window,
471 cx,
472 );
473 });
474
475 assert_eq!(
476 editor
477 .update(cx, |editor, _, cx| display_ranges(editor, cx))
478 .unwrap(),
479 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
480 );
481
482 _ = editor.update(cx, |editor, window, cx| {
483 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
484 editor.update_selection(
485 DisplayPoint::new(DisplayRow(0), 0),
486 0,
487 gpui::Point::<f32>::default(),
488 window,
489 cx,
490 );
491 });
492
493 assert_eq!(
494 editor
495 .update(cx, |editor, _, cx| display_ranges(editor, cx))
496 .unwrap(),
497 [
498 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
499 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
500 ]
501 );
502
503 _ = editor.update(cx, |editor, window, cx| {
504 editor.end_selection(window, cx);
505 });
506
507 assert_eq!(
508 editor
509 .update(cx, |editor, _, cx| display_ranges(editor, cx))
510 .unwrap(),
511 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
512 );
513}
514
515#[gpui::test]
516fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
517 init_test(cx, |_| {});
518
519 let editor = cx.add_window(|window, cx| {
520 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
521 build_editor(buffer, window, cx)
522 });
523
524 _ = editor.update(cx, |editor, window, cx| {
525 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
526 });
527
528 _ = editor.update(cx, |editor, window, cx| {
529 editor.end_selection(window, cx);
530 });
531
532 _ = editor.update(cx, |editor, window, cx| {
533 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
534 });
535
536 _ = editor.update(cx, |editor, window, cx| {
537 editor.end_selection(window, cx);
538 });
539
540 assert_eq!(
541 editor
542 .update(cx, |editor, _, cx| display_ranges(editor, cx))
543 .unwrap(),
544 [
545 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
546 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
547 ]
548 );
549
550 _ = editor.update(cx, |editor, window, cx| {
551 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
552 });
553
554 _ = editor.update(cx, |editor, window, cx| {
555 editor.end_selection(window, cx);
556 });
557
558 assert_eq!(
559 editor
560 .update(cx, |editor, _, cx| display_ranges(editor, cx))
561 .unwrap(),
562 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
563 );
564}
565
566#[gpui::test]
567fn test_canceling_pending_selection(cx: &mut TestAppContext) {
568 init_test(cx, |_| {});
569
570 let editor = cx.add_window(|window, cx| {
571 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
572 build_editor(buffer, window, cx)
573 });
574
575 _ = editor.update(cx, |editor, window, cx| {
576 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
577 assert_eq!(
578 display_ranges(editor, cx),
579 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
580 );
581 });
582
583 _ = editor.update(cx, |editor, window, cx| {
584 editor.update_selection(
585 DisplayPoint::new(DisplayRow(3), 3),
586 0,
587 gpui::Point::<f32>::default(),
588 window,
589 cx,
590 );
591 assert_eq!(
592 display_ranges(editor, cx),
593 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
594 );
595 });
596
597 _ = editor.update(cx, |editor, window, cx| {
598 editor.cancel(&Cancel, window, cx);
599 editor.update_selection(
600 DisplayPoint::new(DisplayRow(1), 1),
601 0,
602 gpui::Point::<f32>::default(),
603 window,
604 cx,
605 );
606 assert_eq!(
607 display_ranges(editor, cx),
608 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
609 );
610 });
611}
612
613#[gpui::test]
614fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
615 init_test(cx, |_| {});
616
617 let editor = cx.add_window(|window, cx| {
618 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
619 build_editor(buffer, window, cx)
620 });
621
622 _ = editor.update(cx, |editor, window, cx| {
623 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
624 assert_eq!(
625 display_ranges(editor, cx),
626 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
627 );
628
629 editor.move_down(&Default::default(), window, cx);
630 assert_eq!(
631 display_ranges(editor, cx),
632 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
633 );
634
635 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
636 assert_eq!(
637 display_ranges(editor, cx),
638 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
639 );
640
641 editor.move_up(&Default::default(), window, cx);
642 assert_eq!(
643 display_ranges(editor, cx),
644 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
645 );
646 });
647}
648
649#[gpui::test]
650fn test_extending_selection(cx: &mut TestAppContext) {
651 init_test(cx, |_| {});
652
653 let editor = cx.add_window(|window, cx| {
654 let buffer = MultiBuffer::build_simple("aaa bbb ccc ddd eee", cx);
655 build_editor(buffer, window, cx)
656 });
657
658 _ = editor.update(cx, |editor, window, cx| {
659 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), false, 1, window, cx);
660 editor.end_selection(window, cx);
661 assert_eq!(
662 display_ranges(editor, cx),
663 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)]
664 );
665
666 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
667 editor.end_selection(window, cx);
668 assert_eq!(
669 display_ranges(editor, cx),
670 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 10)]
671 );
672
673 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
674 editor.end_selection(window, cx);
675 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 2, window, cx);
676 assert_eq!(
677 display_ranges(editor, cx),
678 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 11)]
679 );
680
681 editor.update_selection(
682 DisplayPoint::new(DisplayRow(0), 1),
683 0,
684 gpui::Point::<f32>::default(),
685 window,
686 cx,
687 );
688 editor.end_selection(window, cx);
689 assert_eq!(
690 display_ranges(editor, cx),
691 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 0)]
692 );
693
694 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), true, 1, window, cx);
695 editor.end_selection(window, cx);
696 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), true, 2, window, cx);
697 editor.end_selection(window, cx);
698 assert_eq!(
699 display_ranges(editor, cx),
700 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 7)]
701 );
702
703 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
704 assert_eq!(
705 display_ranges(editor, cx),
706 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 11)]
707 );
708
709 editor.update_selection(
710 DisplayPoint::new(DisplayRow(0), 6),
711 0,
712 gpui::Point::<f32>::default(),
713 window,
714 cx,
715 );
716 assert_eq!(
717 display_ranges(editor, cx),
718 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 7)]
719 );
720
721 editor.update_selection(
722 DisplayPoint::new(DisplayRow(0), 1),
723 0,
724 gpui::Point::<f32>::default(),
725 window,
726 cx,
727 );
728 editor.end_selection(window, cx);
729 assert_eq!(
730 display_ranges(editor, cx),
731 [DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 0)]
732 );
733 });
734}
735
736#[gpui::test]
737fn test_clone(cx: &mut TestAppContext) {
738 init_test(cx, |_| {});
739
740 let (text, selection_ranges) = marked_text_ranges(
741 indoc! {"
742 one
743 two
744 threeˇ
745 four
746 fiveˇ
747 "},
748 true,
749 );
750
751 let editor = cx.add_window(|window, cx| {
752 let buffer = MultiBuffer::build_simple(&text, cx);
753 build_editor(buffer, window, cx)
754 });
755
756 _ = editor.update(cx, |editor, window, cx| {
757 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
758 s.select_ranges(selection_ranges.clone())
759 });
760 editor.fold_creases(
761 vec![
762 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
763 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
764 ],
765 true,
766 window,
767 cx,
768 );
769 });
770
771 let cloned_editor = editor
772 .update(cx, |editor, _, cx| {
773 cx.open_window(Default::default(), |window, cx| {
774 cx.new(|cx| editor.clone(window, cx))
775 })
776 })
777 .unwrap()
778 .unwrap();
779
780 let snapshot = editor
781 .update(cx, |e, window, cx| e.snapshot(window, cx))
782 .unwrap();
783 let cloned_snapshot = cloned_editor
784 .update(cx, |e, window, cx| e.snapshot(window, cx))
785 .unwrap();
786
787 assert_eq!(
788 cloned_editor
789 .update(cx, |e, _, cx| e.display_text(cx))
790 .unwrap(),
791 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
792 );
793 assert_eq!(
794 cloned_snapshot
795 .folds_in_range(0..text.len())
796 .collect::<Vec<_>>(),
797 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
798 );
799 assert_set_eq!(
800 cloned_editor
801 .update(cx, |editor, _, cx| editor
802 .selections
803 .ranges::<Point>(&editor.display_snapshot(cx)))
804 .unwrap(),
805 editor
806 .update(cx, |editor, _, cx| editor
807 .selections
808 .ranges(&editor.display_snapshot(cx)))
809 .unwrap()
810 );
811 assert_set_eq!(
812 cloned_editor
813 .update(cx, |e, _window, cx| e
814 .selections
815 .display_ranges(&e.display_snapshot(cx)))
816 .unwrap(),
817 editor
818 .update(cx, |e, _, cx| e
819 .selections
820 .display_ranges(&e.display_snapshot(cx)))
821 .unwrap()
822 );
823}
824
825#[gpui::test]
826async fn test_navigation_history(cx: &mut TestAppContext) {
827 init_test(cx, |_| {});
828
829 use workspace::item::Item;
830
831 let fs = FakeFs::new(cx.executor());
832 let project = Project::test(fs, [], cx).await;
833 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
834 let pane = workspace
835 .update(cx, |workspace, _, _| workspace.active_pane().clone())
836 .unwrap();
837
838 _ = workspace.update(cx, |_v, window, cx| {
839 cx.new(|cx| {
840 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
841 let mut editor = build_editor(buffer, window, cx);
842 let handle = cx.entity();
843 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
844
845 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
846 editor.nav_history.as_mut().unwrap().pop_backward(cx)
847 }
848
849 // Move the cursor a small distance.
850 // Nothing is added to the navigation history.
851 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
852 s.select_display_ranges([
853 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
854 ])
855 });
856 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
857 s.select_display_ranges([
858 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
859 ])
860 });
861 assert!(pop_history(&mut editor, cx).is_none());
862
863 // Move the cursor a large distance.
864 // The history can jump back to the previous position.
865 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
866 s.select_display_ranges([
867 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
868 ])
869 });
870 let nav_entry = pop_history(&mut editor, cx).unwrap();
871 editor.navigate(nav_entry.data.unwrap(), window, cx);
872 assert_eq!(nav_entry.item.id(), cx.entity_id());
873 assert_eq!(
874 editor
875 .selections
876 .display_ranges(&editor.display_snapshot(cx)),
877 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
878 );
879 assert!(pop_history(&mut editor, cx).is_none());
880
881 // Move the cursor a small distance via the mouse.
882 // Nothing is added to the navigation history.
883 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
884 editor.end_selection(window, cx);
885 assert_eq!(
886 editor
887 .selections
888 .display_ranges(&editor.display_snapshot(cx)),
889 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
890 );
891 assert!(pop_history(&mut editor, cx).is_none());
892
893 // Move the cursor a large distance via the mouse.
894 // The history can jump back to the previous position.
895 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
896 editor.end_selection(window, cx);
897 assert_eq!(
898 editor
899 .selections
900 .display_ranges(&editor.display_snapshot(cx)),
901 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
902 );
903 let nav_entry = pop_history(&mut editor, cx).unwrap();
904 editor.navigate(nav_entry.data.unwrap(), window, cx);
905 assert_eq!(nav_entry.item.id(), cx.entity_id());
906 assert_eq!(
907 editor
908 .selections
909 .display_ranges(&editor.display_snapshot(cx)),
910 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
911 );
912 assert!(pop_history(&mut editor, cx).is_none());
913
914 // Set scroll position to check later
915 editor.set_scroll_position(gpui::Point::<f64>::new(5.5, 5.5), window, cx);
916 let original_scroll_position = editor.scroll_manager.anchor();
917
918 // Jump to the end of the document and adjust scroll
919 editor.move_to_end(&MoveToEnd, window, cx);
920 editor.set_scroll_position(gpui::Point::<f64>::new(-2.5, -0.5), window, cx);
921 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
922
923 let nav_entry = pop_history(&mut editor, cx).unwrap();
924 editor.navigate(nav_entry.data.unwrap(), window, cx);
925 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
926
927 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
928 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
929 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
930 let invalid_point = Point::new(9999, 0);
931 editor.navigate(
932 Box::new(NavigationData {
933 cursor_anchor: invalid_anchor,
934 cursor_position: invalid_point,
935 scroll_anchor: ScrollAnchor {
936 anchor: invalid_anchor,
937 offset: Default::default(),
938 },
939 scroll_top_row: invalid_point.row,
940 }),
941 window,
942 cx,
943 );
944 assert_eq!(
945 editor
946 .selections
947 .display_ranges(&editor.display_snapshot(cx)),
948 &[editor.max_point(cx)..editor.max_point(cx)]
949 );
950 assert_eq!(
951 editor.scroll_position(cx),
952 gpui::Point::new(0., editor.max_point(cx).row().as_f64())
953 );
954
955 editor
956 })
957 });
958}
959
960#[gpui::test]
961fn test_cancel(cx: &mut TestAppContext) {
962 init_test(cx, |_| {});
963
964 let editor = cx.add_window(|window, cx| {
965 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
966 build_editor(buffer, window, cx)
967 });
968
969 _ = editor.update(cx, |editor, window, cx| {
970 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
971 editor.update_selection(
972 DisplayPoint::new(DisplayRow(1), 1),
973 0,
974 gpui::Point::<f32>::default(),
975 window,
976 cx,
977 );
978 editor.end_selection(window, cx);
979
980 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
981 editor.update_selection(
982 DisplayPoint::new(DisplayRow(0), 3),
983 0,
984 gpui::Point::<f32>::default(),
985 window,
986 cx,
987 );
988 editor.end_selection(window, cx);
989 assert_eq!(
990 display_ranges(editor, cx),
991 [
992 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
993 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
994 ]
995 );
996 });
997
998 _ = editor.update(cx, |editor, window, cx| {
999 editor.cancel(&Cancel, window, cx);
1000 assert_eq!(
1001 display_ranges(editor, cx),
1002 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
1003 );
1004 });
1005
1006 _ = editor.update(cx, |editor, window, cx| {
1007 editor.cancel(&Cancel, window, cx);
1008 assert_eq!(
1009 display_ranges(editor, cx),
1010 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
1011 );
1012 });
1013}
1014
1015#[gpui::test]
1016fn test_fold_action(cx: &mut TestAppContext) {
1017 init_test(cx, |_| {});
1018
1019 let editor = cx.add_window(|window, cx| {
1020 let buffer = MultiBuffer::build_simple(
1021 &"
1022 impl Foo {
1023 // Hello!
1024
1025 fn a() {
1026 1
1027 }
1028
1029 fn b() {
1030 2
1031 }
1032
1033 fn c() {
1034 3
1035 }
1036 }
1037 "
1038 .unindent(),
1039 cx,
1040 );
1041 build_editor(buffer, window, cx)
1042 });
1043
1044 _ = editor.update(cx, |editor, window, cx| {
1045 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1046 s.select_display_ranges([
1047 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
1048 ]);
1049 });
1050 editor.fold(&Fold, window, cx);
1051 assert_eq!(
1052 editor.display_text(cx),
1053 "
1054 impl Foo {
1055 // Hello!
1056
1057 fn a() {
1058 1
1059 }
1060
1061 fn b() {⋯
1062 }
1063
1064 fn c() {⋯
1065 }
1066 }
1067 "
1068 .unindent(),
1069 );
1070
1071 editor.fold(&Fold, window, cx);
1072 assert_eq!(
1073 editor.display_text(cx),
1074 "
1075 impl Foo {⋯
1076 }
1077 "
1078 .unindent(),
1079 );
1080
1081 editor.unfold_lines(&UnfoldLines, window, cx);
1082 assert_eq!(
1083 editor.display_text(cx),
1084 "
1085 impl Foo {
1086 // Hello!
1087
1088 fn a() {
1089 1
1090 }
1091
1092 fn b() {⋯
1093 }
1094
1095 fn c() {⋯
1096 }
1097 }
1098 "
1099 .unindent(),
1100 );
1101
1102 editor.unfold_lines(&UnfoldLines, window, cx);
1103 assert_eq!(
1104 editor.display_text(cx),
1105 editor.buffer.read(cx).read(cx).text()
1106 );
1107 });
1108}
1109
1110#[gpui::test]
1111fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
1112 init_test(cx, |_| {});
1113
1114 let editor = cx.add_window(|window, cx| {
1115 let buffer = MultiBuffer::build_simple(
1116 &"
1117 class Foo:
1118 # Hello!
1119
1120 def a():
1121 print(1)
1122
1123 def b():
1124 print(2)
1125
1126 def c():
1127 print(3)
1128 "
1129 .unindent(),
1130 cx,
1131 );
1132 build_editor(buffer, window, cx)
1133 });
1134
1135 _ = editor.update(cx, |editor, window, cx| {
1136 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1137 s.select_display_ranges([
1138 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
1139 ]);
1140 });
1141 editor.fold(&Fold, window, cx);
1142 assert_eq!(
1143 editor.display_text(cx),
1144 "
1145 class Foo:
1146 # Hello!
1147
1148 def a():
1149 print(1)
1150
1151 def b():⋯
1152
1153 def c():⋯
1154 "
1155 .unindent(),
1156 );
1157
1158 editor.fold(&Fold, window, cx);
1159 assert_eq!(
1160 editor.display_text(cx),
1161 "
1162 class Foo:⋯
1163 "
1164 .unindent(),
1165 );
1166
1167 editor.unfold_lines(&UnfoldLines, window, cx);
1168 assert_eq!(
1169 editor.display_text(cx),
1170 "
1171 class Foo:
1172 # Hello!
1173
1174 def a():
1175 print(1)
1176
1177 def b():⋯
1178
1179 def c():⋯
1180 "
1181 .unindent(),
1182 );
1183
1184 editor.unfold_lines(&UnfoldLines, window, cx);
1185 assert_eq!(
1186 editor.display_text(cx),
1187 editor.buffer.read(cx).read(cx).text()
1188 );
1189 });
1190}
1191
1192#[gpui::test]
1193fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1194 init_test(cx, |_| {});
1195
1196 let editor = cx.add_window(|window, cx| {
1197 let buffer = MultiBuffer::build_simple(
1198 &"
1199 class Foo:
1200 # Hello!
1201
1202 def a():
1203 print(1)
1204
1205 def b():
1206 print(2)
1207
1208
1209 def c():
1210 print(3)
1211
1212
1213 "
1214 .unindent(),
1215 cx,
1216 );
1217 build_editor(buffer, window, cx)
1218 });
1219
1220 _ = editor.update(cx, |editor, window, cx| {
1221 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1222 s.select_display_ranges([
1223 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1224 ]);
1225 });
1226 editor.fold(&Fold, window, cx);
1227 assert_eq!(
1228 editor.display_text(cx),
1229 "
1230 class Foo:
1231 # Hello!
1232
1233 def a():
1234 print(1)
1235
1236 def b():⋯
1237
1238
1239 def c():⋯
1240
1241
1242 "
1243 .unindent(),
1244 );
1245
1246 editor.fold(&Fold, window, cx);
1247 assert_eq!(
1248 editor.display_text(cx),
1249 "
1250 class Foo:⋯
1251
1252
1253 "
1254 .unindent(),
1255 );
1256
1257 editor.unfold_lines(&UnfoldLines, window, cx);
1258 assert_eq!(
1259 editor.display_text(cx),
1260 "
1261 class Foo:
1262 # Hello!
1263
1264 def a():
1265 print(1)
1266
1267 def b():⋯
1268
1269
1270 def c():⋯
1271
1272
1273 "
1274 .unindent(),
1275 );
1276
1277 editor.unfold_lines(&UnfoldLines, window, cx);
1278 assert_eq!(
1279 editor.display_text(cx),
1280 editor.buffer.read(cx).read(cx).text()
1281 );
1282 });
1283}
1284
1285#[gpui::test]
1286fn test_fold_at_level(cx: &mut TestAppContext) {
1287 init_test(cx, |_| {});
1288
1289 let editor = cx.add_window(|window, cx| {
1290 let buffer = MultiBuffer::build_simple(
1291 &"
1292 class Foo:
1293 # Hello!
1294
1295 def a():
1296 print(1)
1297
1298 def b():
1299 print(2)
1300
1301
1302 class Bar:
1303 # World!
1304
1305 def a():
1306 print(1)
1307
1308 def b():
1309 print(2)
1310
1311
1312 "
1313 .unindent(),
1314 cx,
1315 );
1316 build_editor(buffer, window, cx)
1317 });
1318
1319 _ = editor.update(cx, |editor, window, cx| {
1320 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1321 assert_eq!(
1322 editor.display_text(cx),
1323 "
1324 class Foo:
1325 # Hello!
1326
1327 def a():⋯
1328
1329 def b():⋯
1330
1331
1332 class Bar:
1333 # World!
1334
1335 def a():⋯
1336
1337 def b():⋯
1338
1339
1340 "
1341 .unindent(),
1342 );
1343
1344 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1345 assert_eq!(
1346 editor.display_text(cx),
1347 "
1348 class Foo:⋯
1349
1350
1351 class Bar:⋯
1352
1353
1354 "
1355 .unindent(),
1356 );
1357
1358 editor.unfold_all(&UnfoldAll, window, cx);
1359 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1360 assert_eq!(
1361 editor.display_text(cx),
1362 "
1363 class Foo:
1364 # Hello!
1365
1366 def a():
1367 print(1)
1368
1369 def b():
1370 print(2)
1371
1372
1373 class Bar:
1374 # World!
1375
1376 def a():
1377 print(1)
1378
1379 def b():
1380 print(2)
1381
1382
1383 "
1384 .unindent(),
1385 );
1386
1387 assert_eq!(
1388 editor.display_text(cx),
1389 editor.buffer.read(cx).read(cx).text()
1390 );
1391 let (_, positions) = marked_text_ranges(
1392 &"
1393 class Foo:
1394 # Hello!
1395
1396 def a():
1397 print(1)
1398
1399 def b():
1400 p«riˇ»nt(2)
1401
1402
1403 class Bar:
1404 # World!
1405
1406 def a():
1407 «ˇprint(1)
1408
1409 def b():
1410 print(2)»
1411
1412
1413 "
1414 .unindent(),
1415 true,
1416 );
1417
1418 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
1419 s.select_ranges(positions)
1420 });
1421
1422 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1423 assert_eq!(
1424 editor.display_text(cx),
1425 "
1426 class Foo:
1427 # Hello!
1428
1429 def a():⋯
1430
1431 def b():
1432 print(2)
1433
1434
1435 class Bar:
1436 # World!
1437
1438 def a():
1439 print(1)
1440
1441 def b():
1442 print(2)
1443
1444
1445 "
1446 .unindent(),
1447 );
1448 });
1449}
1450
1451#[gpui::test]
1452fn test_move_cursor(cx: &mut TestAppContext) {
1453 init_test(cx, |_| {});
1454
1455 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1456 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1457
1458 buffer.update(cx, |buffer, cx| {
1459 buffer.edit(
1460 vec![
1461 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1462 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1463 ],
1464 None,
1465 cx,
1466 );
1467 });
1468 _ = editor.update(cx, |editor, window, cx| {
1469 assert_eq!(
1470 display_ranges(editor, cx),
1471 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1472 );
1473
1474 editor.move_down(&MoveDown, window, cx);
1475 assert_eq!(
1476 display_ranges(editor, cx),
1477 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1478 );
1479
1480 editor.move_right(&MoveRight, window, cx);
1481 assert_eq!(
1482 display_ranges(editor, cx),
1483 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1484 );
1485
1486 editor.move_left(&MoveLeft, window, cx);
1487 assert_eq!(
1488 display_ranges(editor, cx),
1489 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1490 );
1491
1492 editor.move_up(&MoveUp, window, cx);
1493 assert_eq!(
1494 display_ranges(editor, cx),
1495 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1496 );
1497
1498 editor.move_to_end(&MoveToEnd, window, cx);
1499 assert_eq!(
1500 display_ranges(editor, cx),
1501 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1502 );
1503
1504 editor.move_to_beginning(&MoveToBeginning, window, cx);
1505 assert_eq!(
1506 display_ranges(editor, cx),
1507 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1508 );
1509
1510 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1511 s.select_display_ranges([
1512 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1513 ]);
1514 });
1515 editor.select_to_beginning(&SelectToBeginning, window, cx);
1516 assert_eq!(
1517 display_ranges(editor, cx),
1518 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1519 );
1520
1521 editor.select_to_end(&SelectToEnd, window, cx);
1522 assert_eq!(
1523 display_ranges(editor, cx),
1524 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1525 );
1526 });
1527}
1528
1529#[gpui::test]
1530fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1531 init_test(cx, |_| {});
1532
1533 let editor = cx.add_window(|window, cx| {
1534 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1535 build_editor(buffer, window, cx)
1536 });
1537
1538 assert_eq!('🟥'.len_utf8(), 4);
1539 assert_eq!('α'.len_utf8(), 2);
1540
1541 _ = editor.update(cx, |editor, window, cx| {
1542 editor.fold_creases(
1543 vec![
1544 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1545 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1546 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1547 ],
1548 true,
1549 window,
1550 cx,
1551 );
1552 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1553
1554 editor.move_right(&MoveRight, window, cx);
1555 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥".len())]);
1556 editor.move_right(&MoveRight, window, cx);
1557 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥🟧".len())]);
1558 editor.move_right(&MoveRight, window, cx);
1559 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥🟧⋯".len())]);
1560
1561 editor.move_down(&MoveDown, window, cx);
1562 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯e".len())]);
1563 editor.move_left(&MoveLeft, window, cx);
1564 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯".len())]);
1565 editor.move_left(&MoveLeft, window, cx);
1566 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab".len())]);
1567 editor.move_left(&MoveLeft, window, cx);
1568 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "a".len())]);
1569
1570 editor.move_down(&MoveDown, window, cx);
1571 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "α".len())]);
1572 editor.move_right(&MoveRight, window, cx);
1573 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ".len())]);
1574 editor.move_right(&MoveRight, window, cx);
1575 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ⋯".len())]);
1576 editor.move_right(&MoveRight, window, cx);
1577 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ⋯ε".len())]);
1578
1579 editor.move_up(&MoveUp, window, cx);
1580 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯e".len())]);
1581 editor.move_down(&MoveDown, window, cx);
1582 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ⋯ε".len())]);
1583 editor.move_up(&MoveUp, window, cx);
1584 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯e".len())]);
1585
1586 editor.move_up(&MoveUp, window, cx);
1587 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥🟧".len())]);
1588 editor.move_left(&MoveLeft, window, cx);
1589 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥".len())]);
1590 editor.move_left(&MoveLeft, window, cx);
1591 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "".len())]);
1592 });
1593}
1594
1595#[gpui::test]
1596fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1597 init_test(cx, |_| {});
1598
1599 let editor = cx.add_window(|window, cx| {
1600 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1601 build_editor(buffer, window, cx)
1602 });
1603 _ = editor.update(cx, |editor, window, cx| {
1604 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1605 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1606 });
1607
1608 // moving above start of document should move selection to start of document,
1609 // but the next move down should still be at the original goal_x
1610 editor.move_up(&MoveUp, window, cx);
1611 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "".len())]);
1612
1613 editor.move_down(&MoveDown, window, cx);
1614 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "abcd".len())]);
1615
1616 editor.move_down(&MoveDown, window, cx);
1617 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβγ".len())]);
1618
1619 editor.move_down(&MoveDown, window, cx);
1620 assert_eq!(display_ranges(editor, cx), &[empty_range(3, "abcd".len())]);
1621
1622 editor.move_down(&MoveDown, window, cx);
1623 assert_eq!(display_ranges(editor, cx), &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]);
1624
1625 // moving past end of document should not change goal_x
1626 editor.move_down(&MoveDown, window, cx);
1627 assert_eq!(display_ranges(editor, cx), &[empty_range(5, "".len())]);
1628
1629 editor.move_down(&MoveDown, window, cx);
1630 assert_eq!(display_ranges(editor, cx), &[empty_range(5, "".len())]);
1631
1632 editor.move_up(&MoveUp, window, cx);
1633 assert_eq!(display_ranges(editor, cx), &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]);
1634
1635 editor.move_up(&MoveUp, window, cx);
1636 assert_eq!(display_ranges(editor, cx), &[empty_range(3, "abcd".len())]);
1637
1638 editor.move_up(&MoveUp, window, cx);
1639 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβγ".len())]);
1640 });
1641}
1642
1643#[gpui::test]
1644fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1645 init_test(cx, |_| {});
1646 let move_to_beg = MoveToBeginningOfLine {
1647 stop_at_soft_wraps: true,
1648 stop_at_indent: true,
1649 };
1650
1651 let delete_to_beg = DeleteToBeginningOfLine {
1652 stop_at_indent: false,
1653 };
1654
1655 let move_to_end = MoveToEndOfLine {
1656 stop_at_soft_wraps: true,
1657 };
1658
1659 let editor = cx.add_window(|window, cx| {
1660 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1661 build_editor(buffer, window, cx)
1662 });
1663 _ = editor.update(cx, |editor, window, cx| {
1664 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1665 s.select_display_ranges([
1666 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1667 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1668 ]);
1669 });
1670 });
1671
1672 _ = editor.update(cx, |editor, window, cx| {
1673 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1674 assert_eq!(
1675 display_ranges(editor, cx),
1676 &[
1677 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1678 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1679 ]
1680 );
1681 });
1682
1683 _ = editor.update(cx, |editor, window, cx| {
1684 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1685 assert_eq!(
1686 display_ranges(editor, cx),
1687 &[
1688 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1689 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1690 ]
1691 );
1692 });
1693
1694 _ = editor.update(cx, |editor, window, cx| {
1695 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1696 assert_eq!(
1697 display_ranges(editor, cx),
1698 &[
1699 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1700 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1701 ]
1702 );
1703 });
1704
1705 _ = editor.update(cx, |editor, window, cx| {
1706 editor.move_to_end_of_line(&move_to_end, window, cx);
1707 assert_eq!(
1708 display_ranges(editor, cx),
1709 &[
1710 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1711 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1712 ]
1713 );
1714 });
1715
1716 // Moving to the end of line again is a no-op.
1717 _ = editor.update(cx, |editor, window, cx| {
1718 editor.move_to_end_of_line(&move_to_end, window, cx);
1719 assert_eq!(
1720 display_ranges(editor, cx),
1721 &[
1722 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1723 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1724 ]
1725 );
1726 });
1727
1728 _ = editor.update(cx, |editor, window, cx| {
1729 editor.move_left(&MoveLeft, window, cx);
1730 editor.select_to_beginning_of_line(
1731 &SelectToBeginningOfLine {
1732 stop_at_soft_wraps: true,
1733 stop_at_indent: true,
1734 },
1735 window,
1736 cx,
1737 );
1738 assert_eq!(
1739 display_ranges(editor, cx),
1740 &[
1741 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1742 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1743 ]
1744 );
1745 });
1746
1747 _ = editor.update(cx, |editor, window, cx| {
1748 editor.select_to_beginning_of_line(
1749 &SelectToBeginningOfLine {
1750 stop_at_soft_wraps: true,
1751 stop_at_indent: true,
1752 },
1753 window,
1754 cx,
1755 );
1756 assert_eq!(
1757 display_ranges(editor, cx),
1758 &[
1759 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1760 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1761 ]
1762 );
1763 });
1764
1765 _ = editor.update(cx, |editor, window, cx| {
1766 editor.select_to_beginning_of_line(
1767 &SelectToBeginningOfLine {
1768 stop_at_soft_wraps: true,
1769 stop_at_indent: true,
1770 },
1771 window,
1772 cx,
1773 );
1774 assert_eq!(
1775 display_ranges(editor, cx),
1776 &[
1777 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1778 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1779 ]
1780 );
1781 });
1782
1783 _ = editor.update(cx, |editor, window, cx| {
1784 editor.select_to_end_of_line(
1785 &SelectToEndOfLine {
1786 stop_at_soft_wraps: true,
1787 },
1788 window,
1789 cx,
1790 );
1791 assert_eq!(
1792 display_ranges(editor, cx),
1793 &[
1794 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1795 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1796 ]
1797 );
1798 });
1799
1800 _ = editor.update(cx, |editor, window, cx| {
1801 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1802 assert_eq!(editor.display_text(cx), "ab\n de");
1803 assert_eq!(
1804 display_ranges(editor, cx),
1805 &[
1806 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1807 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1808 ]
1809 );
1810 });
1811
1812 _ = editor.update(cx, |editor, window, cx| {
1813 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1814 assert_eq!(editor.display_text(cx), "\n");
1815 assert_eq!(
1816 display_ranges(editor, cx),
1817 &[
1818 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1819 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1820 ]
1821 );
1822 });
1823}
1824
1825#[gpui::test]
1826fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1827 init_test(cx, |_| {});
1828 let move_to_beg = MoveToBeginningOfLine {
1829 stop_at_soft_wraps: false,
1830 stop_at_indent: false,
1831 };
1832
1833 let move_to_end = MoveToEndOfLine {
1834 stop_at_soft_wraps: false,
1835 };
1836
1837 let editor = cx.add_window(|window, cx| {
1838 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1839 build_editor(buffer, window, cx)
1840 });
1841
1842 _ = editor.update(cx, |editor, window, cx| {
1843 editor.set_wrap_width(Some(140.0.into()), cx);
1844
1845 // We expect the following lines after wrapping
1846 // ```
1847 // thequickbrownfox
1848 // jumpedoverthelazydo
1849 // gs
1850 // ```
1851 // The final `gs` was soft-wrapped onto a new line.
1852 assert_eq!(
1853 "thequickbrownfox\njumpedoverthelaz\nydogs",
1854 editor.display_text(cx),
1855 );
1856
1857 // First, let's assert behavior on the first line, that was not soft-wrapped.
1858 // Start the cursor at the `k` on the first line
1859 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1860 s.select_display_ranges([
1861 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1862 ]);
1863 });
1864
1865 // Moving to the beginning of the line should put us at the beginning of the line.
1866 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1867 assert_eq!(
1868 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1869 display_ranges(editor, cx)
1870 );
1871
1872 // Moving to the end of the line should put us at the end of the line.
1873 editor.move_to_end_of_line(&move_to_end, window, cx);
1874 assert_eq!(
1875 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1876 display_ranges(editor, cx)
1877 );
1878
1879 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1880 // Start the cursor at the last line (`y` that was wrapped to a new line)
1881 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1882 s.select_display_ranges([
1883 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1884 ]);
1885 });
1886
1887 // Moving to the beginning of the line should put us at the start of the second line of
1888 // display text, i.e., the `j`.
1889 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1890 assert_eq!(
1891 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1892 display_ranges(editor, cx)
1893 );
1894
1895 // Moving to the beginning of the line again should be a no-op.
1896 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1897 assert_eq!(
1898 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1899 display_ranges(editor, cx)
1900 );
1901
1902 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1903 // next display line.
1904 editor.move_to_end_of_line(&move_to_end, window, cx);
1905 assert_eq!(
1906 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1907 display_ranges(editor, cx)
1908 );
1909
1910 // Moving to the end of the line again should be a no-op.
1911 editor.move_to_end_of_line(&move_to_end, window, cx);
1912 assert_eq!(
1913 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1914 display_ranges(editor, cx)
1915 );
1916 });
1917}
1918
1919#[gpui::test]
1920fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1921 init_test(cx, |_| {});
1922
1923 let move_to_beg = MoveToBeginningOfLine {
1924 stop_at_soft_wraps: true,
1925 stop_at_indent: true,
1926 };
1927
1928 let select_to_beg = SelectToBeginningOfLine {
1929 stop_at_soft_wraps: true,
1930 stop_at_indent: true,
1931 };
1932
1933 let delete_to_beg = DeleteToBeginningOfLine {
1934 stop_at_indent: true,
1935 };
1936
1937 let move_to_end = MoveToEndOfLine {
1938 stop_at_soft_wraps: false,
1939 };
1940
1941 let editor = cx.add_window(|window, cx| {
1942 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1943 build_editor(buffer, window, cx)
1944 });
1945
1946 _ = editor.update(cx, |editor, window, cx| {
1947 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1948 s.select_display_ranges([
1949 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1950 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1951 ]);
1952 });
1953
1954 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1955 // and the second cursor at the first non-whitespace character in the line.
1956 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1957 assert_eq!(
1958 display_ranges(editor, cx),
1959 &[
1960 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1961 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1962 ]
1963 );
1964
1965 // Moving to the beginning of the line again should be a no-op for the first cursor,
1966 // and should move the second cursor to the beginning of the line.
1967 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1968 assert_eq!(
1969 display_ranges(editor, cx),
1970 &[
1971 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1972 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1973 ]
1974 );
1975
1976 // Moving to the beginning of the line again should still be a no-op for the first cursor,
1977 // and should move the second cursor back to the first non-whitespace character in the line.
1978 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1979 assert_eq!(
1980 display_ranges(editor, cx),
1981 &[
1982 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1983 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1984 ]
1985 );
1986
1987 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
1988 // and to the first non-whitespace character in the line for the second cursor.
1989 editor.move_to_end_of_line(&move_to_end, window, cx);
1990 editor.move_left(&MoveLeft, window, cx);
1991 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1992 assert_eq!(
1993 display_ranges(editor, cx),
1994 &[
1995 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1996 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1997 ]
1998 );
1999
2000 // Selecting to the beginning of the line again should be a no-op for the first cursor,
2001 // and should select to the beginning of the line for the second cursor.
2002 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
2003 assert_eq!(
2004 display_ranges(editor, cx),
2005 &[
2006 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
2007 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
2008 ]
2009 );
2010
2011 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
2012 // and should delete to the first non-whitespace character in the line for the second cursor.
2013 editor.move_to_end_of_line(&move_to_end, window, cx);
2014 editor.move_left(&MoveLeft, window, cx);
2015 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
2016 assert_eq!(editor.text(cx), "c\n f");
2017 });
2018}
2019
2020#[gpui::test]
2021fn test_beginning_of_line_with_cursor_between_line_start_and_indent(cx: &mut TestAppContext) {
2022 init_test(cx, |_| {});
2023
2024 let move_to_beg = MoveToBeginningOfLine {
2025 stop_at_soft_wraps: true,
2026 stop_at_indent: true,
2027 };
2028
2029 let editor = cx.add_window(|window, cx| {
2030 let buffer = MultiBuffer::build_simple(" hello\nworld", cx);
2031 build_editor(buffer, window, cx)
2032 });
2033
2034 _ = editor.update(cx, |editor, window, cx| {
2035 // test cursor between line_start and indent_start
2036 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2037 s.select_display_ranges([
2038 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3)
2039 ]);
2040 });
2041
2042 // cursor should move to line_start
2043 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2044 assert_eq!(
2045 display_ranges(editor, cx),
2046 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
2047 );
2048
2049 // cursor should move to indent_start
2050 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2051 assert_eq!(
2052 display_ranges(editor, cx),
2053 &[DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 4)]
2054 );
2055
2056 // cursor should move to back to line_start
2057 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2058 assert_eq!(
2059 display_ranges(editor, cx),
2060 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
2061 );
2062 });
2063}
2064
2065#[gpui::test]
2066fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
2067 init_test(cx, |_| {});
2068
2069 let editor = cx.add_window(|window, cx| {
2070 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
2071 build_editor(buffer, window, cx)
2072 });
2073 _ = editor.update(cx, |editor, window, cx| {
2074 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2075 s.select_display_ranges([
2076 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
2077 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
2078 ])
2079 });
2080 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2081 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
2082
2083 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2084 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
2085
2086 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2087 assert_selection_ranges("use ˇstd::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
2088
2089 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2090 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
2091
2092 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2093 assert_selection_ranges("ˇuse std::str::{foo, ˇbar}\n\n {baz.qux()}", editor, cx);
2094
2095 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2096 assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
2097
2098 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2099 assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
2100
2101 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2102 assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
2103
2104 editor.move_right(&MoveRight, window, cx);
2105 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2106 assert_selection_ranges(
2107 "use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}",
2108 editor,
2109 cx,
2110 );
2111
2112 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2113 assert_selection_ranges(
2114 "use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
2115 editor,
2116 cx,
2117 );
2118
2119 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
2120 assert_selection_ranges(
2121 "use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
2122 editor,
2123 cx,
2124 );
2125 });
2126}
2127
2128#[gpui::test]
2129fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
2130 init_test(cx, |_| {});
2131
2132 let editor = cx.add_window(|window, cx| {
2133 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
2134 build_editor(buffer, window, cx)
2135 });
2136
2137 _ = editor.update(cx, |editor, window, cx| {
2138 editor.set_wrap_width(Some(140.0.into()), cx);
2139 assert_eq!(
2140 editor.display_text(cx),
2141 "use one::{\n two::three::\n four::five\n};"
2142 );
2143
2144 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2145 s.select_display_ranges([
2146 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
2147 ]);
2148 });
2149
2150 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2151 assert_eq!(
2152 display_ranges(editor, cx),
2153 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
2154 );
2155
2156 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2157 assert_eq!(
2158 display_ranges(editor, cx),
2159 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2160 );
2161
2162 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2163 assert_eq!(
2164 display_ranges(editor, cx),
2165 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2166 );
2167
2168 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2169 assert_eq!(
2170 display_ranges(editor, cx),
2171 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2172 );
2173
2174 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2175 assert_eq!(
2176 display_ranges(editor, cx),
2177 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2178 );
2179
2180 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2181 assert_eq!(
2182 display_ranges(editor, cx),
2183 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2184 );
2185 });
2186}
2187
2188#[gpui::test]
2189async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2190 init_test(cx, |_| {});
2191 let mut cx = EditorTestContext::new(cx).await;
2192
2193 let line_height = cx.editor(|editor, window, _| {
2194 editor
2195 .style()
2196 .unwrap()
2197 .text
2198 .line_height_in_pixels(window.rem_size())
2199 });
2200 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2201
2202 cx.set_state(
2203 &r#"ˇone
2204 two
2205
2206 three
2207 fourˇ
2208 five
2209
2210 six"#
2211 .unindent(),
2212 );
2213
2214 cx.update_editor(|editor, window, cx| {
2215 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2216 });
2217 cx.assert_editor_state(
2218 &r#"one
2219 two
2220 ˇ
2221 three
2222 four
2223 five
2224 ˇ
2225 six"#
2226 .unindent(),
2227 );
2228
2229 cx.update_editor(|editor, window, cx| {
2230 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2231 });
2232 cx.assert_editor_state(
2233 &r#"one
2234 two
2235
2236 three
2237 four
2238 five
2239 ˇ
2240 sixˇ"#
2241 .unindent(),
2242 );
2243
2244 cx.update_editor(|editor, window, cx| {
2245 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2246 });
2247 cx.assert_editor_state(
2248 &r#"one
2249 two
2250
2251 three
2252 four
2253 five
2254
2255 sixˇ"#
2256 .unindent(),
2257 );
2258
2259 cx.update_editor(|editor, window, cx| {
2260 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2261 });
2262 cx.assert_editor_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_start_of_paragraph(&MoveToStartOfParagraph, 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_start_of_paragraph(&MoveToStartOfParagraph, 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
2305#[gpui::test]
2306async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2307 init_test(cx, |_| {});
2308 let mut cx = EditorTestContext::new(cx).await;
2309 let line_height = cx.editor(|editor, window, _| {
2310 editor
2311 .style()
2312 .unwrap()
2313 .text
2314 .line_height_in_pixels(window.rem_size())
2315 });
2316 let window = cx.window;
2317 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2318
2319 cx.set_state(
2320 r#"ˇone
2321 two
2322 three
2323 four
2324 five
2325 six
2326 seven
2327 eight
2328 nine
2329 ten
2330 "#,
2331 );
2332
2333 cx.update_editor(|editor, window, cx| {
2334 assert_eq!(
2335 editor.snapshot(window, cx).scroll_position(),
2336 gpui::Point::new(0., 0.)
2337 );
2338 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2339 assert_eq!(
2340 editor.snapshot(window, cx).scroll_position(),
2341 gpui::Point::new(0., 3.)
2342 );
2343 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2344 assert_eq!(
2345 editor.snapshot(window, cx).scroll_position(),
2346 gpui::Point::new(0., 6.)
2347 );
2348 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2349 assert_eq!(
2350 editor.snapshot(window, cx).scroll_position(),
2351 gpui::Point::new(0., 3.)
2352 );
2353
2354 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2355 assert_eq!(
2356 editor.snapshot(window, cx).scroll_position(),
2357 gpui::Point::new(0., 1.)
2358 );
2359 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2360 assert_eq!(
2361 editor.snapshot(window, cx).scroll_position(),
2362 gpui::Point::new(0., 3.)
2363 );
2364 });
2365}
2366
2367#[gpui::test]
2368async fn test_autoscroll(cx: &mut TestAppContext) {
2369 init_test(cx, |_| {});
2370 let mut cx = EditorTestContext::new(cx).await;
2371
2372 let line_height = cx.update_editor(|editor, window, cx| {
2373 editor.set_vertical_scroll_margin(2, cx);
2374 editor
2375 .style()
2376 .unwrap()
2377 .text
2378 .line_height_in_pixels(window.rem_size())
2379 });
2380 let window = cx.window;
2381 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2382
2383 cx.set_state(
2384 r#"ˇone
2385 two
2386 three
2387 four
2388 five
2389 six
2390 seven
2391 eight
2392 nine
2393 ten
2394 "#,
2395 );
2396 cx.update_editor(|editor, window, cx| {
2397 assert_eq!(
2398 editor.snapshot(window, cx).scroll_position(),
2399 gpui::Point::new(0., 0.0)
2400 );
2401 });
2402
2403 // Add a cursor below the visible area. Since both cursors cannot fit
2404 // on screen, the editor autoscrolls to reveal the newest cursor, and
2405 // allows the vertical scroll margin below that cursor.
2406 cx.update_editor(|editor, window, cx| {
2407 editor.change_selections(Default::default(), window, cx, |selections| {
2408 selections.select_ranges([
2409 Point::new(0, 0)..Point::new(0, 0),
2410 Point::new(6, 0)..Point::new(6, 0),
2411 ]);
2412 })
2413 });
2414 cx.update_editor(|editor, window, cx| {
2415 assert_eq!(
2416 editor.snapshot(window, cx).scroll_position(),
2417 gpui::Point::new(0., 3.0)
2418 );
2419 });
2420
2421 // Move down. The editor cursor scrolls down to track the newest cursor.
2422 cx.update_editor(|editor, window, cx| {
2423 editor.move_down(&Default::default(), window, cx);
2424 });
2425 cx.update_editor(|editor, window, cx| {
2426 assert_eq!(
2427 editor.snapshot(window, cx).scroll_position(),
2428 gpui::Point::new(0., 4.0)
2429 );
2430 });
2431
2432 // Add a cursor above the visible area. Since both cursors fit on screen,
2433 // the editor scrolls to show both.
2434 cx.update_editor(|editor, window, cx| {
2435 editor.change_selections(Default::default(), window, cx, |selections| {
2436 selections.select_ranges([
2437 Point::new(1, 0)..Point::new(1, 0),
2438 Point::new(6, 0)..Point::new(6, 0),
2439 ]);
2440 })
2441 });
2442 cx.update_editor(|editor, window, cx| {
2443 assert_eq!(
2444 editor.snapshot(window, cx).scroll_position(),
2445 gpui::Point::new(0., 1.0)
2446 );
2447 });
2448}
2449
2450#[gpui::test]
2451async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2452 init_test(cx, |_| {});
2453 let mut cx = EditorTestContext::new(cx).await;
2454
2455 let line_height = cx.editor(|editor, window, _cx| {
2456 editor
2457 .style()
2458 .unwrap()
2459 .text
2460 .line_height_in_pixels(window.rem_size())
2461 });
2462 let window = cx.window;
2463 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2464 cx.set_state(
2465 &r#"
2466 ˇone
2467 two
2468 threeˇ
2469 four
2470 five
2471 six
2472 seven
2473 eight
2474 nine
2475 ten
2476 "#
2477 .unindent(),
2478 );
2479
2480 cx.update_editor(|editor, window, cx| {
2481 editor.move_page_down(&MovePageDown::default(), window, cx)
2482 });
2483 cx.assert_editor_state(
2484 &r#"
2485 one
2486 two
2487 three
2488 ˇfour
2489 five
2490 sixˇ
2491 seven
2492 eight
2493 nine
2494 ten
2495 "#
2496 .unindent(),
2497 );
2498
2499 cx.update_editor(|editor, window, cx| {
2500 editor.move_page_down(&MovePageDown::default(), window, cx)
2501 });
2502 cx.assert_editor_state(
2503 &r#"
2504 one
2505 two
2506 three
2507 four
2508 five
2509 six
2510 ˇseven
2511 eight
2512 nineˇ
2513 ten
2514 "#
2515 .unindent(),
2516 );
2517
2518 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2519 cx.assert_editor_state(
2520 &r#"
2521 one
2522 two
2523 three
2524 ˇfour
2525 five
2526 sixˇ
2527 seven
2528 eight
2529 nine
2530 ten
2531 "#
2532 .unindent(),
2533 );
2534
2535 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2536 cx.assert_editor_state(
2537 &r#"
2538 ˇone
2539 two
2540 threeˇ
2541 four
2542 five
2543 six
2544 seven
2545 eight
2546 nine
2547 ten
2548 "#
2549 .unindent(),
2550 );
2551
2552 // Test select collapsing
2553 cx.update_editor(|editor, window, cx| {
2554 editor.move_page_down(&MovePageDown::default(), window, cx);
2555 editor.move_page_down(&MovePageDown::default(), window, cx);
2556 editor.move_page_down(&MovePageDown::default(), window, cx);
2557 });
2558 cx.assert_editor_state(
2559 &r#"
2560 one
2561 two
2562 three
2563 four
2564 five
2565 six
2566 seven
2567 eight
2568 nine
2569 ˇten
2570 ˇ"#
2571 .unindent(),
2572 );
2573}
2574
2575#[gpui::test]
2576async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2577 init_test(cx, |_| {});
2578 let mut cx = EditorTestContext::new(cx).await;
2579 cx.set_state("one «two threeˇ» four");
2580 cx.update_editor(|editor, window, cx| {
2581 editor.delete_to_beginning_of_line(
2582 &DeleteToBeginningOfLine {
2583 stop_at_indent: false,
2584 },
2585 window,
2586 cx,
2587 );
2588 assert_eq!(editor.text(cx), " four");
2589 });
2590}
2591
2592#[gpui::test]
2593async fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2594 init_test(cx, |_| {});
2595
2596 let mut cx = EditorTestContext::new(cx).await;
2597
2598 // For an empty selection, the preceding word fragment is deleted.
2599 // For non-empty selections, only selected characters are deleted.
2600 cx.set_state("onˇe two t«hreˇ»e four");
2601 cx.update_editor(|editor, window, cx| {
2602 editor.delete_to_previous_word_start(
2603 &DeleteToPreviousWordStart {
2604 ignore_newlines: false,
2605 ignore_brackets: false,
2606 },
2607 window,
2608 cx,
2609 );
2610 });
2611 cx.assert_editor_state("ˇe two tˇe four");
2612
2613 cx.set_state("e tˇwo te «fˇ»our");
2614 cx.update_editor(|editor, window, cx| {
2615 editor.delete_to_next_word_end(
2616 &DeleteToNextWordEnd {
2617 ignore_newlines: false,
2618 ignore_brackets: false,
2619 },
2620 window,
2621 cx,
2622 );
2623 });
2624 cx.assert_editor_state("e tˇ te ˇour");
2625}
2626
2627#[gpui::test]
2628async fn test_delete_whitespaces(cx: &mut TestAppContext) {
2629 init_test(cx, |_| {});
2630
2631 let mut cx = EditorTestContext::new(cx).await;
2632
2633 cx.set_state("here is some text ˇwith a space");
2634 cx.update_editor(|editor, window, cx| {
2635 editor.delete_to_previous_word_start(
2636 &DeleteToPreviousWordStart {
2637 ignore_newlines: false,
2638 ignore_brackets: true,
2639 },
2640 window,
2641 cx,
2642 );
2643 });
2644 // Continuous whitespace sequences are removed entirely, words behind them are not affected by the deletion action.
2645 cx.assert_editor_state("here is some textˇwith a space");
2646
2647 cx.set_state("here is some text ˇwith a space");
2648 cx.update_editor(|editor, window, cx| {
2649 editor.delete_to_previous_word_start(
2650 &DeleteToPreviousWordStart {
2651 ignore_newlines: false,
2652 ignore_brackets: false,
2653 },
2654 window,
2655 cx,
2656 );
2657 });
2658 cx.assert_editor_state("here is some textˇwith a space");
2659
2660 cx.set_state("here is some textˇ with a space");
2661 cx.update_editor(|editor, window, cx| {
2662 editor.delete_to_next_word_end(
2663 &DeleteToNextWordEnd {
2664 ignore_newlines: false,
2665 ignore_brackets: true,
2666 },
2667 window,
2668 cx,
2669 );
2670 });
2671 // Same happens in the other direction.
2672 cx.assert_editor_state("here is some textˇwith a space");
2673
2674 cx.set_state("here is some textˇ with a space");
2675 cx.update_editor(|editor, window, cx| {
2676 editor.delete_to_next_word_end(
2677 &DeleteToNextWordEnd {
2678 ignore_newlines: false,
2679 ignore_brackets: false,
2680 },
2681 window,
2682 cx,
2683 );
2684 });
2685 cx.assert_editor_state("here is some textˇwith a space");
2686
2687 cx.set_state("here is some textˇ with a space");
2688 cx.update_editor(|editor, window, cx| {
2689 editor.delete_to_next_word_end(
2690 &DeleteToNextWordEnd {
2691 ignore_newlines: true,
2692 ignore_brackets: false,
2693 },
2694 window,
2695 cx,
2696 );
2697 });
2698 cx.assert_editor_state("here is some textˇwith a space");
2699 cx.update_editor(|editor, window, cx| {
2700 editor.delete_to_previous_word_start(
2701 &DeleteToPreviousWordStart {
2702 ignore_newlines: true,
2703 ignore_brackets: false,
2704 },
2705 window,
2706 cx,
2707 );
2708 });
2709 cx.assert_editor_state("here is some ˇwith a space");
2710 cx.update_editor(|editor, window, cx| {
2711 editor.delete_to_previous_word_start(
2712 &DeleteToPreviousWordStart {
2713 ignore_newlines: true,
2714 ignore_brackets: false,
2715 },
2716 window,
2717 cx,
2718 );
2719 });
2720 // Single whitespaces are removed with the word behind them.
2721 cx.assert_editor_state("here is ˇwith a space");
2722 cx.update_editor(|editor, window, cx| {
2723 editor.delete_to_previous_word_start(
2724 &DeleteToPreviousWordStart {
2725 ignore_newlines: true,
2726 ignore_brackets: false,
2727 },
2728 window,
2729 cx,
2730 );
2731 });
2732 cx.assert_editor_state("here ˇwith a space");
2733 cx.update_editor(|editor, window, cx| {
2734 editor.delete_to_previous_word_start(
2735 &DeleteToPreviousWordStart {
2736 ignore_newlines: true,
2737 ignore_brackets: false,
2738 },
2739 window,
2740 cx,
2741 );
2742 });
2743 cx.assert_editor_state("ˇwith a space");
2744 cx.update_editor(|editor, window, cx| {
2745 editor.delete_to_previous_word_start(
2746 &DeleteToPreviousWordStart {
2747 ignore_newlines: true,
2748 ignore_brackets: false,
2749 },
2750 window,
2751 cx,
2752 );
2753 });
2754 cx.assert_editor_state("ˇwith a space");
2755 cx.update_editor(|editor, window, cx| {
2756 editor.delete_to_next_word_end(
2757 &DeleteToNextWordEnd {
2758 ignore_newlines: true,
2759 ignore_brackets: false,
2760 },
2761 window,
2762 cx,
2763 );
2764 });
2765 // Same happens in the other direction.
2766 cx.assert_editor_state("ˇ a space");
2767 cx.update_editor(|editor, window, cx| {
2768 editor.delete_to_next_word_end(
2769 &DeleteToNextWordEnd {
2770 ignore_newlines: true,
2771 ignore_brackets: false,
2772 },
2773 window,
2774 cx,
2775 );
2776 });
2777 cx.assert_editor_state("ˇ space");
2778 cx.update_editor(|editor, window, cx| {
2779 editor.delete_to_next_word_end(
2780 &DeleteToNextWordEnd {
2781 ignore_newlines: true,
2782 ignore_brackets: false,
2783 },
2784 window,
2785 cx,
2786 );
2787 });
2788 cx.assert_editor_state("ˇ");
2789 cx.update_editor(|editor, window, cx| {
2790 editor.delete_to_next_word_end(
2791 &DeleteToNextWordEnd {
2792 ignore_newlines: true,
2793 ignore_brackets: false,
2794 },
2795 window,
2796 cx,
2797 );
2798 });
2799 cx.assert_editor_state("ˇ");
2800 cx.update_editor(|editor, window, cx| {
2801 editor.delete_to_previous_word_start(
2802 &DeleteToPreviousWordStart {
2803 ignore_newlines: true,
2804 ignore_brackets: false,
2805 },
2806 window,
2807 cx,
2808 );
2809 });
2810 cx.assert_editor_state("ˇ");
2811}
2812
2813#[gpui::test]
2814async fn test_delete_to_bracket(cx: &mut TestAppContext) {
2815 init_test(cx, |_| {});
2816
2817 let language = Arc::new(
2818 Language::new(
2819 LanguageConfig {
2820 brackets: BracketPairConfig {
2821 pairs: vec![
2822 BracketPair {
2823 start: "\"".to_string(),
2824 end: "\"".to_string(),
2825 close: true,
2826 surround: true,
2827 newline: false,
2828 },
2829 BracketPair {
2830 start: "(".to_string(),
2831 end: ")".to_string(),
2832 close: true,
2833 surround: true,
2834 newline: true,
2835 },
2836 ],
2837 ..BracketPairConfig::default()
2838 },
2839 ..LanguageConfig::default()
2840 },
2841 Some(tree_sitter_rust::LANGUAGE.into()),
2842 )
2843 .with_brackets_query(
2844 r#"
2845 ("(" @open ")" @close)
2846 ("\"" @open "\"" @close)
2847 "#,
2848 )
2849 .unwrap(),
2850 );
2851
2852 let mut cx = EditorTestContext::new(cx).await;
2853 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2854
2855 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2856 cx.update_editor(|editor, window, cx| {
2857 editor.delete_to_previous_word_start(
2858 &DeleteToPreviousWordStart {
2859 ignore_newlines: true,
2860 ignore_brackets: false,
2861 },
2862 window,
2863 cx,
2864 );
2865 });
2866 // Deletion stops before brackets if asked to not ignore them.
2867 cx.assert_editor_state(r#"macro!("ˇCOMMENT");"#);
2868 cx.update_editor(|editor, window, cx| {
2869 editor.delete_to_previous_word_start(
2870 &DeleteToPreviousWordStart {
2871 ignore_newlines: true,
2872 ignore_brackets: false,
2873 },
2874 window,
2875 cx,
2876 );
2877 });
2878 // Deletion has to remove a single bracket and then stop again.
2879 cx.assert_editor_state(r#"macro!(ˇCOMMENT");"#);
2880
2881 cx.update_editor(|editor, window, cx| {
2882 editor.delete_to_previous_word_start(
2883 &DeleteToPreviousWordStart {
2884 ignore_newlines: true,
2885 ignore_brackets: false,
2886 },
2887 window,
2888 cx,
2889 );
2890 });
2891 cx.assert_editor_state(r#"macro!ˇCOMMENT");"#);
2892
2893 cx.update_editor(|editor, window, cx| {
2894 editor.delete_to_previous_word_start(
2895 &DeleteToPreviousWordStart {
2896 ignore_newlines: true,
2897 ignore_brackets: false,
2898 },
2899 window,
2900 cx,
2901 );
2902 });
2903 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2904
2905 cx.update_editor(|editor, window, cx| {
2906 editor.delete_to_previous_word_start(
2907 &DeleteToPreviousWordStart {
2908 ignore_newlines: true,
2909 ignore_brackets: false,
2910 },
2911 window,
2912 cx,
2913 );
2914 });
2915 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2916
2917 cx.update_editor(|editor, window, cx| {
2918 editor.delete_to_next_word_end(
2919 &DeleteToNextWordEnd {
2920 ignore_newlines: true,
2921 ignore_brackets: false,
2922 },
2923 window,
2924 cx,
2925 );
2926 });
2927 // Brackets on the right are not paired anymore, hence deletion does not stop at them
2928 cx.assert_editor_state(r#"ˇ");"#);
2929
2930 cx.update_editor(|editor, window, cx| {
2931 editor.delete_to_next_word_end(
2932 &DeleteToNextWordEnd {
2933 ignore_newlines: true,
2934 ignore_brackets: false,
2935 },
2936 window,
2937 cx,
2938 );
2939 });
2940 cx.assert_editor_state(r#"ˇ"#);
2941
2942 cx.update_editor(|editor, window, cx| {
2943 editor.delete_to_next_word_end(
2944 &DeleteToNextWordEnd {
2945 ignore_newlines: true,
2946 ignore_brackets: false,
2947 },
2948 window,
2949 cx,
2950 );
2951 });
2952 cx.assert_editor_state(r#"ˇ"#);
2953
2954 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2955 cx.update_editor(|editor, window, cx| {
2956 editor.delete_to_previous_word_start(
2957 &DeleteToPreviousWordStart {
2958 ignore_newlines: true,
2959 ignore_brackets: true,
2960 },
2961 window,
2962 cx,
2963 );
2964 });
2965 cx.assert_editor_state(r#"macroˇCOMMENT");"#);
2966}
2967
2968#[gpui::test]
2969fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2970 init_test(cx, |_| {});
2971
2972 let editor = cx.add_window(|window, cx| {
2973 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2974 build_editor(buffer, window, cx)
2975 });
2976 let del_to_prev_word_start = DeleteToPreviousWordStart {
2977 ignore_newlines: false,
2978 ignore_brackets: false,
2979 };
2980 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2981 ignore_newlines: true,
2982 ignore_brackets: false,
2983 };
2984
2985 _ = editor.update(cx, |editor, window, cx| {
2986 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2987 s.select_display_ranges([
2988 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2989 ])
2990 });
2991 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2992 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2993 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2994 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2995 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2996 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2997 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2998 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
2999 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
3000 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
3001 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
3002 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3003 });
3004}
3005
3006#[gpui::test]
3007fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
3008 init_test(cx, |_| {});
3009
3010 let editor = cx.add_window(|window, cx| {
3011 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
3012 build_editor(buffer, window, cx)
3013 });
3014 let del_to_next_word_end = DeleteToNextWordEnd {
3015 ignore_newlines: false,
3016 ignore_brackets: false,
3017 };
3018 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
3019 ignore_newlines: true,
3020 ignore_brackets: false,
3021 };
3022
3023 _ = editor.update(cx, |editor, window, cx| {
3024 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3025 s.select_display_ranges([
3026 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
3027 ])
3028 });
3029 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3030 assert_eq!(
3031 editor.buffer.read(cx).read(cx).text(),
3032 "one\n two\nthree\n four"
3033 );
3034 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3035 assert_eq!(
3036 editor.buffer.read(cx).read(cx).text(),
3037 "\n two\nthree\n four"
3038 );
3039 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3040 assert_eq!(
3041 editor.buffer.read(cx).read(cx).text(),
3042 "two\nthree\n four"
3043 );
3044 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3045 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
3046 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3047 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
3048 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3049 assert_eq!(editor.buffer.read(cx).read(cx).text(), "four");
3050 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3051 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3052 });
3053}
3054
3055#[gpui::test]
3056fn test_newline(cx: &mut TestAppContext) {
3057 init_test(cx, |_| {});
3058
3059 let editor = cx.add_window(|window, cx| {
3060 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
3061 build_editor(buffer, window, cx)
3062 });
3063
3064 _ = editor.update(cx, |editor, window, cx| {
3065 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3066 s.select_display_ranges([
3067 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
3068 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
3069 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
3070 ])
3071 });
3072
3073 editor.newline(&Newline, window, cx);
3074 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
3075 });
3076}
3077
3078#[gpui::test]
3079async fn test_newline_yaml(cx: &mut TestAppContext) {
3080 init_test(cx, |_| {});
3081
3082 let mut cx = EditorTestContext::new(cx).await;
3083 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3084 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3085
3086 // Object (between 2 fields)
3087 cx.set_state(indoc! {"
3088 test:ˇ
3089 hello: bye"});
3090 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3091 cx.assert_editor_state(indoc! {"
3092 test:
3093 ˇ
3094 hello: bye"});
3095
3096 // Object (first and single line)
3097 cx.set_state(indoc! {"
3098 test:ˇ"});
3099 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3100 cx.assert_editor_state(indoc! {"
3101 test:
3102 ˇ"});
3103
3104 // Array with objects (after first element)
3105 cx.set_state(indoc! {"
3106 test:
3107 - foo: barˇ"});
3108 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3109 cx.assert_editor_state(indoc! {"
3110 test:
3111 - foo: bar
3112 ˇ"});
3113
3114 // Array with objects and comment
3115 cx.set_state(indoc! {"
3116 test:
3117 - foo: bar
3118 - bar: # testˇ"});
3119 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3120 cx.assert_editor_state(indoc! {"
3121 test:
3122 - foo: bar
3123 - bar: # test
3124 ˇ"});
3125
3126 // Array with objects (after second element)
3127 cx.set_state(indoc! {"
3128 test:
3129 - foo: bar
3130 - bar: fooˇ"});
3131 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3132 cx.assert_editor_state(indoc! {"
3133 test:
3134 - foo: bar
3135 - bar: foo
3136 ˇ"});
3137
3138 // Array with strings (after first element)
3139 cx.set_state(indoc! {"
3140 test:
3141 - fooˇ"});
3142 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3143 cx.assert_editor_state(indoc! {"
3144 test:
3145 - foo
3146 ˇ"});
3147}
3148
3149#[gpui::test]
3150fn test_newline_with_old_selections(cx: &mut TestAppContext) {
3151 init_test(cx, |_| {});
3152
3153 let editor = cx.add_window(|window, cx| {
3154 let buffer = MultiBuffer::build_simple(
3155 "
3156 a
3157 b(
3158 X
3159 )
3160 c(
3161 X
3162 )
3163 "
3164 .unindent()
3165 .as_str(),
3166 cx,
3167 );
3168 let mut editor = build_editor(buffer, window, cx);
3169 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3170 s.select_ranges([
3171 Point::new(2, 4)..Point::new(2, 5),
3172 Point::new(5, 4)..Point::new(5, 5),
3173 ])
3174 });
3175 editor
3176 });
3177
3178 _ = editor.update(cx, |editor, window, cx| {
3179 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3180 editor.buffer.update(cx, |buffer, cx| {
3181 buffer.edit(
3182 [
3183 (Point::new(1, 2)..Point::new(3, 0), ""),
3184 (Point::new(4, 2)..Point::new(6, 0), ""),
3185 ],
3186 None,
3187 cx,
3188 );
3189 assert_eq!(
3190 buffer.read(cx).text(),
3191 "
3192 a
3193 b()
3194 c()
3195 "
3196 .unindent()
3197 );
3198 });
3199 assert_eq!(
3200 editor.selections.ranges(&editor.display_snapshot(cx)),
3201 &[
3202 Point::new(1, 2)..Point::new(1, 2),
3203 Point::new(2, 2)..Point::new(2, 2),
3204 ],
3205 );
3206
3207 editor.newline(&Newline, window, cx);
3208 assert_eq!(
3209 editor.text(cx),
3210 "
3211 a
3212 b(
3213 )
3214 c(
3215 )
3216 "
3217 .unindent()
3218 );
3219
3220 // The selections are moved after the inserted newlines
3221 assert_eq!(
3222 editor.selections.ranges(&editor.display_snapshot(cx)),
3223 &[
3224 Point::new(2, 0)..Point::new(2, 0),
3225 Point::new(4, 0)..Point::new(4, 0),
3226 ],
3227 );
3228 });
3229}
3230
3231#[gpui::test]
3232async fn test_newline_above(cx: &mut TestAppContext) {
3233 init_test(cx, |settings| {
3234 settings.defaults.tab_size = NonZeroU32::new(4)
3235 });
3236
3237 let language = Arc::new(
3238 Language::new(
3239 LanguageConfig::default(),
3240 Some(tree_sitter_rust::LANGUAGE.into()),
3241 )
3242 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3243 .unwrap(),
3244 );
3245
3246 let mut cx = EditorTestContext::new(cx).await;
3247 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3248 cx.set_state(indoc! {"
3249 const a: ˇA = (
3250 (ˇ
3251 «const_functionˇ»(ˇ),
3252 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3253 )ˇ
3254 ˇ);ˇ
3255 "});
3256
3257 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
3258 cx.assert_editor_state(indoc! {"
3259 ˇ
3260 const a: A = (
3261 ˇ
3262 (
3263 ˇ
3264 ˇ
3265 const_function(),
3266 ˇ
3267 ˇ
3268 ˇ
3269 ˇ
3270 something_else,
3271 ˇ
3272 )
3273 ˇ
3274 ˇ
3275 );
3276 "});
3277}
3278
3279#[gpui::test]
3280async fn test_newline_below(cx: &mut TestAppContext) {
3281 init_test(cx, |settings| {
3282 settings.defaults.tab_size = NonZeroU32::new(4)
3283 });
3284
3285 let language = Arc::new(
3286 Language::new(
3287 LanguageConfig::default(),
3288 Some(tree_sitter_rust::LANGUAGE.into()),
3289 )
3290 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3291 .unwrap(),
3292 );
3293
3294 let mut cx = EditorTestContext::new(cx).await;
3295 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3296 cx.set_state(indoc! {"
3297 const a: ˇA = (
3298 (ˇ
3299 «const_functionˇ»(ˇ),
3300 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3301 )ˇ
3302 ˇ);ˇ
3303 "});
3304
3305 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
3306 cx.assert_editor_state(indoc! {"
3307 const a: A = (
3308 ˇ
3309 (
3310 ˇ
3311 const_function(),
3312 ˇ
3313 ˇ
3314 something_else,
3315 ˇ
3316 ˇ
3317 ˇ
3318 ˇ
3319 )
3320 ˇ
3321 );
3322 ˇ
3323 ˇ
3324 "});
3325}
3326
3327#[gpui::test]
3328async fn test_newline_comments(cx: &mut TestAppContext) {
3329 init_test(cx, |settings| {
3330 settings.defaults.tab_size = NonZeroU32::new(4)
3331 });
3332
3333 let language = Arc::new(Language::new(
3334 LanguageConfig {
3335 line_comments: vec!["// ".into()],
3336 ..LanguageConfig::default()
3337 },
3338 None,
3339 ));
3340 {
3341 let mut cx = EditorTestContext::new(cx).await;
3342 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3343 cx.set_state(indoc! {"
3344 // Fooˇ
3345 "});
3346
3347 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3348 cx.assert_editor_state(indoc! {"
3349 // Foo
3350 // ˇ
3351 "});
3352 // Ensure that we add comment prefix when existing line contains space
3353 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3354 cx.assert_editor_state(
3355 indoc! {"
3356 // Foo
3357 //s
3358 // ˇ
3359 "}
3360 .replace("s", " ") // s is used as space placeholder to prevent format on save
3361 .as_str(),
3362 );
3363 // Ensure that we add comment prefix when existing line does not contain space
3364 cx.set_state(indoc! {"
3365 // Foo
3366 //ˇ
3367 "});
3368 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3369 cx.assert_editor_state(indoc! {"
3370 // Foo
3371 //
3372 // ˇ
3373 "});
3374 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
3375 cx.set_state(indoc! {"
3376 ˇ// Foo
3377 "});
3378 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3379 cx.assert_editor_state(indoc! {"
3380
3381 ˇ// Foo
3382 "});
3383 }
3384 // Ensure that comment continuations can be disabled.
3385 update_test_language_settings(cx, |settings| {
3386 settings.defaults.extend_comment_on_newline = Some(false);
3387 });
3388 let mut cx = EditorTestContext::new(cx).await;
3389 cx.set_state(indoc! {"
3390 // Fooˇ
3391 "});
3392 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3393 cx.assert_editor_state(indoc! {"
3394 // Foo
3395 ˇ
3396 "});
3397}
3398
3399#[gpui::test]
3400async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
3401 init_test(cx, |settings| {
3402 settings.defaults.tab_size = NonZeroU32::new(4)
3403 });
3404
3405 let language = Arc::new(Language::new(
3406 LanguageConfig {
3407 line_comments: vec!["// ".into(), "/// ".into()],
3408 ..LanguageConfig::default()
3409 },
3410 None,
3411 ));
3412 {
3413 let mut cx = EditorTestContext::new(cx).await;
3414 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3415 cx.set_state(indoc! {"
3416 //ˇ
3417 "});
3418 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3419 cx.assert_editor_state(indoc! {"
3420 //
3421 // ˇ
3422 "});
3423
3424 cx.set_state(indoc! {"
3425 ///ˇ
3426 "});
3427 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3428 cx.assert_editor_state(indoc! {"
3429 ///
3430 /// ˇ
3431 "});
3432 }
3433}
3434
3435#[gpui::test]
3436async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
3437 init_test(cx, |settings| {
3438 settings.defaults.tab_size = NonZeroU32::new(4)
3439 });
3440
3441 let language = Arc::new(
3442 Language::new(
3443 LanguageConfig {
3444 documentation_comment: Some(language::BlockCommentConfig {
3445 start: "/**".into(),
3446 end: "*/".into(),
3447 prefix: "* ".into(),
3448 tab_size: 1,
3449 }),
3450
3451 ..LanguageConfig::default()
3452 },
3453 Some(tree_sitter_rust::LANGUAGE.into()),
3454 )
3455 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
3456 .unwrap(),
3457 );
3458
3459 {
3460 let mut cx = EditorTestContext::new(cx).await;
3461 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3462 cx.set_state(indoc! {"
3463 /**ˇ
3464 "});
3465
3466 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3467 cx.assert_editor_state(indoc! {"
3468 /**
3469 * ˇ
3470 "});
3471 // Ensure that if cursor is before the comment start,
3472 // we do not actually insert a comment prefix.
3473 cx.set_state(indoc! {"
3474 ˇ/**
3475 "});
3476 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3477 cx.assert_editor_state(indoc! {"
3478
3479 ˇ/**
3480 "});
3481 // Ensure that if cursor is between it doesn't add comment prefix.
3482 cx.set_state(indoc! {"
3483 /*ˇ*
3484 "});
3485 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3486 cx.assert_editor_state(indoc! {"
3487 /*
3488 ˇ*
3489 "});
3490 // Ensure that if suffix exists on same line after cursor it adds new line.
3491 cx.set_state(indoc! {"
3492 /**ˇ*/
3493 "});
3494 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3495 cx.assert_editor_state(indoc! {"
3496 /**
3497 * ˇ
3498 */
3499 "});
3500 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3501 cx.set_state(indoc! {"
3502 /**ˇ */
3503 "});
3504 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3505 cx.assert_editor_state(indoc! {"
3506 /**
3507 * ˇ
3508 */
3509 "});
3510 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3511 cx.set_state(indoc! {"
3512 /** ˇ*/
3513 "});
3514 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3515 cx.assert_editor_state(
3516 indoc! {"
3517 /**s
3518 * ˇ
3519 */
3520 "}
3521 .replace("s", " ") // s is used as space placeholder to prevent format on save
3522 .as_str(),
3523 );
3524 // Ensure that delimiter space is preserved when newline on already
3525 // spaced delimiter.
3526 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3527 cx.assert_editor_state(
3528 indoc! {"
3529 /**s
3530 *s
3531 * ˇ
3532 */
3533 "}
3534 .replace("s", " ") // s is used as space placeholder to prevent format on save
3535 .as_str(),
3536 );
3537 // Ensure that delimiter space is preserved when space is not
3538 // on existing delimiter.
3539 cx.set_state(indoc! {"
3540 /**
3541 *ˇ
3542 */
3543 "});
3544 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3545 cx.assert_editor_state(indoc! {"
3546 /**
3547 *
3548 * ˇ
3549 */
3550 "});
3551 // Ensure that if suffix exists on same line after cursor it
3552 // doesn't add extra new line if prefix is not on same line.
3553 cx.set_state(indoc! {"
3554 /**
3555 ˇ*/
3556 "});
3557 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3558 cx.assert_editor_state(indoc! {"
3559 /**
3560
3561 ˇ*/
3562 "});
3563 // Ensure that it detects suffix after existing prefix.
3564 cx.set_state(indoc! {"
3565 /**ˇ/
3566 "});
3567 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3568 cx.assert_editor_state(indoc! {"
3569 /**
3570 ˇ/
3571 "});
3572 // Ensure that if suffix exists on same line before
3573 // cursor it does not add comment prefix.
3574 cx.set_state(indoc! {"
3575 /** */ˇ
3576 "});
3577 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3578 cx.assert_editor_state(indoc! {"
3579 /** */
3580 ˇ
3581 "});
3582 // Ensure that if suffix exists on same line before
3583 // cursor it does not add comment prefix.
3584 cx.set_state(indoc! {"
3585 /**
3586 *
3587 */ˇ
3588 "});
3589 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3590 cx.assert_editor_state(indoc! {"
3591 /**
3592 *
3593 */
3594 ˇ
3595 "});
3596
3597 // Ensure that inline comment followed by code
3598 // doesn't add comment prefix on newline
3599 cx.set_state(indoc! {"
3600 /** */ textˇ
3601 "});
3602 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3603 cx.assert_editor_state(indoc! {"
3604 /** */ text
3605 ˇ
3606 "});
3607
3608 // Ensure that text after comment end tag
3609 // doesn't add comment prefix on newline
3610 cx.set_state(indoc! {"
3611 /**
3612 *
3613 */ˇtext
3614 "});
3615 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3616 cx.assert_editor_state(indoc! {"
3617 /**
3618 *
3619 */
3620 ˇtext
3621 "});
3622
3623 // Ensure if not comment block it doesn't
3624 // add comment prefix on newline
3625 cx.set_state(indoc! {"
3626 * textˇ
3627 "});
3628 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3629 cx.assert_editor_state(indoc! {"
3630 * text
3631 ˇ
3632 "});
3633 }
3634 // Ensure that comment continuations can be disabled.
3635 update_test_language_settings(cx, |settings| {
3636 settings.defaults.extend_comment_on_newline = Some(false);
3637 });
3638 let mut cx = EditorTestContext::new(cx).await;
3639 cx.set_state(indoc! {"
3640 /**ˇ
3641 "});
3642 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3643 cx.assert_editor_state(indoc! {"
3644 /**
3645 ˇ
3646 "});
3647}
3648
3649#[gpui::test]
3650async fn test_newline_comments_with_block_comment(cx: &mut TestAppContext) {
3651 init_test(cx, |settings| {
3652 settings.defaults.tab_size = NonZeroU32::new(4)
3653 });
3654
3655 let lua_language = Arc::new(Language::new(
3656 LanguageConfig {
3657 line_comments: vec!["--".into()],
3658 block_comment: Some(language::BlockCommentConfig {
3659 start: "--[[".into(),
3660 prefix: "".into(),
3661 end: "]]".into(),
3662 tab_size: 0,
3663 }),
3664 ..LanguageConfig::default()
3665 },
3666 None,
3667 ));
3668
3669 let mut cx = EditorTestContext::new(cx).await;
3670 cx.update_buffer(|buffer, cx| buffer.set_language(Some(lua_language), cx));
3671
3672 // Line with line comment should extend
3673 cx.set_state(indoc! {"
3674 --ˇ
3675 "});
3676 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3677 cx.assert_editor_state(indoc! {"
3678 --
3679 --ˇ
3680 "});
3681
3682 // Line with block comment that matches line comment should not extend
3683 cx.set_state(indoc! {"
3684 --[[ˇ
3685 "});
3686 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3687 cx.assert_editor_state(indoc! {"
3688 --[[
3689 ˇ
3690 "});
3691}
3692
3693#[gpui::test]
3694fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3695 init_test(cx, |_| {});
3696
3697 let editor = cx.add_window(|window, cx| {
3698 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3699 let mut editor = build_editor(buffer, window, cx);
3700 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3701 s.select_ranges([3..4, 11..12, 19..20])
3702 });
3703 editor
3704 });
3705
3706 _ = editor.update(cx, |editor, window, cx| {
3707 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3708 editor.buffer.update(cx, |buffer, cx| {
3709 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3710 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3711 });
3712 assert_eq!(
3713 editor.selections.ranges(&editor.display_snapshot(cx)),
3714 &[2..2, 7..7, 12..12],
3715 );
3716
3717 editor.insert("Z", window, cx);
3718 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3719
3720 // The selections are moved after the inserted characters
3721 assert_eq!(
3722 editor.selections.ranges(&editor.display_snapshot(cx)),
3723 &[3..3, 9..9, 15..15],
3724 );
3725 });
3726}
3727
3728#[gpui::test]
3729async fn test_tab(cx: &mut TestAppContext) {
3730 init_test(cx, |settings| {
3731 settings.defaults.tab_size = NonZeroU32::new(3)
3732 });
3733
3734 let mut cx = EditorTestContext::new(cx).await;
3735 cx.set_state(indoc! {"
3736 ˇabˇc
3737 ˇ🏀ˇ🏀ˇefg
3738 dˇ
3739 "});
3740 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3741 cx.assert_editor_state(indoc! {"
3742 ˇab ˇc
3743 ˇ🏀 ˇ🏀 ˇefg
3744 d ˇ
3745 "});
3746
3747 cx.set_state(indoc! {"
3748 a
3749 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3750 "});
3751 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3752 cx.assert_editor_state(indoc! {"
3753 a
3754 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3755 "});
3756}
3757
3758#[gpui::test]
3759async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3760 init_test(cx, |_| {});
3761
3762 let mut cx = EditorTestContext::new(cx).await;
3763 let language = Arc::new(
3764 Language::new(
3765 LanguageConfig::default(),
3766 Some(tree_sitter_rust::LANGUAGE.into()),
3767 )
3768 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3769 .unwrap(),
3770 );
3771 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3772
3773 // test when all cursors are not at suggested indent
3774 // then simply move to their suggested indent location
3775 cx.set_state(indoc! {"
3776 const a: B = (
3777 c(
3778 ˇ
3779 ˇ )
3780 );
3781 "});
3782 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3783 cx.assert_editor_state(indoc! {"
3784 const a: B = (
3785 c(
3786 ˇ
3787 ˇ)
3788 );
3789 "});
3790
3791 // test cursor already at suggested indent not moving when
3792 // other cursors are yet to reach their suggested indents
3793 cx.set_state(indoc! {"
3794 ˇ
3795 const a: B = (
3796 c(
3797 d(
3798 ˇ
3799 )
3800 ˇ
3801 ˇ )
3802 );
3803 "});
3804 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3805 cx.assert_editor_state(indoc! {"
3806 ˇ
3807 const a: B = (
3808 c(
3809 d(
3810 ˇ
3811 )
3812 ˇ
3813 ˇ)
3814 );
3815 "});
3816 // test when all cursors are at suggested indent then tab is inserted
3817 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3818 cx.assert_editor_state(indoc! {"
3819 ˇ
3820 const a: B = (
3821 c(
3822 d(
3823 ˇ
3824 )
3825 ˇ
3826 ˇ)
3827 );
3828 "});
3829
3830 // test when current indent is less than suggested indent,
3831 // we adjust line to match suggested indent and move cursor to it
3832 //
3833 // when no other cursor is at word boundary, all of them should move
3834 cx.set_state(indoc! {"
3835 const a: B = (
3836 c(
3837 d(
3838 ˇ
3839 ˇ )
3840 ˇ )
3841 );
3842 "});
3843 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3844 cx.assert_editor_state(indoc! {"
3845 const a: B = (
3846 c(
3847 d(
3848 ˇ
3849 ˇ)
3850 ˇ)
3851 );
3852 "});
3853
3854 // test when current indent is less than suggested indent,
3855 // we adjust line to match suggested indent and move cursor to it
3856 //
3857 // when some other cursor is at word boundary, it should not move
3858 cx.set_state(indoc! {"
3859 const a: B = (
3860 c(
3861 d(
3862 ˇ
3863 ˇ )
3864 ˇ)
3865 );
3866 "});
3867 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3868 cx.assert_editor_state(indoc! {"
3869 const a: B = (
3870 c(
3871 d(
3872 ˇ
3873 ˇ)
3874 ˇ)
3875 );
3876 "});
3877
3878 // test when current indent is more than suggested indent,
3879 // we just move cursor to current indent instead of suggested indent
3880 //
3881 // when no other cursor is at word boundary, all of them should move
3882 cx.set_state(indoc! {"
3883 const a: B = (
3884 c(
3885 d(
3886 ˇ
3887 ˇ )
3888 ˇ )
3889 );
3890 "});
3891 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3892 cx.assert_editor_state(indoc! {"
3893 const a: B = (
3894 c(
3895 d(
3896 ˇ
3897 ˇ)
3898 ˇ)
3899 );
3900 "});
3901 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3902 cx.assert_editor_state(indoc! {"
3903 const a: B = (
3904 c(
3905 d(
3906 ˇ
3907 ˇ)
3908 ˇ)
3909 );
3910 "});
3911
3912 // test when current indent is more than suggested indent,
3913 // we just move cursor to current indent instead of suggested indent
3914 //
3915 // when some other cursor is at word boundary, it doesn't move
3916 cx.set_state(indoc! {"
3917 const a: B = (
3918 c(
3919 d(
3920 ˇ
3921 ˇ )
3922 ˇ)
3923 );
3924 "});
3925 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3926 cx.assert_editor_state(indoc! {"
3927 const a: B = (
3928 c(
3929 d(
3930 ˇ
3931 ˇ)
3932 ˇ)
3933 );
3934 "});
3935
3936 // handle auto-indent when there are multiple cursors on the same line
3937 cx.set_state(indoc! {"
3938 const a: B = (
3939 c(
3940 ˇ ˇ
3941 ˇ )
3942 );
3943 "});
3944 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3945 cx.assert_editor_state(indoc! {"
3946 const a: B = (
3947 c(
3948 ˇ
3949 ˇ)
3950 );
3951 "});
3952}
3953
3954#[gpui::test]
3955async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3956 init_test(cx, |settings| {
3957 settings.defaults.tab_size = NonZeroU32::new(3)
3958 });
3959
3960 let mut cx = EditorTestContext::new(cx).await;
3961 cx.set_state(indoc! {"
3962 ˇ
3963 \t ˇ
3964 \t ˇ
3965 \t ˇ
3966 \t \t\t \t \t\t \t\t \t \t ˇ
3967 "});
3968
3969 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3970 cx.assert_editor_state(indoc! {"
3971 ˇ
3972 \t ˇ
3973 \t ˇ
3974 \t ˇ
3975 \t \t\t \t \t\t \t\t \t \t ˇ
3976 "});
3977}
3978
3979#[gpui::test]
3980async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3981 init_test(cx, |settings| {
3982 settings.defaults.tab_size = NonZeroU32::new(4)
3983 });
3984
3985 let language = Arc::new(
3986 Language::new(
3987 LanguageConfig::default(),
3988 Some(tree_sitter_rust::LANGUAGE.into()),
3989 )
3990 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3991 .unwrap(),
3992 );
3993
3994 let mut cx = EditorTestContext::new(cx).await;
3995 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3996 cx.set_state(indoc! {"
3997 fn a() {
3998 if b {
3999 \t ˇc
4000 }
4001 }
4002 "});
4003
4004 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4005 cx.assert_editor_state(indoc! {"
4006 fn a() {
4007 if b {
4008 ˇc
4009 }
4010 }
4011 "});
4012}
4013
4014#[gpui::test]
4015async fn test_indent_outdent(cx: &mut TestAppContext) {
4016 init_test(cx, |settings| {
4017 settings.defaults.tab_size = NonZeroU32::new(4);
4018 });
4019
4020 let mut cx = EditorTestContext::new(cx).await;
4021
4022 cx.set_state(indoc! {"
4023 «oneˇ» «twoˇ»
4024 three
4025 four
4026 "});
4027 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4028 cx.assert_editor_state(indoc! {"
4029 «oneˇ» «twoˇ»
4030 three
4031 four
4032 "});
4033
4034 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4035 cx.assert_editor_state(indoc! {"
4036 «oneˇ» «twoˇ»
4037 three
4038 four
4039 "});
4040
4041 // select across line ending
4042 cx.set_state(indoc! {"
4043 one two
4044 t«hree
4045 ˇ» four
4046 "});
4047 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4048 cx.assert_editor_state(indoc! {"
4049 one two
4050 t«hree
4051 ˇ» four
4052 "});
4053
4054 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4055 cx.assert_editor_state(indoc! {"
4056 one two
4057 t«hree
4058 ˇ» four
4059 "});
4060
4061 // Ensure that indenting/outdenting works when the cursor is at column 0.
4062 cx.set_state(indoc! {"
4063 one two
4064 ˇthree
4065 four
4066 "});
4067 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4068 cx.assert_editor_state(indoc! {"
4069 one two
4070 ˇthree
4071 four
4072 "});
4073
4074 cx.set_state(indoc! {"
4075 one two
4076 ˇ three
4077 four
4078 "});
4079 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4080 cx.assert_editor_state(indoc! {"
4081 one two
4082 ˇthree
4083 four
4084 "});
4085}
4086
4087#[gpui::test]
4088async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) {
4089 // This is a regression test for issue #33761
4090 init_test(cx, |_| {});
4091
4092 let mut cx = EditorTestContext::new(cx).await;
4093 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
4094 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
4095
4096 cx.set_state(
4097 r#"ˇ# ingress:
4098ˇ# api:
4099ˇ# enabled: false
4100ˇ# pathType: Prefix
4101ˇ# console:
4102ˇ# enabled: false
4103ˇ# pathType: Prefix
4104"#,
4105 );
4106
4107 // Press tab to indent all lines
4108 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4109
4110 cx.assert_editor_state(
4111 r#" ˇ# ingress:
4112 ˇ# api:
4113 ˇ# enabled: false
4114 ˇ# pathType: Prefix
4115 ˇ# console:
4116 ˇ# enabled: false
4117 ˇ# pathType: Prefix
4118"#,
4119 );
4120}
4121
4122#[gpui::test]
4123async fn test_indent_yaml_non_comments_with_multiple_cursors(cx: &mut TestAppContext) {
4124 // This is a test to make sure our fix for issue #33761 didn't break anything
4125 init_test(cx, |_| {});
4126
4127 let mut cx = EditorTestContext::new(cx).await;
4128 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
4129 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
4130
4131 cx.set_state(
4132 r#"ˇingress:
4133ˇ api:
4134ˇ enabled: false
4135ˇ pathType: Prefix
4136"#,
4137 );
4138
4139 // Press tab to indent all lines
4140 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4141
4142 cx.assert_editor_state(
4143 r#"ˇingress:
4144 ˇapi:
4145 ˇenabled: false
4146 ˇpathType: Prefix
4147"#,
4148 );
4149}
4150
4151#[gpui::test]
4152async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
4153 init_test(cx, |settings| {
4154 settings.defaults.hard_tabs = Some(true);
4155 });
4156
4157 let mut cx = EditorTestContext::new(cx).await;
4158
4159 // select two ranges on one line
4160 cx.set_state(indoc! {"
4161 «oneˇ» «twoˇ»
4162 three
4163 four
4164 "});
4165 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4166 cx.assert_editor_state(indoc! {"
4167 \t«oneˇ» «twoˇ»
4168 three
4169 four
4170 "});
4171 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4172 cx.assert_editor_state(indoc! {"
4173 \t\t«oneˇ» «twoˇ»
4174 three
4175 four
4176 "});
4177 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4178 cx.assert_editor_state(indoc! {"
4179 \t«oneˇ» «twoˇ»
4180 three
4181 four
4182 "});
4183 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4184 cx.assert_editor_state(indoc! {"
4185 «oneˇ» «twoˇ»
4186 three
4187 four
4188 "});
4189
4190 // select across a line ending
4191 cx.set_state(indoc! {"
4192 one two
4193 t«hree
4194 ˇ»four
4195 "});
4196 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4197 cx.assert_editor_state(indoc! {"
4198 one two
4199 \tt«hree
4200 ˇ»four
4201 "});
4202 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4203 cx.assert_editor_state(indoc! {"
4204 one two
4205 \t\tt«hree
4206 ˇ»four
4207 "});
4208 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4209 cx.assert_editor_state(indoc! {"
4210 one two
4211 \tt«hree
4212 ˇ»four
4213 "});
4214 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4215 cx.assert_editor_state(indoc! {"
4216 one two
4217 t«hree
4218 ˇ»four
4219 "});
4220
4221 // Ensure that indenting/outdenting works when the cursor is at column 0.
4222 cx.set_state(indoc! {"
4223 one two
4224 ˇthree
4225 four
4226 "});
4227 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4228 cx.assert_editor_state(indoc! {"
4229 one two
4230 ˇthree
4231 four
4232 "});
4233 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4234 cx.assert_editor_state(indoc! {"
4235 one two
4236 \tˇthree
4237 four
4238 "});
4239 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4240 cx.assert_editor_state(indoc! {"
4241 one two
4242 ˇthree
4243 four
4244 "});
4245}
4246
4247#[gpui::test]
4248fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
4249 init_test(cx, |settings| {
4250 settings.languages.0.extend([
4251 (
4252 "TOML".into(),
4253 LanguageSettingsContent {
4254 tab_size: NonZeroU32::new(2),
4255 ..Default::default()
4256 },
4257 ),
4258 (
4259 "Rust".into(),
4260 LanguageSettingsContent {
4261 tab_size: NonZeroU32::new(4),
4262 ..Default::default()
4263 },
4264 ),
4265 ]);
4266 });
4267
4268 let toml_language = Arc::new(Language::new(
4269 LanguageConfig {
4270 name: "TOML".into(),
4271 ..Default::default()
4272 },
4273 None,
4274 ));
4275 let rust_language = Arc::new(Language::new(
4276 LanguageConfig {
4277 name: "Rust".into(),
4278 ..Default::default()
4279 },
4280 None,
4281 ));
4282
4283 let toml_buffer =
4284 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
4285 let rust_buffer =
4286 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
4287 let multibuffer = cx.new(|cx| {
4288 let mut multibuffer = MultiBuffer::new(ReadWrite);
4289 multibuffer.push_excerpts(
4290 toml_buffer.clone(),
4291 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
4292 cx,
4293 );
4294 multibuffer.push_excerpts(
4295 rust_buffer.clone(),
4296 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
4297 cx,
4298 );
4299 multibuffer
4300 });
4301
4302 cx.add_window(|window, cx| {
4303 let mut editor = build_editor(multibuffer, window, cx);
4304
4305 assert_eq!(
4306 editor.text(cx),
4307 indoc! {"
4308 a = 1
4309 b = 2
4310
4311 const c: usize = 3;
4312 "}
4313 );
4314
4315 select_ranges(
4316 &mut editor,
4317 indoc! {"
4318 «aˇ» = 1
4319 b = 2
4320
4321 «const c:ˇ» usize = 3;
4322 "},
4323 window,
4324 cx,
4325 );
4326
4327 editor.tab(&Tab, 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 editor.backtab(&Backtab, window, cx);
4339 assert_text_with_selections(
4340 &mut editor,
4341 indoc! {"
4342 «aˇ» = 1
4343 b = 2
4344
4345 «const c:ˇ» usize = 3;
4346 "},
4347 cx,
4348 );
4349
4350 editor
4351 });
4352}
4353
4354#[gpui::test]
4355async fn test_backspace(cx: &mut TestAppContext) {
4356 init_test(cx, |_| {});
4357
4358 let mut cx = EditorTestContext::new(cx).await;
4359
4360 // Basic backspace
4361 cx.set_state(indoc! {"
4362 onˇe two three
4363 fou«rˇ» five six
4364 seven «ˇeight nine
4365 »ten
4366 "});
4367 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4368 cx.assert_editor_state(indoc! {"
4369 oˇe two three
4370 fouˇ five six
4371 seven ˇten
4372 "});
4373
4374 // Test backspace inside and around indents
4375 cx.set_state(indoc! {"
4376 zero
4377 ˇone
4378 ˇtwo
4379 ˇ ˇ ˇ three
4380 ˇ ˇ four
4381 "});
4382 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4383 cx.assert_editor_state(indoc! {"
4384 zero
4385 ˇone
4386 ˇtwo
4387 ˇ threeˇ four
4388 "});
4389}
4390
4391#[gpui::test]
4392async fn test_delete(cx: &mut TestAppContext) {
4393 init_test(cx, |_| {});
4394
4395 let mut cx = EditorTestContext::new(cx).await;
4396 cx.set_state(indoc! {"
4397 onˇe two three
4398 fou«rˇ» five six
4399 seven «ˇeight nine
4400 »ten
4401 "});
4402 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
4403 cx.assert_editor_state(indoc! {"
4404 onˇ two three
4405 fouˇ five six
4406 seven ˇten
4407 "});
4408}
4409
4410#[gpui::test]
4411fn test_delete_line(cx: &mut TestAppContext) {
4412 init_test(cx, |_| {});
4413
4414 let editor = cx.add_window(|window, cx| {
4415 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4416 build_editor(buffer, window, cx)
4417 });
4418 _ = editor.update(cx, |editor, window, cx| {
4419 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4420 s.select_display_ranges([
4421 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4422 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4423 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4424 ])
4425 });
4426 editor.delete_line(&DeleteLine, window, cx);
4427 assert_eq!(editor.display_text(cx), "ghi");
4428 assert_eq!(
4429 display_ranges(editor, cx),
4430 vec![
4431 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
4432 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4433 ]
4434 );
4435 });
4436
4437 let editor = cx.add_window(|window, cx| {
4438 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4439 build_editor(buffer, window, cx)
4440 });
4441 _ = editor.update(cx, |editor, window, cx| {
4442 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4443 s.select_display_ranges([
4444 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
4445 ])
4446 });
4447 editor.delete_line(&DeleteLine, window, cx);
4448 assert_eq!(editor.display_text(cx), "ghi\n");
4449 assert_eq!(
4450 display_ranges(editor, cx),
4451 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
4452 );
4453 });
4454
4455 let editor = cx.add_window(|window, cx| {
4456 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n\njkl\nmno", cx);
4457 build_editor(buffer, window, cx)
4458 });
4459 _ = editor.update(cx, |editor, window, cx| {
4460 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4461 s.select_display_ranges([
4462 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(2), 1)
4463 ])
4464 });
4465 editor.delete_line(&DeleteLine, window, cx);
4466 assert_eq!(editor.display_text(cx), "\njkl\nmno");
4467 assert_eq!(
4468 display_ranges(editor, cx),
4469 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
4470 );
4471 });
4472}
4473
4474#[gpui::test]
4475fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
4476 init_test(cx, |_| {});
4477
4478 cx.add_window(|window, cx| {
4479 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4480 let mut editor = build_editor(buffer.clone(), window, cx);
4481 let buffer = buffer.read(cx).as_singleton().unwrap();
4482
4483 assert_eq!(
4484 editor
4485 .selections
4486 .ranges::<Point>(&editor.display_snapshot(cx)),
4487 &[Point::new(0, 0)..Point::new(0, 0)]
4488 );
4489
4490 // When on single line, replace newline at end by space
4491 editor.join_lines(&JoinLines, window, cx);
4492 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4493 assert_eq!(
4494 editor
4495 .selections
4496 .ranges::<Point>(&editor.display_snapshot(cx)),
4497 &[Point::new(0, 3)..Point::new(0, 3)]
4498 );
4499
4500 // When multiple lines are selected, remove newlines that are spanned by the selection
4501 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4502 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
4503 });
4504 editor.join_lines(&JoinLines, window, cx);
4505 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
4506 assert_eq!(
4507 editor
4508 .selections
4509 .ranges::<Point>(&editor.display_snapshot(cx)),
4510 &[Point::new(0, 11)..Point::new(0, 11)]
4511 );
4512
4513 // Undo should be transactional
4514 editor.undo(&Undo, window, cx);
4515 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4516 assert_eq!(
4517 editor
4518 .selections
4519 .ranges::<Point>(&editor.display_snapshot(cx)),
4520 &[Point::new(0, 5)..Point::new(2, 2)]
4521 );
4522
4523 // When joining an empty line don't insert a space
4524 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4525 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
4526 });
4527 editor.join_lines(&JoinLines, window, cx);
4528 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
4529 assert_eq!(
4530 editor
4531 .selections
4532 .ranges::<Point>(&editor.display_snapshot(cx)),
4533 [Point::new(2, 3)..Point::new(2, 3)]
4534 );
4535
4536 // We can remove trailing newlines
4537 editor.join_lines(&JoinLines, window, cx);
4538 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4539 assert_eq!(
4540 editor
4541 .selections
4542 .ranges::<Point>(&editor.display_snapshot(cx)),
4543 [Point::new(2, 3)..Point::new(2, 3)]
4544 );
4545
4546 // We don't blow up on the last line
4547 editor.join_lines(&JoinLines, window, cx);
4548 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4549 assert_eq!(
4550 editor
4551 .selections
4552 .ranges::<Point>(&editor.display_snapshot(cx)),
4553 [Point::new(2, 3)..Point::new(2, 3)]
4554 );
4555
4556 // reset to test indentation
4557 editor.buffer.update(cx, |buffer, cx| {
4558 buffer.edit(
4559 [
4560 (Point::new(1, 0)..Point::new(1, 2), " "),
4561 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
4562 ],
4563 None,
4564 cx,
4565 )
4566 });
4567
4568 // We remove any leading spaces
4569 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
4570 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4571 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
4572 });
4573 editor.join_lines(&JoinLines, window, cx);
4574 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
4575
4576 // We don't insert a space for a line containing only spaces
4577 editor.join_lines(&JoinLines, window, cx);
4578 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
4579
4580 // We ignore any leading tabs
4581 editor.join_lines(&JoinLines, window, cx);
4582 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
4583
4584 editor
4585 });
4586}
4587
4588#[gpui::test]
4589fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
4590 init_test(cx, |_| {});
4591
4592 cx.add_window(|window, cx| {
4593 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4594 let mut editor = build_editor(buffer.clone(), window, cx);
4595 let buffer = buffer.read(cx).as_singleton().unwrap();
4596
4597 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4598 s.select_ranges([
4599 Point::new(0, 2)..Point::new(1, 1),
4600 Point::new(1, 2)..Point::new(1, 2),
4601 Point::new(3, 1)..Point::new(3, 2),
4602 ])
4603 });
4604
4605 editor.join_lines(&JoinLines, window, cx);
4606 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
4607
4608 assert_eq!(
4609 editor
4610 .selections
4611 .ranges::<Point>(&editor.display_snapshot(cx)),
4612 [
4613 Point::new(0, 7)..Point::new(0, 7),
4614 Point::new(1, 3)..Point::new(1, 3)
4615 ]
4616 );
4617 editor
4618 });
4619}
4620
4621#[gpui::test]
4622async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
4623 init_test(cx, |_| {});
4624
4625 let mut cx = EditorTestContext::new(cx).await;
4626
4627 let diff_base = r#"
4628 Line 0
4629 Line 1
4630 Line 2
4631 Line 3
4632 "#
4633 .unindent();
4634
4635 cx.set_state(
4636 &r#"
4637 ˇLine 0
4638 Line 1
4639 Line 2
4640 Line 3
4641 "#
4642 .unindent(),
4643 );
4644
4645 cx.set_head_text(&diff_base);
4646 executor.run_until_parked();
4647
4648 // Join lines
4649 cx.update_editor(|editor, window, cx| {
4650 editor.join_lines(&JoinLines, window, cx);
4651 });
4652 executor.run_until_parked();
4653
4654 cx.assert_editor_state(
4655 &r#"
4656 Line 0ˇ Line 1
4657 Line 2
4658 Line 3
4659 "#
4660 .unindent(),
4661 );
4662 // Join again
4663 cx.update_editor(|editor, window, cx| {
4664 editor.join_lines(&JoinLines, window, cx);
4665 });
4666 executor.run_until_parked();
4667
4668 cx.assert_editor_state(
4669 &r#"
4670 Line 0 Line 1ˇ Line 2
4671 Line 3
4672 "#
4673 .unindent(),
4674 );
4675}
4676
4677#[gpui::test]
4678async fn test_custom_newlines_cause_no_false_positive_diffs(
4679 executor: BackgroundExecutor,
4680 cx: &mut TestAppContext,
4681) {
4682 init_test(cx, |_| {});
4683 let mut cx = EditorTestContext::new(cx).await;
4684 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
4685 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
4686 executor.run_until_parked();
4687
4688 cx.update_editor(|editor, window, cx| {
4689 let snapshot = editor.snapshot(window, cx);
4690 assert_eq!(
4691 snapshot
4692 .buffer_snapshot()
4693 .diff_hunks_in_range(0..snapshot.buffer_snapshot().len())
4694 .collect::<Vec<_>>(),
4695 Vec::new(),
4696 "Should not have any diffs for files with custom newlines"
4697 );
4698 });
4699}
4700
4701#[gpui::test]
4702async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
4703 init_test(cx, |_| {});
4704
4705 let mut cx = EditorTestContext::new(cx).await;
4706
4707 // Test sort_lines_case_insensitive()
4708 cx.set_state(indoc! {"
4709 «z
4710 y
4711 x
4712 Z
4713 Y
4714 Xˇ»
4715 "});
4716 cx.update_editor(|e, window, cx| {
4717 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
4718 });
4719 cx.assert_editor_state(indoc! {"
4720 «x
4721 X
4722 y
4723 Y
4724 z
4725 Zˇ»
4726 "});
4727
4728 // Test sort_lines_by_length()
4729 //
4730 // Demonstrates:
4731 // - ∞ is 3 bytes UTF-8, but sorted by its char count (1)
4732 // - sort is stable
4733 cx.set_state(indoc! {"
4734 «123
4735 æ
4736 12
4737 ∞
4738 1
4739 æˇ»
4740 "});
4741 cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
4742 cx.assert_editor_state(indoc! {"
4743 «æ
4744 ∞
4745 1
4746 æ
4747 12
4748 123ˇ»
4749 "});
4750
4751 // Test reverse_lines()
4752 cx.set_state(indoc! {"
4753 «5
4754 4
4755 3
4756 2
4757 1ˇ»
4758 "});
4759 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4760 cx.assert_editor_state(indoc! {"
4761 «1
4762 2
4763 3
4764 4
4765 5ˇ»
4766 "});
4767
4768 // Skip testing shuffle_line()
4769
4770 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4771 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4772
4773 // Don't manipulate when cursor is on single line, but expand the selection
4774 cx.set_state(indoc! {"
4775 ddˇdd
4776 ccc
4777 bb
4778 a
4779 "});
4780 cx.update_editor(|e, window, cx| {
4781 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4782 });
4783 cx.assert_editor_state(indoc! {"
4784 «ddddˇ»
4785 ccc
4786 bb
4787 a
4788 "});
4789
4790 // Basic manipulate case
4791 // Start selection moves to column 0
4792 // End of selection shrinks to fit shorter line
4793 cx.set_state(indoc! {"
4794 dd«d
4795 ccc
4796 bb
4797 aaaaaˇ»
4798 "});
4799 cx.update_editor(|e, window, cx| {
4800 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4801 });
4802 cx.assert_editor_state(indoc! {"
4803 «aaaaa
4804 bb
4805 ccc
4806 dddˇ»
4807 "});
4808
4809 // Manipulate case with newlines
4810 cx.set_state(indoc! {"
4811 dd«d
4812 ccc
4813
4814 bb
4815 aaaaa
4816
4817 ˇ»
4818 "});
4819 cx.update_editor(|e, window, cx| {
4820 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4821 });
4822 cx.assert_editor_state(indoc! {"
4823 «
4824
4825 aaaaa
4826 bb
4827 ccc
4828 dddˇ»
4829
4830 "});
4831
4832 // Adding new line
4833 cx.set_state(indoc! {"
4834 aa«a
4835 bbˇ»b
4836 "});
4837 cx.update_editor(|e, window, cx| {
4838 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4839 });
4840 cx.assert_editor_state(indoc! {"
4841 «aaa
4842 bbb
4843 added_lineˇ»
4844 "});
4845
4846 // Removing line
4847 cx.set_state(indoc! {"
4848 aa«a
4849 bbbˇ»
4850 "});
4851 cx.update_editor(|e, window, cx| {
4852 e.manipulate_immutable_lines(window, cx, |lines| {
4853 lines.pop();
4854 })
4855 });
4856 cx.assert_editor_state(indoc! {"
4857 «aaaˇ»
4858 "});
4859
4860 // Removing all lines
4861 cx.set_state(indoc! {"
4862 aa«a
4863 bbbˇ»
4864 "});
4865 cx.update_editor(|e, window, cx| {
4866 e.manipulate_immutable_lines(window, cx, |lines| {
4867 lines.drain(..);
4868 })
4869 });
4870 cx.assert_editor_state(indoc! {"
4871 ˇ
4872 "});
4873}
4874
4875#[gpui::test]
4876async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4877 init_test(cx, |_| {});
4878
4879 let mut cx = EditorTestContext::new(cx).await;
4880
4881 // Consider continuous selection as single selection
4882 cx.set_state(indoc! {"
4883 Aaa«aa
4884 cˇ»c«c
4885 bb
4886 aaaˇ»aa
4887 "});
4888 cx.update_editor(|e, window, cx| {
4889 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4890 });
4891 cx.assert_editor_state(indoc! {"
4892 «Aaaaa
4893 ccc
4894 bb
4895 aaaaaˇ»
4896 "});
4897
4898 cx.set_state(indoc! {"
4899 Aaa«aa
4900 cˇ»c«c
4901 bb
4902 aaaˇ»aa
4903 "});
4904 cx.update_editor(|e, window, cx| {
4905 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4906 });
4907 cx.assert_editor_state(indoc! {"
4908 «Aaaaa
4909 ccc
4910 bbˇ»
4911 "});
4912
4913 // Consider non continuous selection as distinct dedup operations
4914 cx.set_state(indoc! {"
4915 «aaaaa
4916 bb
4917 aaaaa
4918 aaaaaˇ»
4919
4920 aaa«aaˇ»
4921 "});
4922 cx.update_editor(|e, window, cx| {
4923 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4924 });
4925 cx.assert_editor_state(indoc! {"
4926 «aaaaa
4927 bbˇ»
4928
4929 «aaaaaˇ»
4930 "});
4931}
4932
4933#[gpui::test]
4934async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4935 init_test(cx, |_| {});
4936
4937 let mut cx = EditorTestContext::new(cx).await;
4938
4939 cx.set_state(indoc! {"
4940 «Aaa
4941 aAa
4942 Aaaˇ»
4943 "});
4944 cx.update_editor(|e, window, cx| {
4945 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4946 });
4947 cx.assert_editor_state(indoc! {"
4948 «Aaa
4949 aAaˇ»
4950 "});
4951
4952 cx.set_state(indoc! {"
4953 «Aaa
4954 aAa
4955 aaAˇ»
4956 "});
4957 cx.update_editor(|e, window, cx| {
4958 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4959 });
4960 cx.assert_editor_state(indoc! {"
4961 «Aaaˇ»
4962 "});
4963}
4964
4965#[gpui::test]
4966async fn test_wrap_in_tag_single_selection(cx: &mut TestAppContext) {
4967 init_test(cx, |_| {});
4968
4969 let mut cx = EditorTestContext::new(cx).await;
4970
4971 let js_language = Arc::new(Language::new(
4972 LanguageConfig {
4973 name: "JavaScript".into(),
4974 wrap_characters: Some(language::WrapCharactersConfig {
4975 start_prefix: "<".into(),
4976 start_suffix: ">".into(),
4977 end_prefix: "</".into(),
4978 end_suffix: ">".into(),
4979 }),
4980 ..LanguageConfig::default()
4981 },
4982 None,
4983 ));
4984
4985 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
4986
4987 cx.set_state(indoc! {"
4988 «testˇ»
4989 "});
4990 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4991 cx.assert_editor_state(indoc! {"
4992 <«ˇ»>test</«ˇ»>
4993 "});
4994
4995 cx.set_state(indoc! {"
4996 «test
4997 testˇ»
4998 "});
4999 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5000 cx.assert_editor_state(indoc! {"
5001 <«ˇ»>test
5002 test</«ˇ»>
5003 "});
5004
5005 cx.set_state(indoc! {"
5006 teˇst
5007 "});
5008 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5009 cx.assert_editor_state(indoc! {"
5010 te<«ˇ»></«ˇ»>st
5011 "});
5012}
5013
5014#[gpui::test]
5015async fn test_wrap_in_tag_multi_selection(cx: &mut TestAppContext) {
5016 init_test(cx, |_| {});
5017
5018 let mut cx = EditorTestContext::new(cx).await;
5019
5020 let js_language = Arc::new(Language::new(
5021 LanguageConfig {
5022 name: "JavaScript".into(),
5023 wrap_characters: Some(language::WrapCharactersConfig {
5024 start_prefix: "<".into(),
5025 start_suffix: ">".into(),
5026 end_prefix: "</".into(),
5027 end_suffix: ">".into(),
5028 }),
5029 ..LanguageConfig::default()
5030 },
5031 None,
5032 ));
5033
5034 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
5035
5036 cx.set_state(indoc! {"
5037 «testˇ»
5038 «testˇ» «testˇ»
5039 «testˇ»
5040 "});
5041 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5042 cx.assert_editor_state(indoc! {"
5043 <«ˇ»>test</«ˇ»>
5044 <«ˇ»>test</«ˇ»> <«ˇ»>test</«ˇ»>
5045 <«ˇ»>test</«ˇ»>
5046 "});
5047
5048 cx.set_state(indoc! {"
5049 «test
5050 testˇ»
5051 «test
5052 testˇ»
5053 "});
5054 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5055 cx.assert_editor_state(indoc! {"
5056 <«ˇ»>test
5057 test</«ˇ»>
5058 <«ˇ»>test
5059 test</«ˇ»>
5060 "});
5061}
5062
5063#[gpui::test]
5064async fn test_wrap_in_tag_does_nothing_in_unsupported_languages(cx: &mut TestAppContext) {
5065 init_test(cx, |_| {});
5066
5067 let mut cx = EditorTestContext::new(cx).await;
5068
5069 let plaintext_language = Arc::new(Language::new(
5070 LanguageConfig {
5071 name: "Plain Text".into(),
5072 ..LanguageConfig::default()
5073 },
5074 None,
5075 ));
5076
5077 cx.update_buffer(|buffer, cx| buffer.set_language(Some(plaintext_language), cx));
5078
5079 cx.set_state(indoc! {"
5080 «testˇ»
5081 "});
5082 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5083 cx.assert_editor_state(indoc! {"
5084 «testˇ»
5085 "});
5086}
5087
5088#[gpui::test]
5089async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
5090 init_test(cx, |_| {});
5091
5092 let mut cx = EditorTestContext::new(cx).await;
5093
5094 // Manipulate with multiple selections on a single line
5095 cx.set_state(indoc! {"
5096 dd«dd
5097 cˇ»c«c
5098 bb
5099 aaaˇ»aa
5100 "});
5101 cx.update_editor(|e, window, cx| {
5102 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5103 });
5104 cx.assert_editor_state(indoc! {"
5105 «aaaaa
5106 bb
5107 ccc
5108 ddddˇ»
5109 "});
5110
5111 // Manipulate with multiple disjoin selections
5112 cx.set_state(indoc! {"
5113 5«
5114 4
5115 3
5116 2
5117 1ˇ»
5118
5119 dd«dd
5120 ccc
5121 bb
5122 aaaˇ»aa
5123 "});
5124 cx.update_editor(|e, window, cx| {
5125 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5126 });
5127 cx.assert_editor_state(indoc! {"
5128 «1
5129 2
5130 3
5131 4
5132 5ˇ»
5133
5134 «aaaaa
5135 bb
5136 ccc
5137 ddddˇ»
5138 "});
5139
5140 // Adding lines on each selection
5141 cx.set_state(indoc! {"
5142 2«
5143 1ˇ»
5144
5145 bb«bb
5146 aaaˇ»aa
5147 "});
5148 cx.update_editor(|e, window, cx| {
5149 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
5150 });
5151 cx.assert_editor_state(indoc! {"
5152 «2
5153 1
5154 added lineˇ»
5155
5156 «bbbb
5157 aaaaa
5158 added lineˇ»
5159 "});
5160
5161 // Removing lines on each selection
5162 cx.set_state(indoc! {"
5163 2«
5164 1ˇ»
5165
5166 bb«bb
5167 aaaˇ»aa
5168 "});
5169 cx.update_editor(|e, window, cx| {
5170 e.manipulate_immutable_lines(window, cx, |lines| {
5171 lines.pop();
5172 })
5173 });
5174 cx.assert_editor_state(indoc! {"
5175 «2ˇ»
5176
5177 «bbbbˇ»
5178 "});
5179}
5180
5181#[gpui::test]
5182async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
5183 init_test(cx, |settings| {
5184 settings.defaults.tab_size = NonZeroU32::new(3)
5185 });
5186
5187 let mut cx = EditorTestContext::new(cx).await;
5188
5189 // MULTI SELECTION
5190 // Ln.1 "«" tests empty lines
5191 // Ln.9 tests just leading whitespace
5192 cx.set_state(indoc! {"
5193 «
5194 abc // No indentationˇ»
5195 «\tabc // 1 tabˇ»
5196 \t\tabc « ˇ» // 2 tabs
5197 \t ab«c // Tab followed by space
5198 \tabc // Space followed by tab (3 spaces should be the result)
5199 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5200 abˇ»ˇc ˇ ˇ // Already space indented«
5201 \t
5202 \tabc\tdef // Only the leading tab is manipulatedˇ»
5203 "});
5204 cx.update_editor(|e, window, cx| {
5205 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5206 });
5207 cx.assert_editor_state(
5208 indoc! {"
5209 «
5210 abc // No indentation
5211 abc // 1 tab
5212 abc // 2 tabs
5213 abc // Tab followed by space
5214 abc // Space followed by tab (3 spaces should be the result)
5215 abc // Mixed indentation (tab conversion depends on the column)
5216 abc // Already space indented
5217 ·
5218 abc\tdef // Only the leading tab is manipulatedˇ»
5219 "}
5220 .replace("·", "")
5221 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5222 );
5223
5224 // Test on just a few lines, the others should remain unchanged
5225 // Only lines (3, 5, 10, 11) should change
5226 cx.set_state(
5227 indoc! {"
5228 ·
5229 abc // No indentation
5230 \tabcˇ // 1 tab
5231 \t\tabc // 2 tabs
5232 \t abcˇ // Tab followed by space
5233 \tabc // Space followed by tab (3 spaces should be the result)
5234 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5235 abc // Already space indented
5236 «\t
5237 \tabc\tdef // Only the leading tab is manipulatedˇ»
5238 "}
5239 .replace("·", "")
5240 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5241 );
5242 cx.update_editor(|e, window, cx| {
5243 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5244 });
5245 cx.assert_editor_state(
5246 indoc! {"
5247 ·
5248 abc // No indentation
5249 « abc // 1 tabˇ»
5250 \t\tabc // 2 tabs
5251 « abc // Tab followed by spaceˇ»
5252 \tabc // Space followed by tab (3 spaces should be the result)
5253 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5254 abc // Already space indented
5255 « ·
5256 abc\tdef // Only the leading tab is manipulatedˇ»
5257 "}
5258 .replace("·", "")
5259 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5260 );
5261
5262 // SINGLE SELECTION
5263 // Ln.1 "«" tests empty lines
5264 // Ln.9 tests just leading whitespace
5265 cx.set_state(indoc! {"
5266 «
5267 abc // No indentation
5268 \tabc // 1 tab
5269 \t\tabc // 2 tabs
5270 \t abc // Tab followed by space
5271 \tabc // Space followed by tab (3 spaces should be the result)
5272 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5273 abc // Already space indented
5274 \t
5275 \tabc\tdef // Only the leading tab is manipulatedˇ»
5276 "});
5277 cx.update_editor(|e, window, cx| {
5278 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5279 });
5280 cx.assert_editor_state(
5281 indoc! {"
5282 «
5283 abc // No indentation
5284 abc // 1 tab
5285 abc // 2 tabs
5286 abc // Tab followed by space
5287 abc // Space followed by tab (3 spaces should be the result)
5288 abc // Mixed indentation (tab conversion depends on the column)
5289 abc // Already space indented
5290 ·
5291 abc\tdef // Only the leading tab is manipulatedˇ»
5292 "}
5293 .replace("·", "")
5294 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5295 );
5296}
5297
5298#[gpui::test]
5299async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
5300 init_test(cx, |settings| {
5301 settings.defaults.tab_size = NonZeroU32::new(3)
5302 });
5303
5304 let mut cx = EditorTestContext::new(cx).await;
5305
5306 // MULTI SELECTION
5307 // Ln.1 "«" tests empty lines
5308 // Ln.11 tests just leading whitespace
5309 cx.set_state(indoc! {"
5310 «
5311 abˇ»ˇc // No indentation
5312 abc ˇ ˇ // 1 space (< 3 so dont convert)
5313 abc « // 2 spaces (< 3 so dont convert)
5314 abc // 3 spaces (convert)
5315 abc ˇ» // 5 spaces (1 tab + 2 spaces)
5316 «\tˇ»\t«\tˇ»abc // Already tab indented
5317 «\t abc // Tab followed by space
5318 \tabc // Space followed by tab (should be consumed due to tab)
5319 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5320 \tˇ» «\t
5321 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
5322 "});
5323 cx.update_editor(|e, window, cx| {
5324 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5325 });
5326 cx.assert_editor_state(indoc! {"
5327 «
5328 abc // No indentation
5329 abc // 1 space (< 3 so dont convert)
5330 abc // 2 spaces (< 3 so dont convert)
5331 \tabc // 3 spaces (convert)
5332 \t abc // 5 spaces (1 tab + 2 spaces)
5333 \t\t\tabc // Already tab indented
5334 \t abc // Tab followed by space
5335 \tabc // Space followed by tab (should be consumed due to tab)
5336 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5337 \t\t\t
5338 \tabc \t // Only the leading spaces should be convertedˇ»
5339 "});
5340
5341 // Test on just a few lines, the other should remain unchanged
5342 // Only lines (4, 8, 11, 12) should change
5343 cx.set_state(
5344 indoc! {"
5345 ·
5346 abc // No indentation
5347 abc // 1 space (< 3 so dont convert)
5348 abc // 2 spaces (< 3 so dont convert)
5349 « abc // 3 spaces (convert)ˇ»
5350 abc // 5 spaces (1 tab + 2 spaces)
5351 \t\t\tabc // Already tab indented
5352 \t abc // Tab followed by space
5353 \tabc ˇ // Space followed by tab (should be consumed due to tab)
5354 \t\t \tabc // Mixed indentation
5355 \t \t \t \tabc // Mixed indentation
5356 \t \tˇ
5357 « abc \t // Only the leading spaces should be convertedˇ»
5358 "}
5359 .replace("·", "")
5360 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5361 );
5362 cx.update_editor(|e, window, cx| {
5363 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5364 });
5365 cx.assert_editor_state(
5366 indoc! {"
5367 ·
5368 abc // No indentation
5369 abc // 1 space (< 3 so dont convert)
5370 abc // 2 spaces (< 3 so dont convert)
5371 «\tabc // 3 spaces (convert)ˇ»
5372 abc // 5 spaces (1 tab + 2 spaces)
5373 \t\t\tabc // Already tab indented
5374 \t abc // Tab followed by space
5375 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
5376 \t\t \tabc // Mixed indentation
5377 \t \t \t \tabc // Mixed indentation
5378 «\t\t\t
5379 \tabc \t // Only the leading spaces should be convertedˇ»
5380 "}
5381 .replace("·", "")
5382 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5383 );
5384
5385 // SINGLE SELECTION
5386 // Ln.1 "«" tests empty lines
5387 // Ln.11 tests just leading whitespace
5388 cx.set_state(indoc! {"
5389 «
5390 abc // No indentation
5391 abc // 1 space (< 3 so dont convert)
5392 abc // 2 spaces (< 3 so dont convert)
5393 abc // 3 spaces (convert)
5394 abc // 5 spaces (1 tab + 2 spaces)
5395 \t\t\tabc // Already tab indented
5396 \t abc // Tab followed by space
5397 \tabc // Space followed by tab (should be consumed due to tab)
5398 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5399 \t \t
5400 abc \t // Only the leading spaces should be convertedˇ»
5401 "});
5402 cx.update_editor(|e, window, cx| {
5403 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5404 });
5405 cx.assert_editor_state(indoc! {"
5406 «
5407 abc // No indentation
5408 abc // 1 space (< 3 so dont convert)
5409 abc // 2 spaces (< 3 so dont convert)
5410 \tabc // 3 spaces (convert)
5411 \t abc // 5 spaces (1 tab + 2 spaces)
5412 \t\t\tabc // Already tab indented
5413 \t abc // Tab followed by space
5414 \tabc // Space followed by tab (should be consumed due to tab)
5415 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5416 \t\t\t
5417 \tabc \t // Only the leading spaces should be convertedˇ»
5418 "});
5419}
5420
5421#[gpui::test]
5422async fn test_toggle_case(cx: &mut TestAppContext) {
5423 init_test(cx, |_| {});
5424
5425 let mut cx = EditorTestContext::new(cx).await;
5426
5427 // If all lower case -> upper case
5428 cx.set_state(indoc! {"
5429 «hello worldˇ»
5430 "});
5431 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5432 cx.assert_editor_state(indoc! {"
5433 «HELLO WORLDˇ»
5434 "});
5435
5436 // If all upper case -> lower case
5437 cx.set_state(indoc! {"
5438 «HELLO WORLDˇ»
5439 "});
5440 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5441 cx.assert_editor_state(indoc! {"
5442 «hello worldˇ»
5443 "});
5444
5445 // If any upper case characters are identified -> lower case
5446 // This matches JetBrains IDEs
5447 cx.set_state(indoc! {"
5448 «hEllo worldˇ»
5449 "});
5450 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5451 cx.assert_editor_state(indoc! {"
5452 «hello worldˇ»
5453 "});
5454}
5455
5456#[gpui::test]
5457async fn test_convert_to_sentence_case(cx: &mut TestAppContext) {
5458 init_test(cx, |_| {});
5459
5460 let mut cx = EditorTestContext::new(cx).await;
5461
5462 cx.set_state(indoc! {"
5463 «implement-windows-supportˇ»
5464 "});
5465 cx.update_editor(|e, window, cx| {
5466 e.convert_to_sentence_case(&ConvertToSentenceCase, window, cx)
5467 });
5468 cx.assert_editor_state(indoc! {"
5469 «Implement windows supportˇ»
5470 "});
5471}
5472
5473#[gpui::test]
5474async fn test_manipulate_text(cx: &mut TestAppContext) {
5475 init_test(cx, |_| {});
5476
5477 let mut cx = EditorTestContext::new(cx).await;
5478
5479 // Test convert_to_upper_case()
5480 cx.set_state(indoc! {"
5481 «hello worldˇ»
5482 "});
5483 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5484 cx.assert_editor_state(indoc! {"
5485 «HELLO WORLDˇ»
5486 "});
5487
5488 // Test convert_to_lower_case()
5489 cx.set_state(indoc! {"
5490 «HELLO WORLDˇ»
5491 "});
5492 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
5493 cx.assert_editor_state(indoc! {"
5494 «hello worldˇ»
5495 "});
5496
5497 // Test multiple line, single selection case
5498 cx.set_state(indoc! {"
5499 «The quick brown
5500 fox jumps over
5501 the lazy dogˇ»
5502 "});
5503 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
5504 cx.assert_editor_state(indoc! {"
5505 «The Quick Brown
5506 Fox Jumps Over
5507 The Lazy Dogˇ»
5508 "});
5509
5510 // Test multiple line, single selection case
5511 cx.set_state(indoc! {"
5512 «The quick brown
5513 fox jumps over
5514 the lazy dogˇ»
5515 "});
5516 cx.update_editor(|e, window, cx| {
5517 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
5518 });
5519 cx.assert_editor_state(indoc! {"
5520 «TheQuickBrown
5521 FoxJumpsOver
5522 TheLazyDogˇ»
5523 "});
5524
5525 // From here on out, test more complex cases of manipulate_text()
5526
5527 // Test no selection case - should affect words cursors are in
5528 // Cursor at beginning, middle, and end of word
5529 cx.set_state(indoc! {"
5530 ˇhello big beauˇtiful worldˇ
5531 "});
5532 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5533 cx.assert_editor_state(indoc! {"
5534 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
5535 "});
5536
5537 // Test multiple selections on a single line and across multiple lines
5538 cx.set_state(indoc! {"
5539 «Theˇ» quick «brown
5540 foxˇ» jumps «overˇ»
5541 the «lazyˇ» dog
5542 "});
5543 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5544 cx.assert_editor_state(indoc! {"
5545 «THEˇ» quick «BROWN
5546 FOXˇ» jumps «OVERˇ»
5547 the «LAZYˇ» dog
5548 "});
5549
5550 // Test case where text length grows
5551 cx.set_state(indoc! {"
5552 «tschüߡ»
5553 "});
5554 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5555 cx.assert_editor_state(indoc! {"
5556 «TSCHÜSSˇ»
5557 "});
5558
5559 // Test to make sure we don't crash when text shrinks
5560 cx.set_state(indoc! {"
5561 aaa_bbbˇ
5562 "});
5563 cx.update_editor(|e, window, cx| {
5564 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5565 });
5566 cx.assert_editor_state(indoc! {"
5567 «aaaBbbˇ»
5568 "});
5569
5570 // Test to make sure we all aware of the fact that each word can grow and shrink
5571 // Final selections should be aware of this fact
5572 cx.set_state(indoc! {"
5573 aaa_bˇbb bbˇb_ccc ˇccc_ddd
5574 "});
5575 cx.update_editor(|e, window, cx| {
5576 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5577 });
5578 cx.assert_editor_state(indoc! {"
5579 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
5580 "});
5581
5582 cx.set_state(indoc! {"
5583 «hElLo, WoRld!ˇ»
5584 "});
5585 cx.update_editor(|e, window, cx| {
5586 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
5587 });
5588 cx.assert_editor_state(indoc! {"
5589 «HeLlO, wOrLD!ˇ»
5590 "});
5591
5592 // Test selections with `line_mode() = true`.
5593 cx.update_editor(|editor, _window, _cx| editor.selections.set_line_mode(true));
5594 cx.set_state(indoc! {"
5595 «The quick brown
5596 fox jumps over
5597 tˇ»he lazy dog
5598 "});
5599 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5600 cx.assert_editor_state(indoc! {"
5601 «THE QUICK BROWN
5602 FOX JUMPS OVER
5603 THE LAZY DOGˇ»
5604 "});
5605}
5606
5607#[gpui::test]
5608fn test_duplicate_line(cx: &mut TestAppContext) {
5609 init_test(cx, |_| {});
5610
5611 let editor = cx.add_window(|window, cx| {
5612 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5613 build_editor(buffer, window, cx)
5614 });
5615 _ = editor.update(cx, |editor, window, cx| {
5616 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5617 s.select_display_ranges([
5618 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5619 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5620 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5621 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5622 ])
5623 });
5624 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5625 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5626 assert_eq!(
5627 display_ranges(editor, cx),
5628 vec![
5629 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
5630 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
5631 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5632 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5633 ]
5634 );
5635 });
5636
5637 let editor = cx.add_window(|window, cx| {
5638 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5639 build_editor(buffer, window, cx)
5640 });
5641 _ = editor.update(cx, |editor, window, cx| {
5642 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5643 s.select_display_ranges([
5644 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5645 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5646 ])
5647 });
5648 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5649 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5650 assert_eq!(
5651 display_ranges(editor, cx),
5652 vec![
5653 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
5654 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
5655 ]
5656 );
5657 });
5658
5659 // With `duplicate_line_up` the selections move to the duplicated lines,
5660 // which are inserted above the original lines
5661 let editor = cx.add_window(|window, cx| {
5662 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5663 build_editor(buffer, window, cx)
5664 });
5665 _ = editor.update(cx, |editor, window, cx| {
5666 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5667 s.select_display_ranges([
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(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5671 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5672 ])
5673 });
5674 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5675 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5676 assert_eq!(
5677 display_ranges(editor, cx),
5678 vec![
5679 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5680 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5681 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
5682 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0),
5683 ]
5684 );
5685 });
5686
5687 let editor = cx.add_window(|window, cx| {
5688 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5689 build_editor(buffer, window, cx)
5690 });
5691 _ = editor.update(cx, |editor, window, cx| {
5692 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5693 s.select_display_ranges([
5694 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5695 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5696 ])
5697 });
5698 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5699 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5700 assert_eq!(
5701 display_ranges(editor, cx),
5702 vec![
5703 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5704 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5705 ]
5706 );
5707 });
5708
5709 let editor = cx.add_window(|window, cx| {
5710 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5711 build_editor(buffer, window, cx)
5712 });
5713 _ = editor.update(cx, |editor, window, cx| {
5714 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5715 s.select_display_ranges([
5716 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5717 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5718 ])
5719 });
5720 editor.duplicate_selection(&DuplicateSelection, window, cx);
5721 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
5722 assert_eq!(
5723 display_ranges(editor, cx),
5724 vec![
5725 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5726 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
5727 ]
5728 );
5729 });
5730}
5731
5732#[gpui::test]
5733fn test_move_line_up_down(cx: &mut TestAppContext) {
5734 init_test(cx, |_| {});
5735
5736 let editor = cx.add_window(|window, cx| {
5737 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5738 build_editor(buffer, window, cx)
5739 });
5740 _ = editor.update(cx, |editor, window, cx| {
5741 editor.fold_creases(
5742 vec![
5743 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5744 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5745 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5746 ],
5747 true,
5748 window,
5749 cx,
5750 );
5751 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5752 s.select_display_ranges([
5753 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5754 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5755 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5756 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
5757 ])
5758 });
5759 assert_eq!(
5760 editor.display_text(cx),
5761 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
5762 );
5763
5764 editor.move_line_up(&MoveLineUp, window, cx);
5765 assert_eq!(
5766 editor.display_text(cx),
5767 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
5768 );
5769 assert_eq!(
5770 display_ranges(editor, cx),
5771 vec![
5772 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5773 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5774 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5775 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5776 ]
5777 );
5778 });
5779
5780 _ = editor.update(cx, |editor, window, cx| {
5781 editor.move_line_down(&MoveLineDown, window, cx);
5782 assert_eq!(
5783 editor.display_text(cx),
5784 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
5785 );
5786 assert_eq!(
5787 display_ranges(editor, cx),
5788 vec![
5789 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5790 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5791 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5792 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5793 ]
5794 );
5795 });
5796
5797 _ = editor.update(cx, |editor, window, cx| {
5798 editor.move_line_down(&MoveLineDown, window, cx);
5799 assert_eq!(
5800 editor.display_text(cx),
5801 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
5802 );
5803 assert_eq!(
5804 display_ranges(editor, cx),
5805 vec![
5806 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5807 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5808 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5809 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5810 ]
5811 );
5812 });
5813
5814 _ = editor.update(cx, |editor, window, cx| {
5815 editor.move_line_up(&MoveLineUp, window, cx);
5816 assert_eq!(
5817 editor.display_text(cx),
5818 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
5819 );
5820 assert_eq!(
5821 display_ranges(editor, cx),
5822 vec![
5823 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5824 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5825 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5826 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5827 ]
5828 );
5829 });
5830}
5831
5832#[gpui::test]
5833fn test_move_line_up_selection_at_end_of_fold(cx: &mut TestAppContext) {
5834 init_test(cx, |_| {});
5835 let editor = cx.add_window(|window, cx| {
5836 let buffer = MultiBuffer::build_simple("\n\n\n\n\n\naaaa\nbbbb\ncccc", cx);
5837 build_editor(buffer, window, cx)
5838 });
5839 _ = editor.update(cx, |editor, window, cx| {
5840 editor.fold_creases(
5841 vec![Crease::simple(
5842 Point::new(6, 4)..Point::new(7, 4),
5843 FoldPlaceholder::test(),
5844 )],
5845 true,
5846 window,
5847 cx,
5848 );
5849 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5850 s.select_ranges([Point::new(7, 4)..Point::new(7, 4)])
5851 });
5852 assert_eq!(editor.display_text(cx), "\n\n\n\n\n\naaaa⋯\ncccc");
5853 editor.move_line_up(&MoveLineUp, window, cx);
5854 let buffer_text = editor.buffer.read(cx).snapshot(cx).text();
5855 assert_eq!(buffer_text, "\n\n\n\n\naaaa\nbbbb\n\ncccc");
5856 });
5857}
5858
5859#[gpui::test]
5860fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
5861 init_test(cx, |_| {});
5862
5863 let editor = cx.add_window(|window, cx| {
5864 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5865 build_editor(buffer, window, cx)
5866 });
5867 _ = editor.update(cx, |editor, window, cx| {
5868 let snapshot = editor.buffer.read(cx).snapshot(cx);
5869 editor.insert_blocks(
5870 [BlockProperties {
5871 style: BlockStyle::Fixed,
5872 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
5873 height: Some(1),
5874 render: Arc::new(|_| div().into_any()),
5875 priority: 0,
5876 }],
5877 Some(Autoscroll::fit()),
5878 cx,
5879 );
5880 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5881 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
5882 });
5883 editor.move_line_down(&MoveLineDown, window, cx);
5884 });
5885}
5886
5887#[gpui::test]
5888async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
5889 init_test(cx, |_| {});
5890
5891 let mut cx = EditorTestContext::new(cx).await;
5892 cx.set_state(
5893 &"
5894 ˇzero
5895 one
5896 two
5897 three
5898 four
5899 five
5900 "
5901 .unindent(),
5902 );
5903
5904 // Create a four-line block that replaces three lines of text.
5905 cx.update_editor(|editor, window, cx| {
5906 let snapshot = editor.snapshot(window, cx);
5907 let snapshot = &snapshot.buffer_snapshot();
5908 let placement = BlockPlacement::Replace(
5909 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
5910 );
5911 editor.insert_blocks(
5912 [BlockProperties {
5913 placement,
5914 height: Some(4),
5915 style: BlockStyle::Sticky,
5916 render: Arc::new(|_| gpui::div().into_any_element()),
5917 priority: 0,
5918 }],
5919 None,
5920 cx,
5921 );
5922 });
5923
5924 // Move down so that the cursor touches the block.
5925 cx.update_editor(|editor, window, cx| {
5926 editor.move_down(&Default::default(), window, cx);
5927 });
5928 cx.assert_editor_state(
5929 &"
5930 zero
5931 «one
5932 two
5933 threeˇ»
5934 four
5935 five
5936 "
5937 .unindent(),
5938 );
5939
5940 // Move down past the block.
5941 cx.update_editor(|editor, window, cx| {
5942 editor.move_down(&Default::default(), window, cx);
5943 });
5944 cx.assert_editor_state(
5945 &"
5946 zero
5947 one
5948 two
5949 three
5950 ˇfour
5951 five
5952 "
5953 .unindent(),
5954 );
5955}
5956
5957#[gpui::test]
5958fn test_transpose(cx: &mut TestAppContext) {
5959 init_test(cx, |_| {});
5960
5961 _ = cx.add_window(|window, cx| {
5962 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
5963 editor.set_style(EditorStyle::default(), window, cx);
5964 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5965 s.select_ranges([1..1])
5966 });
5967 editor.transpose(&Default::default(), window, cx);
5968 assert_eq!(editor.text(cx), "bac");
5969 assert_eq!(
5970 editor.selections.ranges(&editor.display_snapshot(cx)),
5971 [2..2]
5972 );
5973
5974 editor.transpose(&Default::default(), window, cx);
5975 assert_eq!(editor.text(cx), "bca");
5976 assert_eq!(
5977 editor.selections.ranges(&editor.display_snapshot(cx)),
5978 [3..3]
5979 );
5980
5981 editor.transpose(&Default::default(), window, cx);
5982 assert_eq!(editor.text(cx), "bac");
5983 assert_eq!(
5984 editor.selections.ranges(&editor.display_snapshot(cx)),
5985 [3..3]
5986 );
5987
5988 editor
5989 });
5990
5991 _ = cx.add_window(|window, cx| {
5992 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5993 editor.set_style(EditorStyle::default(), window, cx);
5994 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5995 s.select_ranges([3..3])
5996 });
5997 editor.transpose(&Default::default(), window, cx);
5998 assert_eq!(editor.text(cx), "acb\nde");
5999 assert_eq!(
6000 editor.selections.ranges(&editor.display_snapshot(cx)),
6001 [3..3]
6002 );
6003
6004 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6005 s.select_ranges([4..4])
6006 });
6007 editor.transpose(&Default::default(), window, cx);
6008 assert_eq!(editor.text(cx), "acbd\ne");
6009 assert_eq!(
6010 editor.selections.ranges(&editor.display_snapshot(cx)),
6011 [5..5]
6012 );
6013
6014 editor.transpose(&Default::default(), window, cx);
6015 assert_eq!(editor.text(cx), "acbde\n");
6016 assert_eq!(
6017 editor.selections.ranges(&editor.display_snapshot(cx)),
6018 [6..6]
6019 );
6020
6021 editor.transpose(&Default::default(), window, cx);
6022 assert_eq!(editor.text(cx), "acbd\ne");
6023 assert_eq!(
6024 editor.selections.ranges(&editor.display_snapshot(cx)),
6025 [6..6]
6026 );
6027
6028 editor
6029 });
6030
6031 _ = cx.add_window(|window, cx| {
6032 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
6033 editor.set_style(EditorStyle::default(), window, cx);
6034 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6035 s.select_ranges([1..1, 2..2, 4..4])
6036 });
6037 editor.transpose(&Default::default(), window, cx);
6038 assert_eq!(editor.text(cx), "bacd\ne");
6039 assert_eq!(
6040 editor.selections.ranges(&editor.display_snapshot(cx)),
6041 [2..2, 3..3, 5..5]
6042 );
6043
6044 editor.transpose(&Default::default(), window, cx);
6045 assert_eq!(editor.text(cx), "bcade\n");
6046 assert_eq!(
6047 editor.selections.ranges(&editor.display_snapshot(cx)),
6048 [3..3, 4..4, 6..6]
6049 );
6050
6051 editor.transpose(&Default::default(), window, cx);
6052 assert_eq!(editor.text(cx), "bcda\ne");
6053 assert_eq!(
6054 editor.selections.ranges(&editor.display_snapshot(cx)),
6055 [4..4, 6..6]
6056 );
6057
6058 editor.transpose(&Default::default(), window, cx);
6059 assert_eq!(editor.text(cx), "bcade\n");
6060 assert_eq!(
6061 editor.selections.ranges(&editor.display_snapshot(cx)),
6062 [4..4, 6..6]
6063 );
6064
6065 editor.transpose(&Default::default(), window, cx);
6066 assert_eq!(editor.text(cx), "bcaed\n");
6067 assert_eq!(
6068 editor.selections.ranges(&editor.display_snapshot(cx)),
6069 [5..5, 6..6]
6070 );
6071
6072 editor
6073 });
6074
6075 _ = cx.add_window(|window, cx| {
6076 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
6077 editor.set_style(EditorStyle::default(), window, cx);
6078 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6079 s.select_ranges([4..4])
6080 });
6081 editor.transpose(&Default::default(), window, cx);
6082 assert_eq!(editor.text(cx), "🏀🍐✋");
6083 assert_eq!(
6084 editor.selections.ranges(&editor.display_snapshot(cx)),
6085 [8..8]
6086 );
6087
6088 editor.transpose(&Default::default(), window, cx);
6089 assert_eq!(editor.text(cx), "🏀✋🍐");
6090 assert_eq!(
6091 editor.selections.ranges(&editor.display_snapshot(cx)),
6092 [11..11]
6093 );
6094
6095 editor.transpose(&Default::default(), window, cx);
6096 assert_eq!(editor.text(cx), "🏀🍐✋");
6097 assert_eq!(
6098 editor.selections.ranges(&editor.display_snapshot(cx)),
6099 [11..11]
6100 );
6101
6102 editor
6103 });
6104}
6105
6106#[gpui::test]
6107async fn test_rewrap(cx: &mut TestAppContext) {
6108 init_test(cx, |settings| {
6109 settings.languages.0.extend([
6110 (
6111 "Markdown".into(),
6112 LanguageSettingsContent {
6113 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
6114 preferred_line_length: Some(40),
6115 ..Default::default()
6116 },
6117 ),
6118 (
6119 "Plain Text".into(),
6120 LanguageSettingsContent {
6121 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
6122 preferred_line_length: Some(40),
6123 ..Default::default()
6124 },
6125 ),
6126 (
6127 "C++".into(),
6128 LanguageSettingsContent {
6129 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6130 preferred_line_length: Some(40),
6131 ..Default::default()
6132 },
6133 ),
6134 (
6135 "Python".into(),
6136 LanguageSettingsContent {
6137 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6138 preferred_line_length: Some(40),
6139 ..Default::default()
6140 },
6141 ),
6142 (
6143 "Rust".into(),
6144 LanguageSettingsContent {
6145 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6146 preferred_line_length: Some(40),
6147 ..Default::default()
6148 },
6149 ),
6150 ])
6151 });
6152
6153 let mut cx = EditorTestContext::new(cx).await;
6154
6155 let cpp_language = Arc::new(Language::new(
6156 LanguageConfig {
6157 name: "C++".into(),
6158 line_comments: vec!["// ".into()],
6159 ..LanguageConfig::default()
6160 },
6161 None,
6162 ));
6163 let python_language = Arc::new(Language::new(
6164 LanguageConfig {
6165 name: "Python".into(),
6166 line_comments: vec!["# ".into()],
6167 ..LanguageConfig::default()
6168 },
6169 None,
6170 ));
6171 let markdown_language = Arc::new(Language::new(
6172 LanguageConfig {
6173 name: "Markdown".into(),
6174 rewrap_prefixes: vec![
6175 regex::Regex::new("\\d+\\.\\s+").unwrap(),
6176 regex::Regex::new("[-*+]\\s+").unwrap(),
6177 ],
6178 ..LanguageConfig::default()
6179 },
6180 None,
6181 ));
6182 let rust_language = Arc::new(
6183 Language::new(
6184 LanguageConfig {
6185 name: "Rust".into(),
6186 line_comments: vec!["// ".into(), "/// ".into()],
6187 ..LanguageConfig::default()
6188 },
6189 Some(tree_sitter_rust::LANGUAGE.into()),
6190 )
6191 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
6192 .unwrap(),
6193 );
6194
6195 let plaintext_language = Arc::new(Language::new(
6196 LanguageConfig {
6197 name: "Plain Text".into(),
6198 ..LanguageConfig::default()
6199 },
6200 None,
6201 ));
6202
6203 // Test basic rewrapping of a long line with a cursor
6204 assert_rewrap(
6205 indoc! {"
6206 // ˇThis is a long comment that needs to be wrapped.
6207 "},
6208 indoc! {"
6209 // ˇThis is a long comment that needs to
6210 // be wrapped.
6211 "},
6212 cpp_language.clone(),
6213 &mut cx,
6214 );
6215
6216 // Test rewrapping a full selection
6217 assert_rewrap(
6218 indoc! {"
6219 «// This selected long comment needs to be wrapped.ˇ»"
6220 },
6221 indoc! {"
6222 «// This selected long comment needs to
6223 // be wrapped.ˇ»"
6224 },
6225 cpp_language.clone(),
6226 &mut cx,
6227 );
6228
6229 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
6230 assert_rewrap(
6231 indoc! {"
6232 // ˇThis is the first line.
6233 // Thisˇ is the second line.
6234 // This is the thirdˇ line, all part of one paragraph.
6235 "},
6236 indoc! {"
6237 // ˇThis is the first line. Thisˇ is the
6238 // second line. This is the thirdˇ line,
6239 // all part of one paragraph.
6240 "},
6241 cpp_language.clone(),
6242 &mut cx,
6243 );
6244
6245 // Test multiple cursors in different paragraphs trigger separate rewraps
6246 assert_rewrap(
6247 indoc! {"
6248 // ˇThis is the first paragraph, first line.
6249 // ˇThis is the first paragraph, second line.
6250
6251 // ˇThis is the second paragraph, first line.
6252 // ˇThis is the second paragraph, second line.
6253 "},
6254 indoc! {"
6255 // ˇThis is the first paragraph, first
6256 // line. ˇThis is the first paragraph,
6257 // second line.
6258
6259 // ˇThis is the second paragraph, first
6260 // line. ˇThis is the second paragraph,
6261 // second line.
6262 "},
6263 cpp_language.clone(),
6264 &mut cx,
6265 );
6266
6267 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
6268 assert_rewrap(
6269 indoc! {"
6270 «// A regular long long comment to be wrapped.
6271 /// A documentation long comment to be wrapped.ˇ»
6272 "},
6273 indoc! {"
6274 «// A regular long long comment to be
6275 // wrapped.
6276 /// A documentation long comment to be
6277 /// wrapped.ˇ»
6278 "},
6279 rust_language.clone(),
6280 &mut cx,
6281 );
6282
6283 // Test that change in indentation level trigger seperate rewraps
6284 assert_rewrap(
6285 indoc! {"
6286 fn foo() {
6287 «// This is a long comment at the base indent.
6288 // This is a long comment at the next indent.ˇ»
6289 }
6290 "},
6291 indoc! {"
6292 fn foo() {
6293 «// This is a long comment at the
6294 // base indent.
6295 // This is a long comment at the
6296 // next indent.ˇ»
6297 }
6298 "},
6299 rust_language.clone(),
6300 &mut cx,
6301 );
6302
6303 // Test that different comment prefix characters (e.g., '#') are handled correctly
6304 assert_rewrap(
6305 indoc! {"
6306 # ˇThis is a long comment using a pound sign.
6307 "},
6308 indoc! {"
6309 # ˇThis is a long comment using a pound
6310 # sign.
6311 "},
6312 python_language,
6313 &mut cx,
6314 );
6315
6316 // Test rewrapping only affects comments, not code even when selected
6317 assert_rewrap(
6318 indoc! {"
6319 «/// This doc comment is long and should be wrapped.
6320 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6321 "},
6322 indoc! {"
6323 «/// This doc comment is long and should
6324 /// be wrapped.
6325 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6326 "},
6327 rust_language.clone(),
6328 &mut cx,
6329 );
6330
6331 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
6332 assert_rewrap(
6333 indoc! {"
6334 # Header
6335
6336 A long long long line of markdown text to wrap.ˇ
6337 "},
6338 indoc! {"
6339 # Header
6340
6341 A long long long line of markdown text
6342 to wrap.ˇ
6343 "},
6344 markdown_language.clone(),
6345 &mut cx,
6346 );
6347
6348 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
6349 assert_rewrap(
6350 indoc! {"
6351 «1. This is a numbered list item that is very long and needs to be wrapped properly.
6352 2. This is a numbered list item that is very long and needs to be wrapped properly.
6353 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
6354 "},
6355 indoc! {"
6356 «1. This is a numbered list item that is
6357 very long and needs to be wrapped
6358 properly.
6359 2. This is a numbered list item that is
6360 very long and needs to be wrapped
6361 properly.
6362 - This is an unordered list item that is
6363 also very long and should not merge
6364 with the numbered item.ˇ»
6365 "},
6366 markdown_language.clone(),
6367 &mut cx,
6368 );
6369
6370 // Test that rewrapping add indents for rewrapping boundary if not exists already.
6371 assert_rewrap(
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 with
6381 the numbered item.ˇ»
6382 "},
6383 indoc! {"
6384 «1. This is a numbered list item that is
6385 very long and needs to be wrapped
6386 properly.
6387 2. This is a numbered list item that is
6388 very long and needs to be wrapped
6389 properly.
6390 - This is an unordered list item that is
6391 also very long and should not merge
6392 with the numbered item.ˇ»
6393 "},
6394 markdown_language.clone(),
6395 &mut cx,
6396 );
6397
6398 // Test that rewrapping maintain indents even when they already exists.
6399 assert_rewrap(
6400 indoc! {"
6401 «1. This is a numbered list
6402 item that is very long and needs to be wrapped properly.
6403 2. This is a numbered list
6404 item that is very long and needs to be wrapped properly.
6405 - This is an unordered list item that is also very long and
6406 should not merge with the numbered item.ˇ»
6407 "},
6408 indoc! {"
6409 «1. This is a numbered list item that is
6410 very long and needs to be wrapped
6411 properly.
6412 2. This is a numbered list item that is
6413 very long and needs to be wrapped
6414 properly.
6415 - This is an unordered list item that is
6416 also very long and should not merge
6417 with the numbered item.ˇ»
6418 "},
6419 markdown_language,
6420 &mut cx,
6421 );
6422
6423 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
6424 assert_rewrap(
6425 indoc! {"
6426 ˇThis is a very long line of plain text that will be wrapped.
6427 "},
6428 indoc! {"
6429 ˇThis is a very long line of plain text
6430 that will be wrapped.
6431 "},
6432 plaintext_language.clone(),
6433 &mut cx,
6434 );
6435
6436 // Test that non-commented code acts as a paragraph boundary within a selection
6437 assert_rewrap(
6438 indoc! {"
6439 «// This is the first long comment block to be wrapped.
6440 fn my_func(a: u32);
6441 // This is the second long comment block to be wrapped.ˇ»
6442 "},
6443 indoc! {"
6444 «// This is the first long comment block
6445 // to be wrapped.
6446 fn my_func(a: u32);
6447 // This is the second long comment block
6448 // to be wrapped.ˇ»
6449 "},
6450 rust_language,
6451 &mut cx,
6452 );
6453
6454 // Test rewrapping multiple selections, including ones with blank lines or tabs
6455 assert_rewrap(
6456 indoc! {"
6457 «ˇThis is a very long line that will be wrapped.
6458
6459 This is another paragraph in the same selection.»
6460
6461 «\tThis is a very long indented line that will be wrapped.ˇ»
6462 "},
6463 indoc! {"
6464 «ˇThis is a very long line that will be
6465 wrapped.
6466
6467 This is another paragraph in the same
6468 selection.»
6469
6470 «\tThis is a very long indented line
6471 \tthat will be wrapped.ˇ»
6472 "},
6473 plaintext_language,
6474 &mut cx,
6475 );
6476
6477 // Test that an empty comment line acts as a paragraph boundary
6478 assert_rewrap(
6479 indoc! {"
6480 // ˇThis is a long comment that will be wrapped.
6481 //
6482 // And this is another long comment that will also be wrapped.ˇ
6483 "},
6484 indoc! {"
6485 // ˇThis is a long comment that will be
6486 // wrapped.
6487 //
6488 // And this is another long comment that
6489 // will also be wrapped.ˇ
6490 "},
6491 cpp_language,
6492 &mut cx,
6493 );
6494
6495 #[track_caller]
6496 fn assert_rewrap(
6497 unwrapped_text: &str,
6498 wrapped_text: &str,
6499 language: Arc<Language>,
6500 cx: &mut EditorTestContext,
6501 ) {
6502 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6503 cx.set_state(unwrapped_text);
6504 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6505 cx.assert_editor_state(wrapped_text);
6506 }
6507}
6508
6509#[gpui::test]
6510async fn test_rewrap_block_comments(cx: &mut TestAppContext) {
6511 init_test(cx, |settings| {
6512 settings.languages.0.extend([(
6513 "Rust".into(),
6514 LanguageSettingsContent {
6515 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6516 preferred_line_length: Some(40),
6517 ..Default::default()
6518 },
6519 )])
6520 });
6521
6522 let mut cx = EditorTestContext::new(cx).await;
6523
6524 let rust_lang = Arc::new(
6525 Language::new(
6526 LanguageConfig {
6527 name: "Rust".into(),
6528 line_comments: vec!["// ".into()],
6529 block_comment: Some(BlockCommentConfig {
6530 start: "/*".into(),
6531 end: "*/".into(),
6532 prefix: "* ".into(),
6533 tab_size: 1,
6534 }),
6535 documentation_comment: Some(BlockCommentConfig {
6536 start: "/**".into(),
6537 end: "*/".into(),
6538 prefix: "* ".into(),
6539 tab_size: 1,
6540 }),
6541
6542 ..LanguageConfig::default()
6543 },
6544 Some(tree_sitter_rust::LANGUAGE.into()),
6545 )
6546 .with_override_query("[(line_comment) (block_comment)] @comment.inclusive")
6547 .unwrap(),
6548 );
6549
6550 // regular block comment
6551 assert_rewrap(
6552 indoc! {"
6553 /*
6554 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6555 */
6556 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6557 "},
6558 indoc! {"
6559 /*
6560 *ˇ Lorem ipsum dolor sit amet,
6561 * consectetur adipiscing elit.
6562 */
6563 /*
6564 *ˇ Lorem ipsum dolor sit amet,
6565 * consectetur adipiscing elit.
6566 */
6567 "},
6568 rust_lang.clone(),
6569 &mut cx,
6570 );
6571
6572 // indent is respected
6573 assert_rewrap(
6574 indoc! {"
6575 {}
6576 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6577 "},
6578 indoc! {"
6579 {}
6580 /*
6581 *ˇ Lorem ipsum dolor sit amet,
6582 * consectetur adipiscing elit.
6583 */
6584 "},
6585 rust_lang.clone(),
6586 &mut cx,
6587 );
6588
6589 // short block comments with inline delimiters
6590 assert_rewrap(
6591 indoc! {"
6592 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6593 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6594 */
6595 /*
6596 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6597 "},
6598 indoc! {"
6599 /*
6600 *ˇ Lorem ipsum dolor sit amet,
6601 * consectetur adipiscing elit.
6602 */
6603 /*
6604 *ˇ Lorem ipsum dolor sit amet,
6605 * consectetur adipiscing elit.
6606 */
6607 /*
6608 *ˇ Lorem ipsum dolor sit amet,
6609 * consectetur adipiscing elit.
6610 */
6611 "},
6612 rust_lang.clone(),
6613 &mut cx,
6614 );
6615
6616 // multiline block comment with inline start/end delimiters
6617 assert_rewrap(
6618 indoc! {"
6619 /*ˇ Lorem ipsum dolor sit amet,
6620 * consectetur adipiscing elit. */
6621 "},
6622 indoc! {"
6623 /*
6624 *ˇ Lorem ipsum dolor sit amet,
6625 * consectetur adipiscing elit.
6626 */
6627 "},
6628 rust_lang.clone(),
6629 &mut cx,
6630 );
6631
6632 // block comment rewrap still respects paragraph bounds
6633 assert_rewrap(
6634 indoc! {"
6635 /*
6636 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6637 *
6638 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6639 */
6640 "},
6641 indoc! {"
6642 /*
6643 *ˇ Lorem ipsum dolor sit amet,
6644 * consectetur adipiscing elit.
6645 *
6646 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6647 */
6648 "},
6649 rust_lang.clone(),
6650 &mut cx,
6651 );
6652
6653 // documentation comments
6654 assert_rewrap(
6655 indoc! {"
6656 /**ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6657 /**
6658 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6659 */
6660 "},
6661 indoc! {"
6662 /**
6663 *ˇ Lorem ipsum dolor sit amet,
6664 * consectetur adipiscing elit.
6665 */
6666 /**
6667 *ˇ Lorem ipsum dolor sit amet,
6668 * consectetur adipiscing elit.
6669 */
6670 "},
6671 rust_lang.clone(),
6672 &mut cx,
6673 );
6674
6675 // different, adjacent comments
6676 assert_rewrap(
6677 indoc! {"
6678 /**
6679 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6680 */
6681 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6682 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6683 "},
6684 indoc! {"
6685 /**
6686 *ˇ Lorem ipsum dolor sit amet,
6687 * consectetur adipiscing elit.
6688 */
6689 /*
6690 *ˇ Lorem ipsum dolor sit amet,
6691 * consectetur adipiscing elit.
6692 */
6693 //ˇ Lorem ipsum dolor sit amet,
6694 // consectetur adipiscing elit.
6695 "},
6696 rust_lang.clone(),
6697 &mut cx,
6698 );
6699
6700 // selection w/ single short block comment
6701 assert_rewrap(
6702 indoc! {"
6703 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6704 "},
6705 indoc! {"
6706 «/*
6707 * Lorem ipsum dolor sit amet,
6708 * consectetur adipiscing elit.
6709 */ˇ»
6710 "},
6711 rust_lang.clone(),
6712 &mut cx,
6713 );
6714
6715 // rewrapping a single comment w/ abutting comments
6716 assert_rewrap(
6717 indoc! {"
6718 /* ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. */
6719 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6720 "},
6721 indoc! {"
6722 /*
6723 * ˇLorem ipsum dolor sit amet,
6724 * consectetur adipiscing elit.
6725 */
6726 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6727 "},
6728 rust_lang.clone(),
6729 &mut cx,
6730 );
6731
6732 // selection w/ non-abutting short block comments
6733 assert_rewrap(
6734 indoc! {"
6735 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6736
6737 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6738 "},
6739 indoc! {"
6740 «/*
6741 * Lorem ipsum dolor sit amet,
6742 * consectetur adipiscing elit.
6743 */
6744
6745 /*
6746 * Lorem ipsum dolor sit amet,
6747 * consectetur adipiscing elit.
6748 */ˇ»
6749 "},
6750 rust_lang.clone(),
6751 &mut cx,
6752 );
6753
6754 // selection of multiline block comments
6755 assert_rewrap(
6756 indoc! {"
6757 «/* Lorem ipsum dolor sit amet,
6758 * consectetur adipiscing elit. */ˇ»
6759 "},
6760 indoc! {"
6761 «/*
6762 * Lorem ipsum dolor sit amet,
6763 * consectetur adipiscing elit.
6764 */ˇ»
6765 "},
6766 rust_lang.clone(),
6767 &mut cx,
6768 );
6769
6770 // partial selection of multiline block comments
6771 assert_rewrap(
6772 indoc! {"
6773 «/* Lorem ipsum dolor sit amet,ˇ»
6774 * consectetur adipiscing elit. */
6775 /* Lorem ipsum dolor sit amet,
6776 «* consectetur adipiscing elit. */ˇ»
6777 "},
6778 indoc! {"
6779 «/*
6780 * Lorem ipsum dolor sit amet,ˇ»
6781 * consectetur adipiscing elit. */
6782 /* Lorem ipsum dolor sit amet,
6783 «* consectetur adipiscing elit.
6784 */ˇ»
6785 "},
6786 rust_lang.clone(),
6787 &mut cx,
6788 );
6789
6790 // selection w/ abutting short block comments
6791 // TODO: should not be combined; should rewrap as 2 comments
6792 assert_rewrap(
6793 indoc! {"
6794 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6795 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6796 "},
6797 // desired behavior:
6798 // indoc! {"
6799 // «/*
6800 // * Lorem ipsum dolor sit amet,
6801 // * consectetur adipiscing elit.
6802 // */
6803 // /*
6804 // * Lorem ipsum dolor sit amet,
6805 // * consectetur adipiscing elit.
6806 // */ˇ»
6807 // "},
6808 // actual behaviour:
6809 indoc! {"
6810 «/*
6811 * Lorem ipsum dolor sit amet,
6812 * consectetur adipiscing elit. Lorem
6813 * ipsum dolor sit amet, consectetur
6814 * adipiscing elit.
6815 */ˇ»
6816 "},
6817 rust_lang.clone(),
6818 &mut cx,
6819 );
6820
6821 // TODO: same as above, but with delimiters on separate line
6822 // assert_rewrap(
6823 // indoc! {"
6824 // «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6825 // */
6826 // /*
6827 // * Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6828 // "},
6829 // // desired:
6830 // // indoc! {"
6831 // // «/*
6832 // // * Lorem ipsum dolor sit amet,
6833 // // * consectetur adipiscing elit.
6834 // // */
6835 // // /*
6836 // // * Lorem ipsum dolor sit amet,
6837 // // * consectetur adipiscing elit.
6838 // // */ˇ»
6839 // // "},
6840 // // actual: (but with trailing w/s on the empty lines)
6841 // indoc! {"
6842 // «/*
6843 // * Lorem ipsum dolor sit amet,
6844 // * consectetur adipiscing elit.
6845 // *
6846 // */
6847 // /*
6848 // *
6849 // * Lorem ipsum dolor sit amet,
6850 // * consectetur adipiscing elit.
6851 // */ˇ»
6852 // "},
6853 // rust_lang.clone(),
6854 // &mut cx,
6855 // );
6856
6857 // TODO these are unhandled edge cases; not correct, just documenting known issues
6858 assert_rewrap(
6859 indoc! {"
6860 /*
6861 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6862 */
6863 /*
6864 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6865 /*ˇ Lorem ipsum dolor sit amet */ /* consectetur adipiscing elit. */
6866 "},
6867 // desired:
6868 // indoc! {"
6869 // /*
6870 // *ˇ Lorem ipsum dolor sit amet,
6871 // * consectetur adipiscing elit.
6872 // */
6873 // /*
6874 // *ˇ Lorem ipsum dolor sit amet,
6875 // * consectetur adipiscing elit.
6876 // */
6877 // /*
6878 // *ˇ Lorem ipsum dolor sit amet
6879 // */ /* consectetur adipiscing elit. */
6880 // "},
6881 // actual:
6882 indoc! {"
6883 /*
6884 //ˇ Lorem ipsum dolor sit amet,
6885 // consectetur adipiscing elit.
6886 */
6887 /*
6888 * //ˇ Lorem ipsum dolor sit amet,
6889 * consectetur adipiscing elit.
6890 */
6891 /*
6892 *ˇ Lorem ipsum dolor sit amet */ /*
6893 * consectetur adipiscing elit.
6894 */
6895 "},
6896 rust_lang,
6897 &mut cx,
6898 );
6899
6900 #[track_caller]
6901 fn assert_rewrap(
6902 unwrapped_text: &str,
6903 wrapped_text: &str,
6904 language: Arc<Language>,
6905 cx: &mut EditorTestContext,
6906 ) {
6907 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6908 cx.set_state(unwrapped_text);
6909 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6910 cx.assert_editor_state(wrapped_text);
6911 }
6912}
6913
6914#[gpui::test]
6915async fn test_hard_wrap(cx: &mut TestAppContext) {
6916 init_test(cx, |_| {});
6917 let mut cx = EditorTestContext::new(cx).await;
6918
6919 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
6920 cx.update_editor(|editor, _, cx| {
6921 editor.set_hard_wrap(Some(14), cx);
6922 });
6923
6924 cx.set_state(indoc!(
6925 "
6926 one two three ˇ
6927 "
6928 ));
6929 cx.simulate_input("four");
6930 cx.run_until_parked();
6931
6932 cx.assert_editor_state(indoc!(
6933 "
6934 one two three
6935 fourˇ
6936 "
6937 ));
6938
6939 cx.update_editor(|editor, window, cx| {
6940 editor.newline(&Default::default(), window, cx);
6941 });
6942 cx.run_until_parked();
6943 cx.assert_editor_state(indoc!(
6944 "
6945 one two three
6946 four
6947 ˇ
6948 "
6949 ));
6950
6951 cx.simulate_input("five");
6952 cx.run_until_parked();
6953 cx.assert_editor_state(indoc!(
6954 "
6955 one two three
6956 four
6957 fiveˇ
6958 "
6959 ));
6960
6961 cx.update_editor(|editor, window, cx| {
6962 editor.newline(&Default::default(), window, cx);
6963 });
6964 cx.run_until_parked();
6965 cx.simulate_input("# ");
6966 cx.run_until_parked();
6967 cx.assert_editor_state(indoc!(
6968 "
6969 one two three
6970 four
6971 five
6972 # ˇ
6973 "
6974 ));
6975
6976 cx.update_editor(|editor, window, cx| {
6977 editor.newline(&Default::default(), window, cx);
6978 });
6979 cx.run_until_parked();
6980 cx.assert_editor_state(indoc!(
6981 "
6982 one two three
6983 four
6984 five
6985 #\x20
6986 #ˇ
6987 "
6988 ));
6989
6990 cx.simulate_input(" 6");
6991 cx.run_until_parked();
6992 cx.assert_editor_state(indoc!(
6993 "
6994 one two three
6995 four
6996 five
6997 #
6998 # 6ˇ
6999 "
7000 ));
7001}
7002
7003#[gpui::test]
7004async fn test_cut_line_ends(cx: &mut TestAppContext) {
7005 init_test(cx, |_| {});
7006
7007 let mut cx = EditorTestContext::new(cx).await;
7008
7009 cx.set_state(indoc! {"The quick brownˇ"});
7010 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
7011 cx.assert_editor_state(indoc! {"The quick brownˇ"});
7012
7013 cx.set_state(indoc! {"The emacs foxˇ"});
7014 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
7015 cx.assert_editor_state(indoc! {"The emacs foxˇ"});
7016
7017 cx.set_state(indoc! {"
7018 The quick« brownˇ»
7019 fox jumps overˇ
7020 the lazy dog"});
7021 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7022 cx.assert_editor_state(indoc! {"
7023 The quickˇ
7024 ˇthe lazy dog"});
7025
7026 cx.set_state(indoc! {"
7027 The quick« brownˇ»
7028 fox jumps overˇ
7029 the lazy dog"});
7030 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
7031 cx.assert_editor_state(indoc! {"
7032 The quickˇ
7033 fox jumps overˇthe lazy dog"});
7034
7035 cx.set_state(indoc! {"
7036 The quick« brownˇ»
7037 fox jumps overˇ
7038 the lazy dog"});
7039 cx.update_editor(|e, window, cx| {
7040 e.cut_to_end_of_line(
7041 &CutToEndOfLine {
7042 stop_at_newlines: true,
7043 },
7044 window,
7045 cx,
7046 )
7047 });
7048 cx.assert_editor_state(indoc! {"
7049 The quickˇ
7050 fox jumps overˇ
7051 the lazy dog"});
7052
7053 cx.set_state(indoc! {"
7054 The quick« brownˇ»
7055 fox jumps overˇ
7056 the lazy dog"});
7057 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
7058 cx.assert_editor_state(indoc! {"
7059 The quickˇ
7060 fox jumps overˇthe lazy dog"});
7061}
7062
7063#[gpui::test]
7064async fn test_clipboard(cx: &mut TestAppContext) {
7065 init_test(cx, |_| {});
7066
7067 let mut cx = EditorTestContext::new(cx).await;
7068
7069 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
7070 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7071 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
7072
7073 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
7074 cx.set_state("two ˇfour ˇsix ˇ");
7075 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7076 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
7077
7078 // Paste again but with only two cursors. Since the number of cursors doesn't
7079 // match the number of slices in the clipboard, the entire clipboard text
7080 // is pasted at each cursor.
7081 cx.set_state("ˇtwo one✅ four three six five ˇ");
7082 cx.update_editor(|e, window, cx| {
7083 e.handle_input("( ", window, cx);
7084 e.paste(&Paste, window, cx);
7085 e.handle_input(") ", window, cx);
7086 });
7087 cx.assert_editor_state(
7088 &([
7089 "( one✅ ",
7090 "three ",
7091 "five ) ˇtwo one✅ four three six five ( one✅ ",
7092 "three ",
7093 "five ) ˇ",
7094 ]
7095 .join("\n")),
7096 );
7097
7098 // Cut with three selections, one of which is full-line.
7099 cx.set_state(indoc! {"
7100 1«2ˇ»3
7101 4ˇ567
7102 «8ˇ»9"});
7103 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7104 cx.assert_editor_state(indoc! {"
7105 1ˇ3
7106 ˇ9"});
7107
7108 // Paste with three selections, noticing how the copied selection that was full-line
7109 // gets inserted before the second cursor.
7110 cx.set_state(indoc! {"
7111 1ˇ3
7112 9ˇ
7113 «oˇ»ne"});
7114 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7115 cx.assert_editor_state(indoc! {"
7116 12ˇ3
7117 4567
7118 9ˇ
7119 8ˇne"});
7120
7121 // Copy with a single cursor only, which writes the whole line into the clipboard.
7122 cx.set_state(indoc! {"
7123 The quick brown
7124 fox juˇmps over
7125 the lazy dog"});
7126 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7127 assert_eq!(
7128 cx.read_from_clipboard()
7129 .and_then(|item| item.text().as_deref().map(str::to_string)),
7130 Some("fox jumps over\n".to_string())
7131 );
7132
7133 // Paste with three selections, noticing how the copied full-line selection is inserted
7134 // before the empty selections but replaces the selection that is non-empty.
7135 cx.set_state(indoc! {"
7136 Tˇhe quick brown
7137 «foˇ»x jumps over
7138 tˇhe lazy dog"});
7139 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7140 cx.assert_editor_state(indoc! {"
7141 fox jumps over
7142 Tˇhe quick brown
7143 fox jumps over
7144 ˇx jumps over
7145 fox jumps over
7146 tˇhe lazy dog"});
7147}
7148
7149#[gpui::test]
7150async fn test_copy_trim(cx: &mut TestAppContext) {
7151 init_test(cx, |_| {});
7152
7153 let mut cx = EditorTestContext::new(cx).await;
7154 cx.set_state(
7155 r#" «for selection in selections.iter() {
7156 let mut start = selection.start;
7157 let mut end = selection.end;
7158 let is_entire_line = selection.is_empty();
7159 if is_entire_line {
7160 start = Point::new(start.row, 0);ˇ»
7161 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7162 }
7163 "#,
7164 );
7165 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7166 assert_eq!(
7167 cx.read_from_clipboard()
7168 .and_then(|item| item.text().as_deref().map(str::to_string)),
7169 Some(
7170 "for selection in selections.iter() {
7171 let mut start = selection.start;
7172 let mut end = selection.end;
7173 let is_entire_line = selection.is_empty();
7174 if is_entire_line {
7175 start = Point::new(start.row, 0);"
7176 .to_string()
7177 ),
7178 "Regular copying preserves all indentation selected",
7179 );
7180 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7181 assert_eq!(
7182 cx.read_from_clipboard()
7183 .and_then(|item| item.text().as_deref().map(str::to_string)),
7184 Some(
7185 "for selection in selections.iter() {
7186let mut start = selection.start;
7187let mut end = selection.end;
7188let is_entire_line = selection.is_empty();
7189if is_entire_line {
7190 start = Point::new(start.row, 0);"
7191 .to_string()
7192 ),
7193 "Copying with stripping should strip all leading whitespaces"
7194 );
7195
7196 cx.set_state(
7197 r#" « for selection in selections.iter() {
7198 let mut start = selection.start;
7199 let mut end = selection.end;
7200 let is_entire_line = selection.is_empty();
7201 if is_entire_line {
7202 start = Point::new(start.row, 0);ˇ»
7203 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7204 }
7205 "#,
7206 );
7207 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7208 assert_eq!(
7209 cx.read_from_clipboard()
7210 .and_then(|item| item.text().as_deref().map(str::to_string)),
7211 Some(
7212 " for selection in selections.iter() {
7213 let mut start = selection.start;
7214 let mut end = selection.end;
7215 let is_entire_line = selection.is_empty();
7216 if is_entire_line {
7217 start = Point::new(start.row, 0);"
7218 .to_string()
7219 ),
7220 "Regular copying preserves all indentation selected",
7221 );
7222 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7223 assert_eq!(
7224 cx.read_from_clipboard()
7225 .and_then(|item| item.text().as_deref().map(str::to_string)),
7226 Some(
7227 "for selection in selections.iter() {
7228let mut start = selection.start;
7229let mut end = selection.end;
7230let is_entire_line = selection.is_empty();
7231if is_entire_line {
7232 start = Point::new(start.row, 0);"
7233 .to_string()
7234 ),
7235 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
7236 );
7237
7238 cx.set_state(
7239 r#" «ˇ for selection in selections.iter() {
7240 let mut start = selection.start;
7241 let mut end = selection.end;
7242 let is_entire_line = selection.is_empty();
7243 if is_entire_line {
7244 start = Point::new(start.row, 0);»
7245 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7246 }
7247 "#,
7248 );
7249 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7250 assert_eq!(
7251 cx.read_from_clipboard()
7252 .and_then(|item| item.text().as_deref().map(str::to_string)),
7253 Some(
7254 " for selection in selections.iter() {
7255 let mut start = selection.start;
7256 let mut end = selection.end;
7257 let is_entire_line = selection.is_empty();
7258 if is_entire_line {
7259 start = Point::new(start.row, 0);"
7260 .to_string()
7261 ),
7262 "Regular copying for reverse selection works the same",
7263 );
7264 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7265 assert_eq!(
7266 cx.read_from_clipboard()
7267 .and_then(|item| item.text().as_deref().map(str::to_string)),
7268 Some(
7269 "for selection in selections.iter() {
7270let mut start = selection.start;
7271let mut end = selection.end;
7272let is_entire_line = selection.is_empty();
7273if is_entire_line {
7274 start = Point::new(start.row, 0);"
7275 .to_string()
7276 ),
7277 "Copying with stripping for reverse selection works the same"
7278 );
7279
7280 cx.set_state(
7281 r#" for selection «in selections.iter() {
7282 let mut start = selection.start;
7283 let mut end = selection.end;
7284 let is_entire_line = selection.is_empty();
7285 if is_entire_line {
7286 start = Point::new(start.row, 0);ˇ»
7287 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7288 }
7289 "#,
7290 );
7291 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7292 assert_eq!(
7293 cx.read_from_clipboard()
7294 .and_then(|item| item.text().as_deref().map(str::to_string)),
7295 Some(
7296 "in selections.iter() {
7297 let mut start = selection.start;
7298 let mut end = selection.end;
7299 let is_entire_line = selection.is_empty();
7300 if is_entire_line {
7301 start = Point::new(start.row, 0);"
7302 .to_string()
7303 ),
7304 "When selecting past the indent, the copying works as usual",
7305 );
7306 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7307 assert_eq!(
7308 cx.read_from_clipboard()
7309 .and_then(|item| item.text().as_deref().map(str::to_string)),
7310 Some(
7311 "in selections.iter() {
7312 let mut start = selection.start;
7313 let mut end = selection.end;
7314 let is_entire_line = selection.is_empty();
7315 if is_entire_line {
7316 start = Point::new(start.row, 0);"
7317 .to_string()
7318 ),
7319 "When selecting past the indent, nothing is trimmed"
7320 );
7321
7322 cx.set_state(
7323 r#" «for selection in selections.iter() {
7324 let mut start = selection.start;
7325
7326 let mut end = selection.end;
7327 let is_entire_line = selection.is_empty();
7328 if is_entire_line {
7329 start = Point::new(start.row, 0);
7330ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
7331 }
7332 "#,
7333 );
7334 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7335 assert_eq!(
7336 cx.read_from_clipboard()
7337 .and_then(|item| item.text().as_deref().map(str::to_string)),
7338 Some(
7339 "for selection in selections.iter() {
7340let mut start = selection.start;
7341
7342let mut end = selection.end;
7343let is_entire_line = selection.is_empty();
7344if is_entire_line {
7345 start = Point::new(start.row, 0);
7346"
7347 .to_string()
7348 ),
7349 "Copying with stripping should ignore empty lines"
7350 );
7351}
7352
7353#[gpui::test]
7354async fn test_paste_multiline(cx: &mut TestAppContext) {
7355 init_test(cx, |_| {});
7356
7357 let mut cx = EditorTestContext::new(cx).await;
7358 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7359
7360 // Cut an indented block, without the leading whitespace.
7361 cx.set_state(indoc! {"
7362 const a: B = (
7363 c(),
7364 «d(
7365 e,
7366 f
7367 )ˇ»
7368 );
7369 "});
7370 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7371 cx.assert_editor_state(indoc! {"
7372 const a: B = (
7373 c(),
7374 ˇ
7375 );
7376 "});
7377
7378 // Paste it at the same position.
7379 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7380 cx.assert_editor_state(indoc! {"
7381 const a: B = (
7382 c(),
7383 d(
7384 e,
7385 f
7386 )ˇ
7387 );
7388 "});
7389
7390 // Paste it at a line with a lower indent level.
7391 cx.set_state(indoc! {"
7392 ˇ
7393 const a: B = (
7394 c(),
7395 );
7396 "});
7397 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7398 cx.assert_editor_state(indoc! {"
7399 d(
7400 e,
7401 f
7402 )ˇ
7403 const a: B = (
7404 c(),
7405 );
7406 "});
7407
7408 // Cut an indented block, with the leading whitespace.
7409 cx.set_state(indoc! {"
7410 const a: B = (
7411 c(),
7412 « d(
7413 e,
7414 f
7415 )
7416 ˇ»);
7417 "});
7418 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7419 cx.assert_editor_state(indoc! {"
7420 const a: B = (
7421 c(),
7422 ˇ);
7423 "});
7424
7425 // Paste it at the same position.
7426 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7427 cx.assert_editor_state(indoc! {"
7428 const a: B = (
7429 c(),
7430 d(
7431 e,
7432 f
7433 )
7434 ˇ);
7435 "});
7436
7437 // Paste it at a line with a higher indent level.
7438 cx.set_state(indoc! {"
7439 const a: B = (
7440 c(),
7441 d(
7442 e,
7443 fˇ
7444 )
7445 );
7446 "});
7447 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7448 cx.assert_editor_state(indoc! {"
7449 const a: B = (
7450 c(),
7451 d(
7452 e,
7453 f d(
7454 e,
7455 f
7456 )
7457 ˇ
7458 )
7459 );
7460 "});
7461
7462 // Copy an indented block, starting mid-line
7463 cx.set_state(indoc! {"
7464 const a: B = (
7465 c(),
7466 somethin«g(
7467 e,
7468 f
7469 )ˇ»
7470 );
7471 "});
7472 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7473
7474 // Paste it on a line with a lower indent level
7475 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
7476 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7477 cx.assert_editor_state(indoc! {"
7478 const a: B = (
7479 c(),
7480 something(
7481 e,
7482 f
7483 )
7484 );
7485 g(
7486 e,
7487 f
7488 )ˇ"});
7489}
7490
7491#[gpui::test]
7492async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
7493 init_test(cx, |_| {});
7494
7495 cx.write_to_clipboard(ClipboardItem::new_string(
7496 " d(\n e\n );\n".into(),
7497 ));
7498
7499 let mut cx = EditorTestContext::new(cx).await;
7500 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7501
7502 cx.set_state(indoc! {"
7503 fn a() {
7504 b();
7505 if c() {
7506 ˇ
7507 }
7508 }
7509 "});
7510
7511 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7512 cx.assert_editor_state(indoc! {"
7513 fn a() {
7514 b();
7515 if c() {
7516 d(
7517 e
7518 );
7519 ˇ
7520 }
7521 }
7522 "});
7523
7524 cx.set_state(indoc! {"
7525 fn a() {
7526 b();
7527 ˇ
7528 }
7529 "});
7530
7531 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7532 cx.assert_editor_state(indoc! {"
7533 fn a() {
7534 b();
7535 d(
7536 e
7537 );
7538 ˇ
7539 }
7540 "});
7541}
7542
7543#[gpui::test]
7544fn test_select_all(cx: &mut TestAppContext) {
7545 init_test(cx, |_| {});
7546
7547 let editor = cx.add_window(|window, cx| {
7548 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
7549 build_editor(buffer, window, cx)
7550 });
7551 _ = editor.update(cx, |editor, window, cx| {
7552 editor.select_all(&SelectAll, window, cx);
7553 assert_eq!(
7554 display_ranges(editor, cx),
7555 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
7556 );
7557 });
7558}
7559
7560#[gpui::test]
7561fn test_select_line(cx: &mut TestAppContext) {
7562 init_test(cx, |_| {});
7563
7564 let editor = cx.add_window(|window, cx| {
7565 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
7566 build_editor(buffer, window, cx)
7567 });
7568 _ = editor.update(cx, |editor, window, cx| {
7569 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7570 s.select_display_ranges([
7571 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7572 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7573 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7574 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
7575 ])
7576 });
7577 editor.select_line(&SelectLine, window, cx);
7578 assert_eq!(
7579 display_ranges(editor, cx),
7580 vec![
7581 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
7582 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
7583 ]
7584 );
7585 });
7586
7587 _ = editor.update(cx, |editor, window, cx| {
7588 editor.select_line(&SelectLine, window, cx);
7589 assert_eq!(
7590 display_ranges(editor, cx),
7591 vec![
7592 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
7593 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
7594 ]
7595 );
7596 });
7597
7598 _ = editor.update(cx, |editor, window, cx| {
7599 editor.select_line(&SelectLine, window, cx);
7600 assert_eq!(
7601 display_ranges(editor, cx),
7602 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
7603 );
7604 });
7605}
7606
7607#[gpui::test]
7608async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
7609 init_test(cx, |_| {});
7610 let mut cx = EditorTestContext::new(cx).await;
7611
7612 #[track_caller]
7613 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
7614 cx.set_state(initial_state);
7615 cx.update_editor(|e, window, cx| {
7616 e.split_selection_into_lines(&Default::default(), window, cx)
7617 });
7618 cx.assert_editor_state(expected_state);
7619 }
7620
7621 // Selection starts and ends at the middle of lines, left-to-right
7622 test(
7623 &mut cx,
7624 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
7625 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7626 );
7627 // Same thing, right-to-left
7628 test(
7629 &mut cx,
7630 "aa\nb«b\ncc\ndd\neˇ»e\nff",
7631 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7632 );
7633
7634 // Whole buffer, left-to-right, last line *doesn't* end with newline
7635 test(
7636 &mut cx,
7637 "«ˇaa\nbb\ncc\ndd\nee\nff»",
7638 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7639 );
7640 // Same thing, right-to-left
7641 test(
7642 &mut cx,
7643 "«aa\nbb\ncc\ndd\nee\nffˇ»",
7644 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7645 );
7646
7647 // Whole buffer, left-to-right, last line ends with newline
7648 test(
7649 &mut cx,
7650 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
7651 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7652 );
7653 // Same thing, right-to-left
7654 test(
7655 &mut cx,
7656 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
7657 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7658 );
7659
7660 // Starts at the end of a line, ends at the start of another
7661 test(
7662 &mut cx,
7663 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
7664 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
7665 );
7666}
7667
7668#[gpui::test]
7669async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
7670 init_test(cx, |_| {});
7671
7672 let editor = cx.add_window(|window, cx| {
7673 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
7674 build_editor(buffer, window, cx)
7675 });
7676
7677 // setup
7678 _ = editor.update(cx, |editor, window, cx| {
7679 editor.fold_creases(
7680 vec![
7681 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
7682 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
7683 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
7684 ],
7685 true,
7686 window,
7687 cx,
7688 );
7689 assert_eq!(
7690 editor.display_text(cx),
7691 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7692 );
7693 });
7694
7695 _ = editor.update(cx, |editor, window, cx| {
7696 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7697 s.select_display_ranges([
7698 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7699 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7700 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7701 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
7702 ])
7703 });
7704 editor.split_selection_into_lines(&Default::default(), window, cx);
7705 assert_eq!(
7706 editor.display_text(cx),
7707 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7708 );
7709 });
7710 EditorTestContext::for_editor(editor, cx)
7711 .await
7712 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
7713
7714 _ = editor.update(cx, |editor, window, cx| {
7715 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7716 s.select_display_ranges([
7717 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
7718 ])
7719 });
7720 editor.split_selection_into_lines(&Default::default(), window, cx);
7721 assert_eq!(
7722 editor.display_text(cx),
7723 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
7724 );
7725 assert_eq!(
7726 display_ranges(editor, cx),
7727 [
7728 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
7729 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
7730 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
7731 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
7732 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
7733 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
7734 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
7735 ]
7736 );
7737 });
7738 EditorTestContext::for_editor(editor, cx)
7739 .await
7740 .assert_editor_state(
7741 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
7742 );
7743}
7744
7745#[gpui::test]
7746async fn test_add_selection_above_below(cx: &mut TestAppContext) {
7747 init_test(cx, |_| {});
7748
7749 let mut cx = EditorTestContext::new(cx).await;
7750
7751 cx.set_state(indoc!(
7752 r#"abc
7753 defˇghi
7754
7755 jk
7756 nlmo
7757 "#
7758 ));
7759
7760 cx.update_editor(|editor, window, cx| {
7761 editor.add_selection_above(&Default::default(), window, cx);
7762 });
7763
7764 cx.assert_editor_state(indoc!(
7765 r#"abcˇ
7766 defˇghi
7767
7768 jk
7769 nlmo
7770 "#
7771 ));
7772
7773 cx.update_editor(|editor, window, cx| {
7774 editor.add_selection_above(&Default::default(), window, cx);
7775 });
7776
7777 cx.assert_editor_state(indoc!(
7778 r#"abcˇ
7779 defˇghi
7780
7781 jk
7782 nlmo
7783 "#
7784 ));
7785
7786 cx.update_editor(|editor, window, cx| {
7787 editor.add_selection_below(&Default::default(), window, cx);
7788 });
7789
7790 cx.assert_editor_state(indoc!(
7791 r#"abc
7792 defˇghi
7793
7794 jk
7795 nlmo
7796 "#
7797 ));
7798
7799 cx.update_editor(|editor, window, cx| {
7800 editor.undo_selection(&Default::default(), window, cx);
7801 });
7802
7803 cx.assert_editor_state(indoc!(
7804 r#"abcˇ
7805 defˇghi
7806
7807 jk
7808 nlmo
7809 "#
7810 ));
7811
7812 cx.update_editor(|editor, window, cx| {
7813 editor.redo_selection(&Default::default(), window, cx);
7814 });
7815
7816 cx.assert_editor_state(indoc!(
7817 r#"abc
7818 defˇghi
7819
7820 jk
7821 nlmo
7822 "#
7823 ));
7824
7825 cx.update_editor(|editor, window, cx| {
7826 editor.add_selection_below(&Default::default(), window, cx);
7827 });
7828
7829 cx.assert_editor_state(indoc!(
7830 r#"abc
7831 defˇghi
7832 ˇ
7833 jk
7834 nlmo
7835 "#
7836 ));
7837
7838 cx.update_editor(|editor, window, cx| {
7839 editor.add_selection_below(&Default::default(), window, cx);
7840 });
7841
7842 cx.assert_editor_state(indoc!(
7843 r#"abc
7844 defˇghi
7845 ˇ
7846 jkˇ
7847 nlmo
7848 "#
7849 ));
7850
7851 cx.update_editor(|editor, window, cx| {
7852 editor.add_selection_below(&Default::default(), window, cx);
7853 });
7854
7855 cx.assert_editor_state(indoc!(
7856 r#"abc
7857 defˇghi
7858 ˇ
7859 jkˇ
7860 nlmˇo
7861 "#
7862 ));
7863
7864 cx.update_editor(|editor, window, cx| {
7865 editor.add_selection_below(&Default::default(), window, cx);
7866 });
7867
7868 cx.assert_editor_state(indoc!(
7869 r#"abc
7870 defˇghi
7871 ˇ
7872 jkˇ
7873 nlmˇo
7874 ˇ"#
7875 ));
7876
7877 // change selections
7878 cx.set_state(indoc!(
7879 r#"abc
7880 def«ˇg»hi
7881
7882 jk
7883 nlmo
7884 "#
7885 ));
7886
7887 cx.update_editor(|editor, window, cx| {
7888 editor.add_selection_below(&Default::default(), window, cx);
7889 });
7890
7891 cx.assert_editor_state(indoc!(
7892 r#"abc
7893 def«ˇg»hi
7894
7895 jk
7896 nlm«ˇo»
7897 "#
7898 ));
7899
7900 cx.update_editor(|editor, window, cx| {
7901 editor.add_selection_below(&Default::default(), window, cx);
7902 });
7903
7904 cx.assert_editor_state(indoc!(
7905 r#"abc
7906 def«ˇg»hi
7907
7908 jk
7909 nlm«ˇo»
7910 "#
7911 ));
7912
7913 cx.update_editor(|editor, window, cx| {
7914 editor.add_selection_above(&Default::default(), window, cx);
7915 });
7916
7917 cx.assert_editor_state(indoc!(
7918 r#"abc
7919 def«ˇg»hi
7920
7921 jk
7922 nlmo
7923 "#
7924 ));
7925
7926 cx.update_editor(|editor, window, cx| {
7927 editor.add_selection_above(&Default::default(), window, cx);
7928 });
7929
7930 cx.assert_editor_state(indoc!(
7931 r#"abc
7932 def«ˇg»hi
7933
7934 jk
7935 nlmo
7936 "#
7937 ));
7938
7939 // Change selections again
7940 cx.set_state(indoc!(
7941 r#"a«bc
7942 defgˇ»hi
7943
7944 jk
7945 nlmo
7946 "#
7947 ));
7948
7949 cx.update_editor(|editor, window, cx| {
7950 editor.add_selection_below(&Default::default(), window, cx);
7951 });
7952
7953 cx.assert_editor_state(indoc!(
7954 r#"a«bcˇ»
7955 d«efgˇ»hi
7956
7957 j«kˇ»
7958 nlmo
7959 "#
7960 ));
7961
7962 cx.update_editor(|editor, window, cx| {
7963 editor.add_selection_below(&Default::default(), window, cx);
7964 });
7965 cx.assert_editor_state(indoc!(
7966 r#"a«bcˇ»
7967 d«efgˇ»hi
7968
7969 j«kˇ»
7970 n«lmoˇ»
7971 "#
7972 ));
7973 cx.update_editor(|editor, window, cx| {
7974 editor.add_selection_above(&Default::default(), window, cx);
7975 });
7976
7977 cx.assert_editor_state(indoc!(
7978 r#"a«bcˇ»
7979 d«efgˇ»hi
7980
7981 j«kˇ»
7982 nlmo
7983 "#
7984 ));
7985
7986 // Change selections again
7987 cx.set_state(indoc!(
7988 r#"abc
7989 d«ˇefghi
7990
7991 jk
7992 nlm»o
7993 "#
7994 ));
7995
7996 cx.update_editor(|editor, window, cx| {
7997 editor.add_selection_above(&Default::default(), window, cx);
7998 });
7999
8000 cx.assert_editor_state(indoc!(
8001 r#"a«ˇbc»
8002 d«ˇef»ghi
8003
8004 j«ˇk»
8005 n«ˇlm»o
8006 "#
8007 ));
8008
8009 cx.update_editor(|editor, window, cx| {
8010 editor.add_selection_below(&Default::default(), window, cx);
8011 });
8012
8013 cx.assert_editor_state(indoc!(
8014 r#"abc
8015 d«ˇef»ghi
8016
8017 j«ˇk»
8018 n«ˇlm»o
8019 "#
8020 ));
8021}
8022
8023#[gpui::test]
8024async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
8025 init_test(cx, |_| {});
8026 let mut cx = EditorTestContext::new(cx).await;
8027
8028 cx.set_state(indoc!(
8029 r#"line onˇe
8030 liˇne two
8031 line three
8032 line four"#
8033 ));
8034
8035 cx.update_editor(|editor, window, cx| {
8036 editor.add_selection_below(&Default::default(), window, cx);
8037 });
8038
8039 // test multiple cursors expand in the same direction
8040 cx.assert_editor_state(indoc!(
8041 r#"line onˇe
8042 liˇne twˇo
8043 liˇne three
8044 line four"#
8045 ));
8046
8047 cx.update_editor(|editor, window, cx| {
8048 editor.add_selection_below(&Default::default(), window, cx);
8049 });
8050
8051 cx.update_editor(|editor, window, cx| {
8052 editor.add_selection_below(&Default::default(), window, cx);
8053 });
8054
8055 // test multiple cursors expand below overflow
8056 cx.assert_editor_state(indoc!(
8057 r#"line onˇe
8058 liˇne twˇo
8059 liˇne thˇree
8060 liˇne foˇur"#
8061 ));
8062
8063 cx.update_editor(|editor, window, cx| {
8064 editor.add_selection_above(&Default::default(), window, cx);
8065 });
8066
8067 // test multiple cursors retrieves back correctly
8068 cx.assert_editor_state(indoc!(
8069 r#"line onˇe
8070 liˇne twˇo
8071 liˇne thˇree
8072 line four"#
8073 ));
8074
8075 cx.update_editor(|editor, window, cx| {
8076 editor.add_selection_above(&Default::default(), window, cx);
8077 });
8078
8079 cx.update_editor(|editor, window, cx| {
8080 editor.add_selection_above(&Default::default(), window, cx);
8081 });
8082
8083 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
8084 cx.assert_editor_state(indoc!(
8085 r#"liˇne onˇe
8086 liˇne two
8087 line three
8088 line four"#
8089 ));
8090
8091 cx.update_editor(|editor, window, cx| {
8092 editor.undo_selection(&Default::default(), window, cx);
8093 });
8094
8095 // test undo
8096 cx.assert_editor_state(indoc!(
8097 r#"line onˇe
8098 liˇne twˇo
8099 line three
8100 line four"#
8101 ));
8102
8103 cx.update_editor(|editor, window, cx| {
8104 editor.redo_selection(&Default::default(), window, cx);
8105 });
8106
8107 // test redo
8108 cx.assert_editor_state(indoc!(
8109 r#"liˇne onˇe
8110 liˇne two
8111 line three
8112 line four"#
8113 ));
8114
8115 cx.set_state(indoc!(
8116 r#"abcd
8117 ef«ghˇ»
8118 ijkl
8119 «mˇ»nop"#
8120 ));
8121
8122 cx.update_editor(|editor, window, cx| {
8123 editor.add_selection_above(&Default::default(), window, cx);
8124 });
8125
8126 // test multiple selections expand in the same direction
8127 cx.assert_editor_state(indoc!(
8128 r#"ab«cdˇ»
8129 ef«ghˇ»
8130 «iˇ»jkl
8131 «mˇ»nop"#
8132 ));
8133
8134 cx.update_editor(|editor, window, cx| {
8135 editor.add_selection_above(&Default::default(), window, cx);
8136 });
8137
8138 // test multiple selection upward overflow
8139 cx.assert_editor_state(indoc!(
8140 r#"ab«cdˇ»
8141 «eˇ»f«ghˇ»
8142 «iˇ»jkl
8143 «mˇ»nop"#
8144 ));
8145
8146 cx.update_editor(|editor, window, cx| {
8147 editor.add_selection_below(&Default::default(), window, cx);
8148 });
8149
8150 // test multiple selection retrieves back correctly
8151 cx.assert_editor_state(indoc!(
8152 r#"abcd
8153 ef«ghˇ»
8154 «iˇ»jkl
8155 «mˇ»nop"#
8156 ));
8157
8158 cx.update_editor(|editor, window, cx| {
8159 editor.add_selection_below(&Default::default(), window, cx);
8160 });
8161
8162 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
8163 cx.assert_editor_state(indoc!(
8164 r#"abcd
8165 ef«ghˇ»
8166 ij«klˇ»
8167 «mˇ»nop"#
8168 ));
8169
8170 cx.update_editor(|editor, window, cx| {
8171 editor.undo_selection(&Default::default(), window, cx);
8172 });
8173
8174 // test undo
8175 cx.assert_editor_state(indoc!(
8176 r#"abcd
8177 ef«ghˇ»
8178 «iˇ»jkl
8179 «mˇ»nop"#
8180 ));
8181
8182 cx.update_editor(|editor, window, cx| {
8183 editor.redo_selection(&Default::default(), window, cx);
8184 });
8185
8186 // test redo
8187 cx.assert_editor_state(indoc!(
8188 r#"abcd
8189 ef«ghˇ»
8190 ij«klˇ»
8191 «mˇ»nop"#
8192 ));
8193}
8194
8195#[gpui::test]
8196async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
8197 init_test(cx, |_| {});
8198 let mut cx = EditorTestContext::new(cx).await;
8199
8200 cx.set_state(indoc!(
8201 r#"line onˇe
8202 liˇne two
8203 line three
8204 line four"#
8205 ));
8206
8207 cx.update_editor(|editor, window, cx| {
8208 editor.add_selection_below(&Default::default(), window, cx);
8209 editor.add_selection_below(&Default::default(), window, cx);
8210 editor.add_selection_below(&Default::default(), window, cx);
8211 });
8212
8213 // initial state with two multi cursor groups
8214 cx.assert_editor_state(indoc!(
8215 r#"line onˇe
8216 liˇne twˇo
8217 liˇne thˇree
8218 liˇne foˇur"#
8219 ));
8220
8221 // add single cursor in middle - simulate opt click
8222 cx.update_editor(|editor, window, cx| {
8223 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
8224 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8225 editor.end_selection(window, cx);
8226 });
8227
8228 cx.assert_editor_state(indoc!(
8229 r#"line onˇe
8230 liˇne twˇo
8231 liˇneˇ thˇree
8232 liˇne foˇur"#
8233 ));
8234
8235 cx.update_editor(|editor, window, cx| {
8236 editor.add_selection_above(&Default::default(), window, cx);
8237 });
8238
8239 // test new added selection expands above and existing selection shrinks
8240 cx.assert_editor_state(indoc!(
8241 r#"line onˇe
8242 liˇneˇ twˇo
8243 liˇneˇ thˇree
8244 line four"#
8245 ));
8246
8247 cx.update_editor(|editor, window, cx| {
8248 editor.add_selection_above(&Default::default(), window, cx);
8249 });
8250
8251 // test new added selection expands above and existing selection shrinks
8252 cx.assert_editor_state(indoc!(
8253 r#"lineˇ onˇe
8254 liˇneˇ twˇo
8255 lineˇ three
8256 line four"#
8257 ));
8258
8259 // intial state with two selection groups
8260 cx.set_state(indoc!(
8261 r#"abcd
8262 ef«ghˇ»
8263 ijkl
8264 «mˇ»nop"#
8265 ));
8266
8267 cx.update_editor(|editor, window, cx| {
8268 editor.add_selection_above(&Default::default(), window, cx);
8269 editor.add_selection_above(&Default::default(), window, cx);
8270 });
8271
8272 cx.assert_editor_state(indoc!(
8273 r#"ab«cdˇ»
8274 «eˇ»f«ghˇ»
8275 «iˇ»jkl
8276 «mˇ»nop"#
8277 ));
8278
8279 // add single selection in middle - simulate opt drag
8280 cx.update_editor(|editor, window, cx| {
8281 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
8282 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8283 editor.update_selection(
8284 DisplayPoint::new(DisplayRow(2), 4),
8285 0,
8286 gpui::Point::<f32>::default(),
8287 window,
8288 cx,
8289 );
8290 editor.end_selection(window, cx);
8291 });
8292
8293 cx.assert_editor_state(indoc!(
8294 r#"ab«cdˇ»
8295 «eˇ»f«ghˇ»
8296 «iˇ»jk«lˇ»
8297 «mˇ»nop"#
8298 ));
8299
8300 cx.update_editor(|editor, window, cx| {
8301 editor.add_selection_below(&Default::default(), window, cx);
8302 });
8303
8304 // test new added selection expands below, others shrinks from above
8305 cx.assert_editor_state(indoc!(
8306 r#"abcd
8307 ef«ghˇ»
8308 «iˇ»jk«lˇ»
8309 «mˇ»no«pˇ»"#
8310 ));
8311}
8312
8313#[gpui::test]
8314async fn test_select_next(cx: &mut TestAppContext) {
8315 init_test(cx, |_| {});
8316
8317 let mut cx = EditorTestContext::new(cx).await;
8318 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8319
8320 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8321 .unwrap();
8322 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8323
8324 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8325 .unwrap();
8326 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8327
8328 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8329 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8330
8331 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8332 cx.assert_editor_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\n«abcˇ»");
8337
8338 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8339 .unwrap();
8340 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8341
8342 // Test selection direction should be preserved
8343 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8344
8345 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8346 .unwrap();
8347 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
8348}
8349
8350#[gpui::test]
8351async fn test_select_all_matches(cx: &mut TestAppContext) {
8352 init_test(cx, |_| {});
8353
8354 let mut cx = EditorTestContext::new(cx).await;
8355
8356 // Test caret-only selections
8357 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8358 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8359 .unwrap();
8360 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8361
8362 // Test left-to-right selections
8363 cx.set_state("abc\n«abcˇ»\nabc");
8364 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8365 .unwrap();
8366 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
8367
8368 // Test right-to-left selections
8369 cx.set_state("abc\n«ˇabc»\nabc");
8370 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8371 .unwrap();
8372 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
8373
8374 // Test selecting whitespace with caret selection
8375 cx.set_state("abc\nˇ abc\nabc");
8376 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8377 .unwrap();
8378 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8379
8380 // Test selecting whitespace with left-to-right selection
8381 cx.set_state("abc\n«ˇ »abc\nabc");
8382 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8383 .unwrap();
8384 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
8385
8386 // Test no matches with right-to-left selection
8387 cx.set_state("abc\n« ˇ»abc\nabc");
8388 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8389 .unwrap();
8390 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8391
8392 // Test with a single word and clip_at_line_ends=true (#29823)
8393 cx.set_state("aˇbc");
8394 cx.update_editor(|e, window, cx| {
8395 e.set_clip_at_line_ends(true, cx);
8396 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8397 e.set_clip_at_line_ends(false, cx);
8398 });
8399 cx.assert_editor_state("«abcˇ»");
8400}
8401
8402#[gpui::test]
8403async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
8404 init_test(cx, |_| {});
8405
8406 let mut cx = EditorTestContext::new(cx).await;
8407
8408 let large_body_1 = "\nd".repeat(200);
8409 let large_body_2 = "\ne".repeat(200);
8410
8411 cx.set_state(&format!(
8412 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
8413 ));
8414 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
8415 let scroll_position = editor.scroll_position(cx);
8416 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
8417 scroll_position
8418 });
8419
8420 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8421 .unwrap();
8422 cx.assert_editor_state(&format!(
8423 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
8424 ));
8425 let scroll_position_after_selection =
8426 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
8427 assert_eq!(
8428 initial_scroll_position, scroll_position_after_selection,
8429 "Scroll position should not change after selecting all matches"
8430 );
8431}
8432
8433#[gpui::test]
8434async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
8435 init_test(cx, |_| {});
8436
8437 let mut cx = EditorLspTestContext::new_rust(
8438 lsp::ServerCapabilities {
8439 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8440 ..Default::default()
8441 },
8442 cx,
8443 )
8444 .await;
8445
8446 cx.set_state(indoc! {"
8447 line 1
8448 line 2
8449 linˇe 3
8450 line 4
8451 line 5
8452 "});
8453
8454 // Make an edit
8455 cx.update_editor(|editor, window, cx| {
8456 editor.handle_input("X", window, cx);
8457 });
8458
8459 // Move cursor to a different position
8460 cx.update_editor(|editor, window, cx| {
8461 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8462 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
8463 });
8464 });
8465
8466 cx.assert_editor_state(indoc! {"
8467 line 1
8468 line 2
8469 linXe 3
8470 line 4
8471 liˇne 5
8472 "});
8473
8474 cx.lsp
8475 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8476 Ok(Some(vec![lsp::TextEdit::new(
8477 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8478 "PREFIX ".to_string(),
8479 )]))
8480 });
8481
8482 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
8483 .unwrap()
8484 .await
8485 .unwrap();
8486
8487 cx.assert_editor_state(indoc! {"
8488 PREFIX line 1
8489 line 2
8490 linXe 3
8491 line 4
8492 liˇne 5
8493 "});
8494
8495 // Undo formatting
8496 cx.update_editor(|editor, window, cx| {
8497 editor.undo(&Default::default(), window, cx);
8498 });
8499
8500 // Verify cursor moved back to position after edit
8501 cx.assert_editor_state(indoc! {"
8502 line 1
8503 line 2
8504 linXˇe 3
8505 line 4
8506 line 5
8507 "});
8508}
8509
8510#[gpui::test]
8511async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) {
8512 init_test(cx, |_| {});
8513
8514 let mut cx = EditorTestContext::new(cx).await;
8515
8516 let provider = cx.new(|_| FakeEditPredictionProvider::default());
8517 cx.update_editor(|editor, window, cx| {
8518 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
8519 });
8520
8521 cx.set_state(indoc! {"
8522 line 1
8523 line 2
8524 linˇe 3
8525 line 4
8526 line 5
8527 line 6
8528 line 7
8529 line 8
8530 line 9
8531 line 10
8532 "});
8533
8534 let snapshot = cx.buffer_snapshot();
8535 let edit_position = snapshot.anchor_after(Point::new(2, 4));
8536
8537 cx.update(|_, cx| {
8538 provider.update(cx, |provider, _| {
8539 provider.set_edit_prediction(Some(edit_prediction::EditPrediction::Local {
8540 id: None,
8541 edits: vec![(edit_position..edit_position, "X".into())],
8542 edit_preview: None,
8543 }))
8544 })
8545 });
8546
8547 cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
8548 cx.update_editor(|editor, window, cx| {
8549 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
8550 });
8551
8552 cx.assert_editor_state(indoc! {"
8553 line 1
8554 line 2
8555 lineXˇ 3
8556 line 4
8557 line 5
8558 line 6
8559 line 7
8560 line 8
8561 line 9
8562 line 10
8563 "});
8564
8565 cx.update_editor(|editor, window, cx| {
8566 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8567 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
8568 });
8569 });
8570
8571 cx.assert_editor_state(indoc! {"
8572 line 1
8573 line 2
8574 lineX 3
8575 line 4
8576 line 5
8577 line 6
8578 line 7
8579 line 8
8580 line 9
8581 liˇne 10
8582 "});
8583
8584 cx.update_editor(|editor, window, cx| {
8585 editor.undo(&Default::default(), window, cx);
8586 });
8587
8588 cx.assert_editor_state(indoc! {"
8589 line 1
8590 line 2
8591 lineˇ 3
8592 line 4
8593 line 5
8594 line 6
8595 line 7
8596 line 8
8597 line 9
8598 line 10
8599 "});
8600}
8601
8602#[gpui::test]
8603async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
8604 init_test(cx, |_| {});
8605
8606 let mut cx = EditorTestContext::new(cx).await;
8607 cx.set_state(
8608 r#"let foo = 2;
8609lˇet foo = 2;
8610let fooˇ = 2;
8611let foo = 2;
8612let foo = ˇ2;"#,
8613 );
8614
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 // noop for multiple selections with different contents
8626 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8627 .unwrap();
8628 cx.assert_editor_state(
8629 r#"let foo = 2;
8630«letˇ» foo = 2;
8631let «fooˇ» = 2;
8632let foo = 2;
8633let foo = «2ˇ»;"#,
8634 );
8635
8636 // Test last selection direction should be preserved
8637 cx.set_state(
8638 r#"let foo = 2;
8639let foo = 2;
8640let «fooˇ» = 2;
8641let «ˇfoo» = 2;
8642let foo = 2;"#,
8643 );
8644
8645 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8646 .unwrap();
8647 cx.assert_editor_state(
8648 r#"let foo = 2;
8649let foo = 2;
8650let «fooˇ» = 2;
8651let «ˇfoo» = 2;
8652let «ˇfoo» = 2;"#,
8653 );
8654}
8655
8656#[gpui::test]
8657async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
8658 init_test(cx, |_| {});
8659
8660 let mut cx =
8661 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
8662
8663 cx.assert_editor_state(indoc! {"
8664 ˇbbb
8665 ccc
8666
8667 bbb
8668 ccc
8669 "});
8670 cx.dispatch_action(SelectPrevious::default());
8671 cx.assert_editor_state(indoc! {"
8672 «bbbˇ»
8673 ccc
8674
8675 bbb
8676 ccc
8677 "});
8678 cx.dispatch_action(SelectPrevious::default());
8679 cx.assert_editor_state(indoc! {"
8680 «bbbˇ»
8681 ccc
8682
8683 «bbbˇ»
8684 ccc
8685 "});
8686}
8687
8688#[gpui::test]
8689async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
8690 init_test(cx, |_| {});
8691
8692 let mut cx = EditorTestContext::new(cx).await;
8693 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8694
8695 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8696 .unwrap();
8697 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8698
8699 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8700 .unwrap();
8701 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8702
8703 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8704 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8705
8706 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8707 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8708
8709 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8710 .unwrap();
8711 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
8712
8713 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8714 .unwrap();
8715 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8716}
8717
8718#[gpui::test]
8719async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
8720 init_test(cx, |_| {});
8721
8722 let mut cx = EditorTestContext::new(cx).await;
8723 cx.set_state("aˇ");
8724
8725 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8726 .unwrap();
8727 cx.assert_editor_state("«aˇ»");
8728 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8729 .unwrap();
8730 cx.assert_editor_state("«aˇ»");
8731}
8732
8733#[gpui::test]
8734async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
8735 init_test(cx, |_| {});
8736
8737 let mut cx = EditorTestContext::new(cx).await;
8738 cx.set_state(
8739 r#"let foo = 2;
8740lˇet foo = 2;
8741let fooˇ = 2;
8742let foo = 2;
8743let foo = ˇ2;"#,
8744 );
8745
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 // noop for multiple selections with different contents
8757 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8758 .unwrap();
8759 cx.assert_editor_state(
8760 r#"let foo = 2;
8761«letˇ» foo = 2;
8762let «fooˇ» = 2;
8763let foo = 2;
8764let foo = «2ˇ»;"#,
8765 );
8766}
8767
8768#[gpui::test]
8769async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
8770 init_test(cx, |_| {});
8771
8772 let mut cx = EditorTestContext::new(cx).await;
8773 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8774
8775 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8776 .unwrap();
8777 // selection direction is preserved
8778 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8779
8780 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8781 .unwrap();
8782 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8783
8784 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8785 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8786
8787 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8788 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8789
8790 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8791 .unwrap();
8792 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
8793
8794 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8795 .unwrap();
8796 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
8797}
8798
8799#[gpui::test]
8800async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
8801 init_test(cx, |_| {});
8802
8803 let language = Arc::new(Language::new(
8804 LanguageConfig::default(),
8805 Some(tree_sitter_rust::LANGUAGE.into()),
8806 ));
8807
8808 let text = r#"
8809 use mod1::mod2::{mod3, mod4};
8810
8811 fn fn_1(param1: bool, param2: &str) {
8812 let var1 = "text";
8813 }
8814 "#
8815 .unindent();
8816
8817 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8818 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8819 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8820
8821 editor
8822 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8823 .await;
8824
8825 editor.update_in(cx, |editor, window, cx| {
8826 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8827 s.select_display_ranges([
8828 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
8829 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
8830 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
8831 ]);
8832 });
8833 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8834 });
8835 editor.update(cx, |editor, cx| {
8836 assert_text_with_selections(
8837 editor,
8838 indoc! {r#"
8839 use mod1::mod2::{mod3, «mod4ˇ»};
8840
8841 fn fn_1«ˇ(param1: bool, param2: &str)» {
8842 let var1 = "«ˇtext»";
8843 }
8844 "#},
8845 cx,
8846 );
8847 });
8848
8849 editor.update_in(cx, |editor, window, cx| {
8850 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8851 });
8852 editor.update(cx, |editor, cx| {
8853 assert_text_with_selections(
8854 editor,
8855 indoc! {r#"
8856 use mod1::mod2::«{mod3, mod4}ˇ»;
8857
8858 «ˇfn fn_1(param1: bool, param2: &str) {
8859 let var1 = "text";
8860 }»
8861 "#},
8862 cx,
8863 );
8864 });
8865
8866 editor.update_in(cx, |editor, window, cx| {
8867 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8868 });
8869 assert_eq!(
8870 editor.update(cx, |editor, cx| editor
8871 .selections
8872 .display_ranges(&editor.display_snapshot(cx))),
8873 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8874 );
8875
8876 // Trying to expand the selected syntax node one more time has no effect.
8877 editor.update_in(cx, |editor, window, cx| {
8878 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8879 });
8880 assert_eq!(
8881 editor.update(cx, |editor, cx| editor
8882 .selections
8883 .display_ranges(&editor.display_snapshot(cx))),
8884 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8885 );
8886
8887 editor.update_in(cx, |editor, window, cx| {
8888 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8889 });
8890 editor.update(cx, |editor, cx| {
8891 assert_text_with_selections(
8892 editor,
8893 indoc! {r#"
8894 use mod1::mod2::«{mod3, mod4}ˇ»;
8895
8896 «ˇfn fn_1(param1: bool, param2: &str) {
8897 let var1 = "text";
8898 }»
8899 "#},
8900 cx,
8901 );
8902 });
8903
8904 editor.update_in(cx, |editor, window, cx| {
8905 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8906 });
8907 editor.update(cx, |editor, cx| {
8908 assert_text_with_selections(
8909 editor,
8910 indoc! {r#"
8911 use mod1::mod2::{mod3, «mod4ˇ»};
8912
8913 fn fn_1«ˇ(param1: bool, param2: &str)» {
8914 let var1 = "«ˇtext»";
8915 }
8916 "#},
8917 cx,
8918 );
8919 });
8920
8921 editor.update_in(cx, |editor, window, cx| {
8922 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8923 });
8924 editor.update(cx, |editor, cx| {
8925 assert_text_with_selections(
8926 editor,
8927 indoc! {r#"
8928 use mod1::mod2::{mod3, moˇd4};
8929
8930 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
8931 let var1 = "teˇxt";
8932 }
8933 "#},
8934 cx,
8935 );
8936 });
8937
8938 // Trying to shrink the selected syntax node one more time has no effect.
8939 editor.update_in(cx, |editor, window, cx| {
8940 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8941 });
8942 editor.update_in(cx, |editor, _, cx| {
8943 assert_text_with_selections(
8944 editor,
8945 indoc! {r#"
8946 use mod1::mod2::{mod3, moˇd4};
8947
8948 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
8949 let var1 = "teˇxt";
8950 }
8951 "#},
8952 cx,
8953 );
8954 });
8955
8956 // Ensure that we keep expanding the selection if the larger selection starts or ends within
8957 // a fold.
8958 editor.update_in(cx, |editor, window, cx| {
8959 editor.fold_creases(
8960 vec![
8961 Crease::simple(
8962 Point::new(0, 21)..Point::new(0, 24),
8963 FoldPlaceholder::test(),
8964 ),
8965 Crease::simple(
8966 Point::new(3, 20)..Point::new(3, 22),
8967 FoldPlaceholder::test(),
8968 ),
8969 ],
8970 true,
8971 window,
8972 cx,
8973 );
8974 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8975 });
8976 editor.update(cx, |editor, cx| {
8977 assert_text_with_selections(
8978 editor,
8979 indoc! {r#"
8980 use mod1::mod2::«{mod3, mod4}ˇ»;
8981
8982 fn fn_1«ˇ(param1: bool, param2: &str)» {
8983 let var1 = "«ˇtext»";
8984 }
8985 "#},
8986 cx,
8987 );
8988 });
8989}
8990
8991#[gpui::test]
8992async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
8993 init_test(cx, |_| {});
8994
8995 let language = Arc::new(Language::new(
8996 LanguageConfig::default(),
8997 Some(tree_sitter_rust::LANGUAGE.into()),
8998 ));
8999
9000 let text = "let a = 2;";
9001
9002 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9003 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9004 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9005
9006 editor
9007 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9008 .await;
9009
9010 // Test case 1: Cursor at end of word
9011 editor.update_in(cx, |editor, window, cx| {
9012 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9013 s.select_display_ranges([
9014 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
9015 ]);
9016 });
9017 });
9018 editor.update(cx, |editor, cx| {
9019 assert_text_with_selections(editor, "let aˇ = 2;", cx);
9020 });
9021 editor.update_in(cx, |editor, window, cx| {
9022 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9023 });
9024 editor.update(cx, |editor, cx| {
9025 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
9026 });
9027 editor.update_in(cx, |editor, window, cx| {
9028 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9029 });
9030 editor.update(cx, |editor, cx| {
9031 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
9032 });
9033
9034 // Test case 2: Cursor at end of statement
9035 editor.update_in(cx, |editor, window, cx| {
9036 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9037 s.select_display_ranges([
9038 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
9039 ]);
9040 });
9041 });
9042 editor.update(cx, |editor, cx| {
9043 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
9044 });
9045 editor.update_in(cx, |editor, window, cx| {
9046 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9047 });
9048 editor.update(cx, |editor, cx| {
9049 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
9050 });
9051}
9052
9053#[gpui::test]
9054async fn test_select_larger_syntax_node_for_cursor_at_symbol(cx: &mut TestAppContext) {
9055 init_test(cx, |_| {});
9056
9057 let language = Arc::new(Language::new(
9058 LanguageConfig {
9059 name: "JavaScript".into(),
9060 ..Default::default()
9061 },
9062 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
9063 ));
9064
9065 let text = r#"
9066 let a = {
9067 key: "value",
9068 };
9069 "#
9070 .unindent();
9071
9072 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9073 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9074 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9075
9076 editor
9077 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9078 .await;
9079
9080 // Test case 1: Cursor after '{'
9081 editor.update_in(cx, |editor, window, cx| {
9082 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9083 s.select_display_ranges([
9084 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 9)
9085 ]);
9086 });
9087 });
9088 editor.update(cx, |editor, cx| {
9089 assert_text_with_selections(
9090 editor,
9091 indoc! {r#"
9092 let a = {ˇ
9093 key: "value",
9094 };
9095 "#},
9096 cx,
9097 );
9098 });
9099 editor.update_in(cx, |editor, window, cx| {
9100 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9101 });
9102 editor.update(cx, |editor, cx| {
9103 assert_text_with_selections(
9104 editor,
9105 indoc! {r#"
9106 let a = «ˇ{
9107 key: "value",
9108 }»;
9109 "#},
9110 cx,
9111 );
9112 });
9113
9114 // Test case 2: Cursor after ':'
9115 editor.update_in(cx, |editor, window, cx| {
9116 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9117 s.select_display_ranges([
9118 DisplayPoint::new(DisplayRow(1), 8)..DisplayPoint::new(DisplayRow(1), 8)
9119 ]);
9120 });
9121 });
9122 editor.update(cx, |editor, cx| {
9123 assert_text_with_selections(
9124 editor,
9125 indoc! {r#"
9126 let a = {
9127 key:ˇ "value",
9128 };
9129 "#},
9130 cx,
9131 );
9132 });
9133 editor.update_in(cx, |editor, window, cx| {
9134 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9135 });
9136 editor.update(cx, |editor, cx| {
9137 assert_text_with_selections(
9138 editor,
9139 indoc! {r#"
9140 let a = {
9141 «ˇkey: "value"»,
9142 };
9143 "#},
9144 cx,
9145 );
9146 });
9147 editor.update_in(cx, |editor, window, cx| {
9148 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9149 });
9150 editor.update(cx, |editor, cx| {
9151 assert_text_with_selections(
9152 editor,
9153 indoc! {r#"
9154 let a = «ˇ{
9155 key: "value",
9156 }»;
9157 "#},
9158 cx,
9159 );
9160 });
9161
9162 // Test case 3: Cursor after ','
9163 editor.update_in(cx, |editor, window, cx| {
9164 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9165 s.select_display_ranges([
9166 DisplayPoint::new(DisplayRow(1), 17)..DisplayPoint::new(DisplayRow(1), 17)
9167 ]);
9168 });
9169 });
9170 editor.update(cx, |editor, cx| {
9171 assert_text_with_selections(
9172 editor,
9173 indoc! {r#"
9174 let a = {
9175 key: "value",ˇ
9176 };
9177 "#},
9178 cx,
9179 );
9180 });
9181 editor.update_in(cx, |editor, window, cx| {
9182 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9183 });
9184 editor.update(cx, |editor, cx| {
9185 assert_text_with_selections(
9186 editor,
9187 indoc! {r#"
9188 let a = «ˇ{
9189 key: "value",
9190 }»;
9191 "#},
9192 cx,
9193 );
9194 });
9195
9196 // Test case 4: Cursor after ';'
9197 editor.update_in(cx, |editor, window, cx| {
9198 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9199 s.select_display_ranges([
9200 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)
9201 ]);
9202 });
9203 });
9204 editor.update(cx, |editor, cx| {
9205 assert_text_with_selections(
9206 editor,
9207 indoc! {r#"
9208 let a = {
9209 key: "value",
9210 };ˇ
9211 "#},
9212 cx,
9213 );
9214 });
9215 editor.update_in(cx, |editor, window, cx| {
9216 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9217 });
9218 editor.update(cx, |editor, cx| {
9219 assert_text_with_selections(
9220 editor,
9221 indoc! {r#"
9222 «ˇlet a = {
9223 key: "value",
9224 };
9225 »"#},
9226 cx,
9227 );
9228 });
9229}
9230
9231#[gpui::test]
9232async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
9233 init_test(cx, |_| {});
9234
9235 let language = Arc::new(Language::new(
9236 LanguageConfig::default(),
9237 Some(tree_sitter_rust::LANGUAGE.into()),
9238 ));
9239
9240 let text = r#"
9241 use mod1::mod2::{mod3, mod4};
9242
9243 fn fn_1(param1: bool, param2: &str) {
9244 let var1 = "hello world";
9245 }
9246 "#
9247 .unindent();
9248
9249 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9250 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9251 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9252
9253 editor
9254 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9255 .await;
9256
9257 // Test 1: Cursor on a letter of a string word
9258 editor.update_in(cx, |editor, window, cx| {
9259 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9260 s.select_display_ranges([
9261 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
9262 ]);
9263 });
9264 });
9265 editor.update_in(cx, |editor, window, cx| {
9266 assert_text_with_selections(
9267 editor,
9268 indoc! {r#"
9269 use mod1::mod2::{mod3, mod4};
9270
9271 fn fn_1(param1: bool, param2: &str) {
9272 let var1 = "hˇello world";
9273 }
9274 "#},
9275 cx,
9276 );
9277 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9278 assert_text_with_selections(
9279 editor,
9280 indoc! {r#"
9281 use mod1::mod2::{mod3, mod4};
9282
9283 fn fn_1(param1: bool, param2: &str) {
9284 let var1 = "«ˇhello» world";
9285 }
9286 "#},
9287 cx,
9288 );
9289 });
9290
9291 // Test 2: Partial selection within a word
9292 editor.update_in(cx, |editor, window, cx| {
9293 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9294 s.select_display_ranges([
9295 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
9296 ]);
9297 });
9298 });
9299 editor.update_in(cx, |editor, window, cx| {
9300 assert_text_with_selections(
9301 editor,
9302 indoc! {r#"
9303 use mod1::mod2::{mod3, mod4};
9304
9305 fn fn_1(param1: bool, param2: &str) {
9306 let var1 = "h«elˇ»lo world";
9307 }
9308 "#},
9309 cx,
9310 );
9311 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9312 assert_text_with_selections(
9313 editor,
9314 indoc! {r#"
9315 use mod1::mod2::{mod3, mod4};
9316
9317 fn fn_1(param1: bool, param2: &str) {
9318 let var1 = "«ˇhello» world";
9319 }
9320 "#},
9321 cx,
9322 );
9323 });
9324
9325 // Test 3: Complete word already selected
9326 editor.update_in(cx, |editor, window, cx| {
9327 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9328 s.select_display_ranges([
9329 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
9330 ]);
9331 });
9332 });
9333 editor.update_in(cx, |editor, window, cx| {
9334 assert_text_with_selections(
9335 editor,
9336 indoc! {r#"
9337 use mod1::mod2::{mod3, mod4};
9338
9339 fn fn_1(param1: bool, param2: &str) {
9340 let var1 = "«helloˇ» world";
9341 }
9342 "#},
9343 cx,
9344 );
9345 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9346 assert_text_with_selections(
9347 editor,
9348 indoc! {r#"
9349 use mod1::mod2::{mod3, mod4};
9350
9351 fn fn_1(param1: bool, param2: &str) {
9352 let var1 = "«hello worldˇ»";
9353 }
9354 "#},
9355 cx,
9356 );
9357 });
9358
9359 // Test 4: Selection spanning across words
9360 editor.update_in(cx, |editor, window, cx| {
9361 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9362 s.select_display_ranges([
9363 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
9364 ]);
9365 });
9366 });
9367 editor.update_in(cx, |editor, window, cx| {
9368 assert_text_with_selections(
9369 editor,
9370 indoc! {r#"
9371 use mod1::mod2::{mod3, mod4};
9372
9373 fn fn_1(param1: bool, param2: &str) {
9374 let var1 = "hel«lo woˇ»rld";
9375 }
9376 "#},
9377 cx,
9378 );
9379 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9380 assert_text_with_selections(
9381 editor,
9382 indoc! {r#"
9383 use mod1::mod2::{mod3, mod4};
9384
9385 fn fn_1(param1: bool, param2: &str) {
9386 let var1 = "«ˇhello world»";
9387 }
9388 "#},
9389 cx,
9390 );
9391 });
9392
9393 // Test 5: Expansion beyond string
9394 editor.update_in(cx, |editor, window, cx| {
9395 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9396 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9397 assert_text_with_selections(
9398 editor,
9399 indoc! {r#"
9400 use mod1::mod2::{mod3, mod4};
9401
9402 fn fn_1(param1: bool, param2: &str) {
9403 «ˇlet var1 = "hello world";»
9404 }
9405 "#},
9406 cx,
9407 );
9408 });
9409}
9410
9411#[gpui::test]
9412async fn test_unwrap_syntax_nodes(cx: &mut gpui::TestAppContext) {
9413 init_test(cx, |_| {});
9414
9415 let mut cx = EditorTestContext::new(cx).await;
9416
9417 let language = Arc::new(Language::new(
9418 LanguageConfig::default(),
9419 Some(tree_sitter_rust::LANGUAGE.into()),
9420 ));
9421
9422 cx.update_buffer(|buffer, cx| {
9423 buffer.set_language(Some(language), cx);
9424 });
9425
9426 cx.set_state(indoc! { r#"use mod1::{mod2::{«mod3ˇ», mod4}, mod5::{mod6, «mod7ˇ»}};"# });
9427 cx.update_editor(|editor, window, cx| {
9428 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9429 });
9430
9431 cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# });
9432
9433 cx.set_state(indoc! { r#"fn a() {
9434 // what
9435 // a
9436 // ˇlong
9437 // method
9438 // I
9439 // sure
9440 // hope
9441 // it
9442 // works
9443 }"# });
9444
9445 let buffer = cx.update_multibuffer(|multibuffer, _| multibuffer.as_singleton().unwrap());
9446 let multi_buffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
9447 cx.update(|_, cx| {
9448 multi_buffer.update(cx, |multi_buffer, cx| {
9449 multi_buffer.set_excerpts_for_path(
9450 PathKey::for_buffer(&buffer, cx),
9451 buffer,
9452 [Point::new(1, 0)..Point::new(1, 0)],
9453 3,
9454 cx,
9455 );
9456 });
9457 });
9458
9459 let editor2 = cx.new_window_entity(|window, cx| {
9460 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
9461 });
9462
9463 let mut cx = EditorTestContext::for_editor_in(editor2, &mut cx).await;
9464 cx.update_editor(|editor, window, cx| {
9465 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
9466 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)]);
9467 })
9468 });
9469
9470 cx.assert_editor_state(indoc! { "
9471 fn a() {
9472 // what
9473 // a
9474 ˇ // long
9475 // method"});
9476
9477 cx.update_editor(|editor, window, cx| {
9478 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9479 });
9480
9481 // Although we could potentially make the action work when the syntax node
9482 // is half-hidden, it seems a bit dangerous as you can't easily tell what it
9483 // did. Maybe we could also expand the excerpt to contain the range?
9484 cx.assert_editor_state(indoc! { "
9485 fn a() {
9486 // what
9487 // a
9488 ˇ // long
9489 // method"});
9490}
9491
9492#[gpui::test]
9493async fn test_fold_function_bodies(cx: &mut TestAppContext) {
9494 init_test(cx, |_| {});
9495
9496 let base_text = r#"
9497 impl A {
9498 // this is an uncommitted comment
9499
9500 fn b() {
9501 c();
9502 }
9503
9504 // this is another uncommitted comment
9505
9506 fn d() {
9507 // e
9508 // f
9509 }
9510 }
9511
9512 fn g() {
9513 // h
9514 }
9515 "#
9516 .unindent();
9517
9518 let text = r#"
9519 ˇimpl A {
9520
9521 fn b() {
9522 c();
9523 }
9524
9525 fn d() {
9526 // e
9527 // f
9528 }
9529 }
9530
9531 fn g() {
9532 // h
9533 }
9534 "#
9535 .unindent();
9536
9537 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9538 cx.set_state(&text);
9539 cx.set_head_text(&base_text);
9540 cx.update_editor(|editor, window, cx| {
9541 editor.expand_all_diff_hunks(&Default::default(), window, cx);
9542 });
9543
9544 cx.assert_state_with_diff(
9545 "
9546 ˇimpl A {
9547 - // this is an uncommitted comment
9548
9549 fn b() {
9550 c();
9551 }
9552
9553 - // this is another uncommitted comment
9554 -
9555 fn d() {
9556 // e
9557 // f
9558 }
9559 }
9560
9561 fn g() {
9562 // h
9563 }
9564 "
9565 .unindent(),
9566 );
9567
9568 let expected_display_text = "
9569 impl A {
9570 // this is an uncommitted comment
9571
9572 fn b() {
9573 ⋯
9574 }
9575
9576 // this is another uncommitted comment
9577
9578 fn d() {
9579 ⋯
9580 }
9581 }
9582
9583 fn g() {
9584 ⋯
9585 }
9586 "
9587 .unindent();
9588
9589 cx.update_editor(|editor, window, cx| {
9590 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
9591 assert_eq!(editor.display_text(cx), expected_display_text);
9592 });
9593}
9594
9595#[gpui::test]
9596async fn test_autoindent(cx: &mut TestAppContext) {
9597 init_test(cx, |_| {});
9598
9599 let language = Arc::new(
9600 Language::new(
9601 LanguageConfig {
9602 brackets: BracketPairConfig {
9603 pairs: vec![
9604 BracketPair {
9605 start: "{".to_string(),
9606 end: "}".to_string(),
9607 close: false,
9608 surround: false,
9609 newline: true,
9610 },
9611 BracketPair {
9612 start: "(".to_string(),
9613 end: ")".to_string(),
9614 close: false,
9615 surround: false,
9616 newline: true,
9617 },
9618 ],
9619 ..Default::default()
9620 },
9621 ..Default::default()
9622 },
9623 Some(tree_sitter_rust::LANGUAGE.into()),
9624 )
9625 .with_indents_query(
9626 r#"
9627 (_ "(" ")" @end) @indent
9628 (_ "{" "}" @end) @indent
9629 "#,
9630 )
9631 .unwrap(),
9632 );
9633
9634 let text = "fn a() {}";
9635
9636 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9637 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9638 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9639 editor
9640 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9641 .await;
9642
9643 editor.update_in(cx, |editor, window, cx| {
9644 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9645 s.select_ranges([5..5, 8..8, 9..9])
9646 });
9647 editor.newline(&Newline, window, cx);
9648 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
9649 assert_eq!(
9650 editor.selections.ranges(&editor.display_snapshot(cx)),
9651 &[
9652 Point::new(1, 4)..Point::new(1, 4),
9653 Point::new(3, 4)..Point::new(3, 4),
9654 Point::new(5, 0)..Point::new(5, 0)
9655 ]
9656 );
9657 });
9658}
9659
9660#[gpui::test]
9661async fn test_autoindent_disabled(cx: &mut TestAppContext) {
9662 init_test(cx, |settings| settings.defaults.auto_indent = Some(false));
9663
9664 let language = Arc::new(
9665 Language::new(
9666 LanguageConfig {
9667 brackets: BracketPairConfig {
9668 pairs: vec![
9669 BracketPair {
9670 start: "{".to_string(),
9671 end: "}".to_string(),
9672 close: false,
9673 surround: false,
9674 newline: true,
9675 },
9676 BracketPair {
9677 start: "(".to_string(),
9678 end: ")".to_string(),
9679 close: false,
9680 surround: false,
9681 newline: true,
9682 },
9683 ],
9684 ..Default::default()
9685 },
9686 ..Default::default()
9687 },
9688 Some(tree_sitter_rust::LANGUAGE.into()),
9689 )
9690 .with_indents_query(
9691 r#"
9692 (_ "(" ")" @end) @indent
9693 (_ "{" "}" @end) @indent
9694 "#,
9695 )
9696 .unwrap(),
9697 );
9698
9699 let text = "fn a() {}";
9700
9701 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9702 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9703 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9704 editor
9705 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9706 .await;
9707
9708 editor.update_in(cx, |editor, window, cx| {
9709 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9710 s.select_ranges([5..5, 8..8, 9..9])
9711 });
9712 editor.newline(&Newline, window, cx);
9713 assert_eq!(
9714 editor.text(cx),
9715 indoc!(
9716 "
9717 fn a(
9718
9719 ) {
9720
9721 }
9722 "
9723 )
9724 );
9725 assert_eq!(
9726 editor.selections.ranges(&editor.display_snapshot(cx)),
9727 &[
9728 Point::new(1, 0)..Point::new(1, 0),
9729 Point::new(3, 0)..Point::new(3, 0),
9730 Point::new(5, 0)..Point::new(5, 0)
9731 ]
9732 );
9733 });
9734}
9735
9736#[gpui::test]
9737async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) {
9738 init_test(cx, |settings| {
9739 settings.defaults.auto_indent = Some(true);
9740 settings.languages.0.insert(
9741 "python".into(),
9742 LanguageSettingsContent {
9743 auto_indent: Some(false),
9744 ..Default::default()
9745 },
9746 );
9747 });
9748
9749 let mut cx = EditorTestContext::new(cx).await;
9750
9751 let injected_language = Arc::new(
9752 Language::new(
9753 LanguageConfig {
9754 brackets: BracketPairConfig {
9755 pairs: vec![
9756 BracketPair {
9757 start: "{".to_string(),
9758 end: "}".to_string(),
9759 close: false,
9760 surround: false,
9761 newline: true,
9762 },
9763 BracketPair {
9764 start: "(".to_string(),
9765 end: ")".to_string(),
9766 close: true,
9767 surround: false,
9768 newline: true,
9769 },
9770 ],
9771 ..Default::default()
9772 },
9773 name: "python".into(),
9774 ..Default::default()
9775 },
9776 Some(tree_sitter_python::LANGUAGE.into()),
9777 )
9778 .with_indents_query(
9779 r#"
9780 (_ "(" ")" @end) @indent
9781 (_ "{" "}" @end) @indent
9782 "#,
9783 )
9784 .unwrap(),
9785 );
9786
9787 let language = Arc::new(
9788 Language::new(
9789 LanguageConfig {
9790 brackets: BracketPairConfig {
9791 pairs: vec![
9792 BracketPair {
9793 start: "{".to_string(),
9794 end: "}".to_string(),
9795 close: false,
9796 surround: false,
9797 newline: true,
9798 },
9799 BracketPair {
9800 start: "(".to_string(),
9801 end: ")".to_string(),
9802 close: true,
9803 surround: false,
9804 newline: true,
9805 },
9806 ],
9807 ..Default::default()
9808 },
9809 name: LanguageName::new("rust"),
9810 ..Default::default()
9811 },
9812 Some(tree_sitter_rust::LANGUAGE.into()),
9813 )
9814 .with_indents_query(
9815 r#"
9816 (_ "(" ")" @end) @indent
9817 (_ "{" "}" @end) @indent
9818 "#,
9819 )
9820 .unwrap()
9821 .with_injection_query(
9822 r#"
9823 (macro_invocation
9824 macro: (identifier) @_macro_name
9825 (token_tree) @injection.content
9826 (#set! injection.language "python"))
9827 "#,
9828 )
9829 .unwrap(),
9830 );
9831
9832 cx.language_registry().add(injected_language);
9833 cx.language_registry().add(language.clone());
9834
9835 cx.update_buffer(|buffer, cx| {
9836 buffer.set_language(Some(language), cx);
9837 });
9838
9839 cx.set_state(r#"struct A {ˇ}"#);
9840
9841 cx.update_editor(|editor, window, cx| {
9842 editor.newline(&Default::default(), window, cx);
9843 });
9844
9845 cx.assert_editor_state(indoc!(
9846 "struct A {
9847 ˇ
9848 }"
9849 ));
9850
9851 cx.set_state(r#"select_biased!(ˇ)"#);
9852
9853 cx.update_editor(|editor, window, cx| {
9854 editor.newline(&Default::default(), window, cx);
9855 editor.handle_input("def ", window, cx);
9856 editor.handle_input("(", window, cx);
9857 editor.newline(&Default::default(), window, cx);
9858 editor.handle_input("a", window, cx);
9859 });
9860
9861 cx.assert_editor_state(indoc!(
9862 "select_biased!(
9863 def (
9864 aˇ
9865 )
9866 )"
9867 ));
9868}
9869
9870#[gpui::test]
9871async fn test_autoindent_selections(cx: &mut TestAppContext) {
9872 init_test(cx, |_| {});
9873
9874 {
9875 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9876 cx.set_state(indoc! {"
9877 impl A {
9878
9879 fn b() {}
9880
9881 «fn c() {
9882
9883 }ˇ»
9884 }
9885 "});
9886
9887 cx.update_editor(|editor, window, cx| {
9888 editor.autoindent(&Default::default(), window, cx);
9889 });
9890
9891 cx.assert_editor_state(indoc! {"
9892 impl A {
9893
9894 fn b() {}
9895
9896 «fn c() {
9897
9898 }ˇ»
9899 }
9900 "});
9901 }
9902
9903 {
9904 let mut cx = EditorTestContext::new_multibuffer(
9905 cx,
9906 [indoc! { "
9907 impl A {
9908 «
9909 // a
9910 fn b(){}
9911 »
9912 «
9913 }
9914 fn c(){}
9915 »
9916 "}],
9917 );
9918
9919 let buffer = cx.update_editor(|editor, _, cx| {
9920 let buffer = editor.buffer().update(cx, |buffer, _| {
9921 buffer.all_buffers().iter().next().unwrap().clone()
9922 });
9923 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
9924 buffer
9925 });
9926
9927 cx.run_until_parked();
9928 cx.update_editor(|editor, window, cx| {
9929 editor.select_all(&Default::default(), window, cx);
9930 editor.autoindent(&Default::default(), window, cx)
9931 });
9932 cx.run_until_parked();
9933
9934 cx.update(|_, cx| {
9935 assert_eq!(
9936 buffer.read(cx).text(),
9937 indoc! { "
9938 impl A {
9939
9940 // a
9941 fn b(){}
9942
9943
9944 }
9945 fn c(){}
9946
9947 " }
9948 )
9949 });
9950 }
9951}
9952
9953#[gpui::test]
9954async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
9955 init_test(cx, |_| {});
9956
9957 let mut cx = EditorTestContext::new(cx).await;
9958
9959 let language = Arc::new(Language::new(
9960 LanguageConfig {
9961 brackets: BracketPairConfig {
9962 pairs: vec![
9963 BracketPair {
9964 start: "{".to_string(),
9965 end: "}".to_string(),
9966 close: true,
9967 surround: true,
9968 newline: true,
9969 },
9970 BracketPair {
9971 start: "(".to_string(),
9972 end: ")".to_string(),
9973 close: true,
9974 surround: true,
9975 newline: true,
9976 },
9977 BracketPair {
9978 start: "/*".to_string(),
9979 end: " */".to_string(),
9980 close: true,
9981 surround: true,
9982 newline: true,
9983 },
9984 BracketPair {
9985 start: "[".to_string(),
9986 end: "]".to_string(),
9987 close: false,
9988 surround: false,
9989 newline: true,
9990 },
9991 BracketPair {
9992 start: "\"".to_string(),
9993 end: "\"".to_string(),
9994 close: true,
9995 surround: true,
9996 newline: false,
9997 },
9998 BracketPair {
9999 start: "<".to_string(),
10000 end: ">".to_string(),
10001 close: false,
10002 surround: true,
10003 newline: true,
10004 },
10005 ],
10006 ..Default::default()
10007 },
10008 autoclose_before: "})]".to_string(),
10009 ..Default::default()
10010 },
10011 Some(tree_sitter_rust::LANGUAGE.into()),
10012 ));
10013
10014 cx.language_registry().add(language.clone());
10015 cx.update_buffer(|buffer, cx| {
10016 buffer.set_language(Some(language), cx);
10017 });
10018
10019 cx.set_state(
10020 &r#"
10021 🏀ˇ
10022 εˇ
10023 ❤️ˇ
10024 "#
10025 .unindent(),
10026 );
10027
10028 // autoclose multiple nested brackets at multiple cursors
10029 cx.update_editor(|editor, window, cx| {
10030 editor.handle_input("{", window, cx);
10031 editor.handle_input("{", window, cx);
10032 editor.handle_input("{", window, cx);
10033 });
10034 cx.assert_editor_state(
10035 &"
10036 🏀{{{ˇ}}}
10037 ε{{{ˇ}}}
10038 ❤️{{{ˇ}}}
10039 "
10040 .unindent(),
10041 );
10042
10043 // insert a different closing bracket
10044 cx.update_editor(|editor, window, cx| {
10045 editor.handle_input(")", window, cx);
10046 });
10047 cx.assert_editor_state(
10048 &"
10049 🏀{{{)ˇ}}}
10050 ε{{{)ˇ}}}
10051 ❤️{{{)ˇ}}}
10052 "
10053 .unindent(),
10054 );
10055
10056 // skip over the auto-closed brackets when typing a closing bracket
10057 cx.update_editor(|editor, window, cx| {
10058 editor.move_right(&MoveRight, window, cx);
10059 editor.handle_input("}", window, cx);
10060 editor.handle_input("}", window, cx);
10061 editor.handle_input("}", window, cx);
10062 });
10063 cx.assert_editor_state(
10064 &"
10065 🏀{{{)}}}}ˇ
10066 ε{{{)}}}}ˇ
10067 ❤️{{{)}}}}ˇ
10068 "
10069 .unindent(),
10070 );
10071
10072 // autoclose multi-character pairs
10073 cx.set_state(
10074 &"
10075 ˇ
10076 ˇ
10077 "
10078 .unindent(),
10079 );
10080 cx.update_editor(|editor, window, cx| {
10081 editor.handle_input("/", window, cx);
10082 editor.handle_input("*", window, cx);
10083 });
10084 cx.assert_editor_state(
10085 &"
10086 /*ˇ */
10087 /*ˇ */
10088 "
10089 .unindent(),
10090 );
10091
10092 // one cursor autocloses a multi-character pair, one cursor
10093 // does not autoclose.
10094 cx.set_state(
10095 &"
10096 /ˇ
10097 ˇ
10098 "
10099 .unindent(),
10100 );
10101 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
10102 cx.assert_editor_state(
10103 &"
10104 /*ˇ */
10105 *ˇ
10106 "
10107 .unindent(),
10108 );
10109
10110 // Don't autoclose if the next character isn't whitespace and isn't
10111 // listed in the language's "autoclose_before" section.
10112 cx.set_state("ˇa b");
10113 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10114 cx.assert_editor_state("{ˇa b");
10115
10116 // Don't autoclose if `close` is false for the bracket pair
10117 cx.set_state("ˇ");
10118 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
10119 cx.assert_editor_state("[ˇ");
10120
10121 // Surround with brackets if text is selected
10122 cx.set_state("«aˇ» b");
10123 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10124 cx.assert_editor_state("{«aˇ»} b");
10125
10126 // Autoclose when not immediately after a word character
10127 cx.set_state("a ˇ");
10128 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10129 cx.assert_editor_state("a \"ˇ\"");
10130
10131 // Autoclose pair where the start and end characters are the same
10132 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10133 cx.assert_editor_state("a \"\"ˇ");
10134
10135 // Don't autoclose when immediately after a word character
10136 cx.set_state("aˇ");
10137 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10138 cx.assert_editor_state("a\"ˇ");
10139
10140 // Do autoclose when after a non-word character
10141 cx.set_state("{ˇ");
10142 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10143 cx.assert_editor_state("{\"ˇ\"");
10144
10145 // Non identical pairs autoclose regardless of preceding character
10146 cx.set_state("aˇ");
10147 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10148 cx.assert_editor_state("a{ˇ}");
10149
10150 // Don't autoclose pair if autoclose is disabled
10151 cx.set_state("ˇ");
10152 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
10153 cx.assert_editor_state("<ˇ");
10154
10155 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
10156 cx.set_state("«aˇ» b");
10157 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
10158 cx.assert_editor_state("<«aˇ»> b");
10159}
10160
10161#[gpui::test]
10162async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
10163 init_test(cx, |settings| {
10164 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10165 });
10166
10167 let mut cx = EditorTestContext::new(cx).await;
10168
10169 let language = Arc::new(Language::new(
10170 LanguageConfig {
10171 brackets: BracketPairConfig {
10172 pairs: vec![
10173 BracketPair {
10174 start: "{".to_string(),
10175 end: "}".to_string(),
10176 close: true,
10177 surround: true,
10178 newline: true,
10179 },
10180 BracketPair {
10181 start: "(".to_string(),
10182 end: ")".to_string(),
10183 close: true,
10184 surround: true,
10185 newline: true,
10186 },
10187 BracketPair {
10188 start: "[".to_string(),
10189 end: "]".to_string(),
10190 close: false,
10191 surround: false,
10192 newline: true,
10193 },
10194 ],
10195 ..Default::default()
10196 },
10197 autoclose_before: "})]".to_string(),
10198 ..Default::default()
10199 },
10200 Some(tree_sitter_rust::LANGUAGE.into()),
10201 ));
10202
10203 cx.language_registry().add(language.clone());
10204 cx.update_buffer(|buffer, cx| {
10205 buffer.set_language(Some(language), cx);
10206 });
10207
10208 cx.set_state(
10209 &"
10210 ˇ
10211 ˇ
10212 ˇ
10213 "
10214 .unindent(),
10215 );
10216
10217 // ensure only matching closing brackets are skipped over
10218 cx.update_editor(|editor, window, cx| {
10219 editor.handle_input("}", window, cx);
10220 editor.move_left(&MoveLeft, window, cx);
10221 editor.handle_input(")", window, cx);
10222 editor.move_left(&MoveLeft, window, cx);
10223 });
10224 cx.assert_editor_state(
10225 &"
10226 ˇ)}
10227 ˇ)}
10228 ˇ)}
10229 "
10230 .unindent(),
10231 );
10232
10233 // skip-over closing brackets at multiple cursors
10234 cx.update_editor(|editor, window, cx| {
10235 editor.handle_input(")", window, cx);
10236 editor.handle_input("}", window, cx);
10237 });
10238 cx.assert_editor_state(
10239 &"
10240 )}ˇ
10241 )}ˇ
10242 )}ˇ
10243 "
10244 .unindent(),
10245 );
10246
10247 // ignore non-close brackets
10248 cx.update_editor(|editor, window, cx| {
10249 editor.handle_input("]", window, cx);
10250 editor.move_left(&MoveLeft, window, cx);
10251 editor.handle_input("]", window, cx);
10252 });
10253 cx.assert_editor_state(
10254 &"
10255 )}]ˇ]
10256 )}]ˇ]
10257 )}]ˇ]
10258 "
10259 .unindent(),
10260 );
10261}
10262
10263#[gpui::test]
10264async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
10265 init_test(cx, |_| {});
10266
10267 let mut cx = EditorTestContext::new(cx).await;
10268
10269 let html_language = Arc::new(
10270 Language::new(
10271 LanguageConfig {
10272 name: "HTML".into(),
10273 brackets: BracketPairConfig {
10274 pairs: vec![
10275 BracketPair {
10276 start: "<".into(),
10277 end: ">".into(),
10278 close: true,
10279 ..Default::default()
10280 },
10281 BracketPair {
10282 start: "{".into(),
10283 end: "}".into(),
10284 close: true,
10285 ..Default::default()
10286 },
10287 BracketPair {
10288 start: "(".into(),
10289 end: ")".into(),
10290 close: true,
10291 ..Default::default()
10292 },
10293 ],
10294 ..Default::default()
10295 },
10296 autoclose_before: "})]>".into(),
10297 ..Default::default()
10298 },
10299 Some(tree_sitter_html::LANGUAGE.into()),
10300 )
10301 .with_injection_query(
10302 r#"
10303 (script_element
10304 (raw_text) @injection.content
10305 (#set! injection.language "javascript"))
10306 "#,
10307 )
10308 .unwrap(),
10309 );
10310
10311 let javascript_language = Arc::new(Language::new(
10312 LanguageConfig {
10313 name: "JavaScript".into(),
10314 brackets: BracketPairConfig {
10315 pairs: vec![
10316 BracketPair {
10317 start: "/*".into(),
10318 end: " */".into(),
10319 close: true,
10320 ..Default::default()
10321 },
10322 BracketPair {
10323 start: "{".into(),
10324 end: "}".into(),
10325 close: true,
10326 ..Default::default()
10327 },
10328 BracketPair {
10329 start: "(".into(),
10330 end: ")".into(),
10331 close: true,
10332 ..Default::default()
10333 },
10334 ],
10335 ..Default::default()
10336 },
10337 autoclose_before: "})]>".into(),
10338 ..Default::default()
10339 },
10340 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
10341 ));
10342
10343 cx.language_registry().add(html_language.clone());
10344 cx.language_registry().add(javascript_language);
10345 cx.executor().run_until_parked();
10346
10347 cx.update_buffer(|buffer, cx| {
10348 buffer.set_language(Some(html_language), cx);
10349 });
10350
10351 cx.set_state(
10352 &r#"
10353 <body>ˇ
10354 <script>
10355 var x = 1;ˇ
10356 </script>
10357 </body>ˇ
10358 "#
10359 .unindent(),
10360 );
10361
10362 // Precondition: different languages are active at different locations.
10363 cx.update_editor(|editor, window, cx| {
10364 let snapshot = editor.snapshot(window, cx);
10365 let cursors = editor
10366 .selections
10367 .ranges::<usize>(&editor.display_snapshot(cx));
10368 let languages = cursors
10369 .iter()
10370 .map(|c| snapshot.language_at(c.start).unwrap().name())
10371 .collect::<Vec<_>>();
10372 assert_eq!(
10373 languages,
10374 &["HTML".into(), "JavaScript".into(), "HTML".into()]
10375 );
10376 });
10377
10378 // Angle brackets autoclose in HTML, but not JavaScript.
10379 cx.update_editor(|editor, window, cx| {
10380 editor.handle_input("<", window, cx);
10381 editor.handle_input("a", window, cx);
10382 });
10383 cx.assert_editor_state(
10384 &r#"
10385 <body><aˇ>
10386 <script>
10387 var x = 1;<aˇ
10388 </script>
10389 </body><aˇ>
10390 "#
10391 .unindent(),
10392 );
10393
10394 // Curly braces and parens autoclose in both HTML and JavaScript.
10395 cx.update_editor(|editor, window, cx| {
10396 editor.handle_input(" b=", window, cx);
10397 editor.handle_input("{", window, cx);
10398 editor.handle_input("c", window, cx);
10399 editor.handle_input("(", window, cx);
10400 });
10401 cx.assert_editor_state(
10402 &r#"
10403 <body><a b={c(ˇ)}>
10404 <script>
10405 var x = 1;<a b={c(ˇ)}
10406 </script>
10407 </body><a b={c(ˇ)}>
10408 "#
10409 .unindent(),
10410 );
10411
10412 // Brackets that were already autoclosed are skipped.
10413 cx.update_editor(|editor, window, cx| {
10414 editor.handle_input(")", window, cx);
10415 editor.handle_input("d", window, cx);
10416 editor.handle_input("}", window, cx);
10417 });
10418 cx.assert_editor_state(
10419 &r#"
10420 <body><a b={c()d}ˇ>
10421 <script>
10422 var x = 1;<a b={c()d}ˇ
10423 </script>
10424 </body><a b={c()d}ˇ>
10425 "#
10426 .unindent(),
10427 );
10428 cx.update_editor(|editor, window, cx| {
10429 editor.handle_input(">", window, cx);
10430 });
10431 cx.assert_editor_state(
10432 &r#"
10433 <body><a b={c()d}>ˇ
10434 <script>
10435 var x = 1;<a b={c()d}>ˇ
10436 </script>
10437 </body><a b={c()d}>ˇ
10438 "#
10439 .unindent(),
10440 );
10441
10442 // Reset
10443 cx.set_state(
10444 &r#"
10445 <body>ˇ
10446 <script>
10447 var x = 1;ˇ
10448 </script>
10449 </body>ˇ
10450 "#
10451 .unindent(),
10452 );
10453
10454 cx.update_editor(|editor, window, cx| {
10455 editor.handle_input("<", 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 // When backspacing, the closing angle brackets are removed.
10469 cx.update_editor(|editor, window, cx| {
10470 editor.backspace(&Backspace, window, cx);
10471 });
10472 cx.assert_editor_state(
10473 &r#"
10474 <body>ˇ
10475 <script>
10476 var x = 1;ˇ
10477 </script>
10478 </body>ˇ
10479 "#
10480 .unindent(),
10481 );
10482
10483 // Block comments autoclose in JavaScript, but not HTML.
10484 cx.update_editor(|editor, window, cx| {
10485 editor.handle_input("/", window, cx);
10486 editor.handle_input("*", window, cx);
10487 });
10488 cx.assert_editor_state(
10489 &r#"
10490 <body>/*ˇ
10491 <script>
10492 var x = 1;/*ˇ */
10493 </script>
10494 </body>/*ˇ
10495 "#
10496 .unindent(),
10497 );
10498}
10499
10500#[gpui::test]
10501async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
10502 init_test(cx, |_| {});
10503
10504 let mut cx = EditorTestContext::new(cx).await;
10505
10506 let rust_language = Arc::new(
10507 Language::new(
10508 LanguageConfig {
10509 name: "Rust".into(),
10510 brackets: serde_json::from_value(json!([
10511 { "start": "{", "end": "}", "close": true, "newline": true },
10512 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
10513 ]))
10514 .unwrap(),
10515 autoclose_before: "})]>".into(),
10516 ..Default::default()
10517 },
10518 Some(tree_sitter_rust::LANGUAGE.into()),
10519 )
10520 .with_override_query("(string_literal) @string")
10521 .unwrap(),
10522 );
10523
10524 cx.language_registry().add(rust_language.clone());
10525 cx.update_buffer(|buffer, cx| {
10526 buffer.set_language(Some(rust_language), cx);
10527 });
10528
10529 cx.set_state(
10530 &r#"
10531 let x = ˇ
10532 "#
10533 .unindent(),
10534 );
10535
10536 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
10537 cx.update_editor(|editor, window, cx| {
10538 editor.handle_input("\"", window, cx);
10539 });
10540 cx.assert_editor_state(
10541 &r#"
10542 let x = "ˇ"
10543 "#
10544 .unindent(),
10545 );
10546
10547 // Inserting another quotation mark. The cursor moves across the existing
10548 // automatically-inserted quotation mark.
10549 cx.update_editor(|editor, window, cx| {
10550 editor.handle_input("\"", window, cx);
10551 });
10552 cx.assert_editor_state(
10553 &r#"
10554 let x = ""ˇ
10555 "#
10556 .unindent(),
10557 );
10558
10559 // Reset
10560 cx.set_state(
10561 &r#"
10562 let x = ˇ
10563 "#
10564 .unindent(),
10565 );
10566
10567 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
10568 cx.update_editor(|editor, window, cx| {
10569 editor.handle_input("\"", window, cx);
10570 editor.handle_input(" ", window, cx);
10571 editor.move_left(&Default::default(), window, cx);
10572 editor.handle_input("\\", window, cx);
10573 editor.handle_input("\"", window, cx);
10574 });
10575 cx.assert_editor_state(
10576 &r#"
10577 let x = "\"ˇ "
10578 "#
10579 .unindent(),
10580 );
10581
10582 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
10583 // mark. Nothing is inserted.
10584 cx.update_editor(|editor, window, cx| {
10585 editor.move_right(&Default::default(), window, cx);
10586 editor.handle_input("\"", window, cx);
10587 });
10588 cx.assert_editor_state(
10589 &r#"
10590 let x = "\" "ˇ
10591 "#
10592 .unindent(),
10593 );
10594}
10595
10596#[gpui::test]
10597async fn test_surround_with_pair(cx: &mut TestAppContext) {
10598 init_test(cx, |_| {});
10599
10600 let language = Arc::new(Language::new(
10601 LanguageConfig {
10602 brackets: BracketPairConfig {
10603 pairs: vec![
10604 BracketPair {
10605 start: "{".to_string(),
10606 end: "}".to_string(),
10607 close: true,
10608 surround: true,
10609 newline: true,
10610 },
10611 BracketPair {
10612 start: "/* ".to_string(),
10613 end: "*/".to_string(),
10614 close: true,
10615 surround: true,
10616 ..Default::default()
10617 },
10618 ],
10619 ..Default::default()
10620 },
10621 ..Default::default()
10622 },
10623 Some(tree_sitter_rust::LANGUAGE.into()),
10624 ));
10625
10626 let text = r#"
10627 a
10628 b
10629 c
10630 "#
10631 .unindent();
10632
10633 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10634 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10635 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10636 editor
10637 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10638 .await;
10639
10640 editor.update_in(cx, |editor, window, cx| {
10641 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10642 s.select_display_ranges([
10643 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10644 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10645 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
10646 ])
10647 });
10648
10649 editor.handle_input("{", window, cx);
10650 editor.handle_input("{", window, cx);
10651 editor.handle_input("{", window, cx);
10652 assert_eq!(
10653 editor.text(cx),
10654 "
10655 {{{a}}}
10656 {{{b}}}
10657 {{{c}}}
10658 "
10659 .unindent()
10660 );
10661 assert_eq!(
10662 display_ranges(editor, cx),
10663 [
10664 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
10665 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
10666 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
10667 ]
10668 );
10669
10670 editor.undo(&Undo, window, cx);
10671 editor.undo(&Undo, window, cx);
10672 editor.undo(&Undo, window, cx);
10673 assert_eq!(
10674 editor.text(cx),
10675 "
10676 a
10677 b
10678 c
10679 "
10680 .unindent()
10681 );
10682 assert_eq!(
10683 display_ranges(editor, cx),
10684 [
10685 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10686 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10687 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10688 ]
10689 );
10690
10691 // Ensure inserting the first character of a multi-byte bracket pair
10692 // doesn't surround the selections with the bracket.
10693 editor.handle_input("/", window, cx);
10694 assert_eq!(
10695 editor.text(cx),
10696 "
10697 /
10698 /
10699 /
10700 "
10701 .unindent()
10702 );
10703 assert_eq!(
10704 display_ranges(editor, cx),
10705 [
10706 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10707 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10708 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10709 ]
10710 );
10711
10712 editor.undo(&Undo, window, cx);
10713 assert_eq!(
10714 editor.text(cx),
10715 "
10716 a
10717 b
10718 c
10719 "
10720 .unindent()
10721 );
10722 assert_eq!(
10723 display_ranges(editor, cx),
10724 [
10725 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10726 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10727 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10728 ]
10729 );
10730
10731 // Ensure inserting the last character of a multi-byte bracket pair
10732 // doesn't surround the selections with the bracket.
10733 editor.handle_input("*", window, cx);
10734 assert_eq!(
10735 editor.text(cx),
10736 "
10737 *
10738 *
10739 *
10740 "
10741 .unindent()
10742 );
10743 assert_eq!(
10744 display_ranges(editor, cx),
10745 [
10746 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10747 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10748 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10749 ]
10750 );
10751 });
10752}
10753
10754#[gpui::test]
10755async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
10756 init_test(cx, |_| {});
10757
10758 let language = Arc::new(Language::new(
10759 LanguageConfig {
10760 brackets: BracketPairConfig {
10761 pairs: vec![BracketPair {
10762 start: "{".to_string(),
10763 end: "}".to_string(),
10764 close: true,
10765 surround: true,
10766 newline: true,
10767 }],
10768 ..Default::default()
10769 },
10770 autoclose_before: "}".to_string(),
10771 ..Default::default()
10772 },
10773 Some(tree_sitter_rust::LANGUAGE.into()),
10774 ));
10775
10776 let text = r#"
10777 a
10778 b
10779 c
10780 "#
10781 .unindent();
10782
10783 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10784 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10785 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10786 editor
10787 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10788 .await;
10789
10790 editor.update_in(cx, |editor, window, cx| {
10791 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10792 s.select_ranges([
10793 Point::new(0, 1)..Point::new(0, 1),
10794 Point::new(1, 1)..Point::new(1, 1),
10795 Point::new(2, 1)..Point::new(2, 1),
10796 ])
10797 });
10798
10799 editor.handle_input("{", window, cx);
10800 editor.handle_input("{", window, cx);
10801 editor.handle_input("_", window, cx);
10802 assert_eq!(
10803 editor.text(cx),
10804 "
10805 a{{_}}
10806 b{{_}}
10807 c{{_}}
10808 "
10809 .unindent()
10810 );
10811 assert_eq!(
10812 editor
10813 .selections
10814 .ranges::<Point>(&editor.display_snapshot(cx)),
10815 [
10816 Point::new(0, 4)..Point::new(0, 4),
10817 Point::new(1, 4)..Point::new(1, 4),
10818 Point::new(2, 4)..Point::new(2, 4)
10819 ]
10820 );
10821
10822 editor.backspace(&Default::default(), window, cx);
10823 editor.backspace(&Default::default(), window, cx);
10824 assert_eq!(
10825 editor.text(cx),
10826 "
10827 a{}
10828 b{}
10829 c{}
10830 "
10831 .unindent()
10832 );
10833 assert_eq!(
10834 editor
10835 .selections
10836 .ranges::<Point>(&editor.display_snapshot(cx)),
10837 [
10838 Point::new(0, 2)..Point::new(0, 2),
10839 Point::new(1, 2)..Point::new(1, 2),
10840 Point::new(2, 2)..Point::new(2, 2)
10841 ]
10842 );
10843
10844 editor.delete_to_previous_word_start(&Default::default(), window, cx);
10845 assert_eq!(
10846 editor.text(cx),
10847 "
10848 a
10849 b
10850 c
10851 "
10852 .unindent()
10853 );
10854 assert_eq!(
10855 editor
10856 .selections
10857 .ranges::<Point>(&editor.display_snapshot(cx)),
10858 [
10859 Point::new(0, 1)..Point::new(0, 1),
10860 Point::new(1, 1)..Point::new(1, 1),
10861 Point::new(2, 1)..Point::new(2, 1)
10862 ]
10863 );
10864 });
10865}
10866
10867#[gpui::test]
10868async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
10869 init_test(cx, |settings| {
10870 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10871 });
10872
10873 let mut cx = EditorTestContext::new(cx).await;
10874
10875 let language = Arc::new(Language::new(
10876 LanguageConfig {
10877 brackets: BracketPairConfig {
10878 pairs: vec![
10879 BracketPair {
10880 start: "{".to_string(),
10881 end: "}".to_string(),
10882 close: true,
10883 surround: true,
10884 newline: true,
10885 },
10886 BracketPair {
10887 start: "(".to_string(),
10888 end: ")".to_string(),
10889 close: true,
10890 surround: true,
10891 newline: true,
10892 },
10893 BracketPair {
10894 start: "[".to_string(),
10895 end: "]".to_string(),
10896 close: false,
10897 surround: true,
10898 newline: true,
10899 },
10900 ],
10901 ..Default::default()
10902 },
10903 autoclose_before: "})]".to_string(),
10904 ..Default::default()
10905 },
10906 Some(tree_sitter_rust::LANGUAGE.into()),
10907 ));
10908
10909 cx.language_registry().add(language.clone());
10910 cx.update_buffer(|buffer, cx| {
10911 buffer.set_language(Some(language), cx);
10912 });
10913
10914 cx.set_state(
10915 &"
10916 {(ˇ)}
10917 [[ˇ]]
10918 {(ˇ)}
10919 "
10920 .unindent(),
10921 );
10922
10923 cx.update_editor(|editor, window, cx| {
10924 editor.backspace(&Default::default(), window, cx);
10925 editor.backspace(&Default::default(), window, cx);
10926 });
10927
10928 cx.assert_editor_state(
10929 &"
10930 ˇ
10931 ˇ]]
10932 ˇ
10933 "
10934 .unindent(),
10935 );
10936
10937 cx.update_editor(|editor, window, cx| {
10938 editor.handle_input("{", window, cx);
10939 editor.handle_input("{", window, cx);
10940 editor.move_right(&MoveRight, window, cx);
10941 editor.move_right(&MoveRight, window, cx);
10942 editor.move_left(&MoveLeft, window, cx);
10943 editor.move_left(&MoveLeft, window, cx);
10944 editor.backspace(&Default::default(), window, cx);
10945 });
10946
10947 cx.assert_editor_state(
10948 &"
10949 {ˇ}
10950 {ˇ}]]
10951 {ˇ}
10952 "
10953 .unindent(),
10954 );
10955
10956 cx.update_editor(|editor, window, cx| {
10957 editor.backspace(&Default::default(), window, cx);
10958 });
10959
10960 cx.assert_editor_state(
10961 &"
10962 ˇ
10963 ˇ]]
10964 ˇ
10965 "
10966 .unindent(),
10967 );
10968}
10969
10970#[gpui::test]
10971async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
10972 init_test(cx, |_| {});
10973
10974 let language = Arc::new(Language::new(
10975 LanguageConfig::default(),
10976 Some(tree_sitter_rust::LANGUAGE.into()),
10977 ));
10978
10979 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
10980 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10981 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10982 editor
10983 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10984 .await;
10985
10986 editor.update_in(cx, |editor, window, cx| {
10987 editor.set_auto_replace_emoji_shortcode(true);
10988
10989 editor.handle_input("Hello ", window, cx);
10990 editor.handle_input(":wave", window, cx);
10991 assert_eq!(editor.text(cx), "Hello :wave".unindent());
10992
10993 editor.handle_input(":", window, cx);
10994 assert_eq!(editor.text(cx), "Hello 👋".unindent());
10995
10996 editor.handle_input(" :smile", window, cx);
10997 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
10998
10999 editor.handle_input(":", window, cx);
11000 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
11001
11002 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
11003 editor.handle_input(":wave", window, cx);
11004 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
11005
11006 editor.handle_input(":", window, cx);
11007 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
11008
11009 editor.handle_input(":1", window, cx);
11010 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
11011
11012 editor.handle_input(":", window, cx);
11013 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
11014
11015 // Ensure shortcode does not get replaced when it is part of a word
11016 editor.handle_input(" Test:wave", window, cx);
11017 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
11018
11019 editor.handle_input(":", window, cx);
11020 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
11021
11022 editor.set_auto_replace_emoji_shortcode(false);
11023
11024 // Ensure shortcode does not get replaced when auto replace is off
11025 editor.handle_input(" :wave", window, cx);
11026 assert_eq!(
11027 editor.text(cx),
11028 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
11029 );
11030
11031 editor.handle_input(":", window, cx);
11032 assert_eq!(
11033 editor.text(cx),
11034 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
11035 );
11036 });
11037}
11038
11039#[gpui::test]
11040async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
11041 init_test(cx, |_| {});
11042
11043 let (text, insertion_ranges) = marked_text_ranges(
11044 indoc! {"
11045 ˇ
11046 "},
11047 false,
11048 );
11049
11050 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
11051 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11052
11053 _ = editor.update_in(cx, |editor, window, cx| {
11054 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
11055
11056 editor
11057 .insert_snippet(&insertion_ranges, snippet, window, cx)
11058 .unwrap();
11059
11060 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
11061 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
11062 assert_eq!(editor.text(cx), expected_text);
11063 assert_eq!(
11064 editor
11065 .selections
11066 .ranges::<usize>(&editor.display_snapshot(cx)),
11067 selection_ranges
11068 );
11069 }
11070
11071 assert(
11072 editor,
11073 cx,
11074 indoc! {"
11075 type «» =•
11076 "},
11077 );
11078
11079 assert!(editor.context_menu_visible(), "There should be a matches");
11080 });
11081}
11082
11083#[gpui::test]
11084async fn test_snippets(cx: &mut TestAppContext) {
11085 init_test(cx, |_| {});
11086
11087 let mut cx = EditorTestContext::new(cx).await;
11088
11089 cx.set_state(indoc! {"
11090 a.ˇ b
11091 a.ˇ b
11092 a.ˇ b
11093 "});
11094
11095 cx.update_editor(|editor, window, cx| {
11096 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
11097 let insertion_ranges = editor
11098 .selections
11099 .all(&editor.display_snapshot(cx))
11100 .iter()
11101 .map(|s| s.range())
11102 .collect::<Vec<_>>();
11103 editor
11104 .insert_snippet(&insertion_ranges, snippet, window, cx)
11105 .unwrap();
11106 });
11107
11108 cx.assert_editor_state(indoc! {"
11109 a.f(«oneˇ», two, «threeˇ») b
11110 a.f(«oneˇ», two, «threeˇ») b
11111 a.f(«oneˇ», two, «threeˇ») b
11112 "});
11113
11114 // Can't move earlier than the first tab stop
11115 cx.update_editor(|editor, window, cx| {
11116 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
11117 });
11118 cx.assert_editor_state(indoc! {"
11119 a.f(«oneˇ», two, «threeˇ») b
11120 a.f(«oneˇ», two, «threeˇ») b
11121 a.f(«oneˇ», two, «threeˇ») b
11122 "});
11123
11124 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11125 cx.assert_editor_state(indoc! {"
11126 a.f(one, «twoˇ», three) b
11127 a.f(one, «twoˇ», three) b
11128 a.f(one, «twoˇ», three) b
11129 "});
11130
11131 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
11132 cx.assert_editor_state(indoc! {"
11133 a.f(«oneˇ», two, «threeˇ») b
11134 a.f(«oneˇ», two, «threeˇ») b
11135 a.f(«oneˇ», two, «threeˇ») b
11136 "});
11137
11138 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11139 cx.assert_editor_state(indoc! {"
11140 a.f(one, «twoˇ», three) b
11141 a.f(one, «twoˇ», three) b
11142 a.f(one, «twoˇ», three) b
11143 "});
11144 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11145 cx.assert_editor_state(indoc! {"
11146 a.f(one, two, three)ˇ b
11147 a.f(one, two, three)ˇ b
11148 a.f(one, two, three)ˇ b
11149 "});
11150
11151 // As soon as the last tab stop is reached, snippet state is gone
11152 cx.update_editor(|editor, window, cx| {
11153 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
11154 });
11155 cx.assert_editor_state(indoc! {"
11156 a.f(one, two, three)ˇ b
11157 a.f(one, two, three)ˇ b
11158 a.f(one, two, three)ˇ b
11159 "});
11160}
11161
11162#[gpui::test]
11163async fn test_snippet_indentation(cx: &mut TestAppContext) {
11164 init_test(cx, |_| {});
11165
11166 let mut cx = EditorTestContext::new(cx).await;
11167
11168 cx.update_editor(|editor, window, cx| {
11169 let snippet = Snippet::parse(indoc! {"
11170 /*
11171 * Multiline comment with leading indentation
11172 *
11173 * $1
11174 */
11175 $0"})
11176 .unwrap();
11177 let insertion_ranges = editor
11178 .selections
11179 .all(&editor.display_snapshot(cx))
11180 .iter()
11181 .map(|s| s.range())
11182 .collect::<Vec<_>>();
11183 editor
11184 .insert_snippet(&insertion_ranges, snippet, window, cx)
11185 .unwrap();
11186 });
11187
11188 cx.assert_editor_state(indoc! {"
11189 /*
11190 * Multiline comment with leading indentation
11191 *
11192 * ˇ
11193 */
11194 "});
11195
11196 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11197 cx.assert_editor_state(indoc! {"
11198 /*
11199 * Multiline comment with leading indentation
11200 *
11201 *•
11202 */
11203 ˇ"});
11204}
11205
11206#[gpui::test]
11207async fn test_document_format_during_save(cx: &mut TestAppContext) {
11208 init_test(cx, |_| {});
11209
11210 let fs = FakeFs::new(cx.executor());
11211 fs.insert_file(path!("/file.rs"), Default::default()).await;
11212
11213 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
11214
11215 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11216 language_registry.add(rust_lang());
11217 let mut fake_servers = language_registry.register_fake_lsp(
11218 "Rust",
11219 FakeLspAdapter {
11220 capabilities: lsp::ServerCapabilities {
11221 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11222 ..Default::default()
11223 },
11224 ..Default::default()
11225 },
11226 );
11227
11228 let buffer = project
11229 .update(cx, |project, cx| {
11230 project.open_local_buffer(path!("/file.rs"), cx)
11231 })
11232 .await
11233 .unwrap();
11234
11235 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11236 let (editor, cx) = cx.add_window_view(|window, cx| {
11237 build_editor_with_project(project.clone(), buffer, window, cx)
11238 });
11239 editor.update_in(cx, |editor, window, cx| {
11240 editor.set_text("one\ntwo\nthree\n", window, cx)
11241 });
11242 assert!(cx.read(|cx| editor.is_dirty(cx)));
11243
11244 cx.executor().start_waiting();
11245 let fake_server = fake_servers.next().await.unwrap();
11246
11247 {
11248 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11249 move |params, _| async move {
11250 assert_eq!(
11251 params.text_document.uri,
11252 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11253 );
11254 assert_eq!(params.options.tab_size, 4);
11255 Ok(Some(vec![lsp::TextEdit::new(
11256 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11257 ", ".to_string(),
11258 )]))
11259 },
11260 );
11261 let save = editor
11262 .update_in(cx, |editor, window, cx| {
11263 editor.save(
11264 SaveOptions {
11265 format: true,
11266 autosave: false,
11267 },
11268 project.clone(),
11269 window,
11270 cx,
11271 )
11272 })
11273 .unwrap();
11274 cx.executor().start_waiting();
11275 save.await;
11276
11277 assert_eq!(
11278 editor.update(cx, |editor, cx| editor.text(cx)),
11279 "one, two\nthree\n"
11280 );
11281 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11282 }
11283
11284 {
11285 editor.update_in(cx, |editor, window, cx| {
11286 editor.set_text("one\ntwo\nthree\n", window, cx)
11287 });
11288 assert!(cx.read(|cx| editor.is_dirty(cx)));
11289
11290 // Ensure we can still save even if formatting hangs.
11291 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11292 move |params, _| async move {
11293 assert_eq!(
11294 params.text_document.uri,
11295 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11296 );
11297 futures::future::pending::<()>().await;
11298 unreachable!()
11299 },
11300 );
11301 let save = editor
11302 .update_in(cx, |editor, window, cx| {
11303 editor.save(
11304 SaveOptions {
11305 format: true,
11306 autosave: false,
11307 },
11308 project.clone(),
11309 window,
11310 cx,
11311 )
11312 })
11313 .unwrap();
11314 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11315 cx.executor().start_waiting();
11316 save.await;
11317 assert_eq!(
11318 editor.update(cx, |editor, cx| editor.text(cx)),
11319 "one\ntwo\nthree\n"
11320 );
11321 }
11322
11323 // Set rust language override and assert overridden tabsize is sent to language server
11324 update_test_language_settings(cx, |settings| {
11325 settings.languages.0.insert(
11326 "Rust".into(),
11327 LanguageSettingsContent {
11328 tab_size: NonZeroU32::new(8),
11329 ..Default::default()
11330 },
11331 );
11332 });
11333
11334 {
11335 editor.update_in(cx, |editor, window, cx| {
11336 editor.set_text("somehting_new\n", window, cx)
11337 });
11338 assert!(cx.read(|cx| editor.is_dirty(cx)));
11339 let _formatting_request_signal = fake_server
11340 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
11341 assert_eq!(
11342 params.text_document.uri,
11343 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11344 );
11345 assert_eq!(params.options.tab_size, 8);
11346 Ok(Some(vec![]))
11347 });
11348 let save = editor
11349 .update_in(cx, |editor, window, cx| {
11350 editor.save(
11351 SaveOptions {
11352 format: true,
11353 autosave: false,
11354 },
11355 project.clone(),
11356 window,
11357 cx,
11358 )
11359 })
11360 .unwrap();
11361 cx.executor().start_waiting();
11362 save.await;
11363 }
11364}
11365
11366#[gpui::test]
11367async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
11368 init_test(cx, |settings| {
11369 settings.defaults.ensure_final_newline_on_save = Some(false);
11370 });
11371
11372 let fs = FakeFs::new(cx.executor());
11373 fs.insert_file(path!("/file.txt"), "foo".into()).await;
11374
11375 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
11376
11377 let buffer = project
11378 .update(cx, |project, cx| {
11379 project.open_local_buffer(path!("/file.txt"), cx)
11380 })
11381 .await
11382 .unwrap();
11383
11384 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11385 let (editor, cx) = cx.add_window_view(|window, cx| {
11386 build_editor_with_project(project.clone(), buffer, window, cx)
11387 });
11388 editor.update_in(cx, |editor, window, cx| {
11389 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
11390 s.select_ranges([0..0])
11391 });
11392 });
11393 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11394
11395 editor.update_in(cx, |editor, window, cx| {
11396 editor.handle_input("\n", window, cx)
11397 });
11398 cx.run_until_parked();
11399 save(&editor, &project, cx).await;
11400 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11401
11402 editor.update_in(cx, |editor, window, cx| {
11403 editor.undo(&Default::default(), window, cx);
11404 });
11405 save(&editor, &project, cx).await;
11406 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11407
11408 editor.update_in(cx, |editor, window, cx| {
11409 editor.redo(&Default::default(), window, cx);
11410 });
11411 cx.run_until_parked();
11412 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11413
11414 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
11415 let save = editor
11416 .update_in(cx, |editor, window, cx| {
11417 editor.save(
11418 SaveOptions {
11419 format: true,
11420 autosave: false,
11421 },
11422 project.clone(),
11423 window,
11424 cx,
11425 )
11426 })
11427 .unwrap();
11428 cx.executor().start_waiting();
11429 save.await;
11430 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11431 }
11432}
11433
11434#[gpui::test]
11435async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
11436 init_test(cx, |_| {});
11437
11438 let cols = 4;
11439 let rows = 10;
11440 let sample_text_1 = sample_text(rows, cols, 'a');
11441 assert_eq!(
11442 sample_text_1,
11443 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
11444 );
11445 let sample_text_2 = sample_text(rows, cols, 'l');
11446 assert_eq!(
11447 sample_text_2,
11448 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
11449 );
11450 let sample_text_3 = sample_text(rows, cols, 'v');
11451 assert_eq!(
11452 sample_text_3,
11453 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
11454 );
11455
11456 let fs = FakeFs::new(cx.executor());
11457 fs.insert_tree(
11458 path!("/a"),
11459 json!({
11460 "main.rs": sample_text_1,
11461 "other.rs": sample_text_2,
11462 "lib.rs": sample_text_3,
11463 }),
11464 )
11465 .await;
11466
11467 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11468 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11469 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11470
11471 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11472 language_registry.add(rust_lang());
11473 let mut fake_servers = language_registry.register_fake_lsp(
11474 "Rust",
11475 FakeLspAdapter {
11476 capabilities: lsp::ServerCapabilities {
11477 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11478 ..Default::default()
11479 },
11480 ..Default::default()
11481 },
11482 );
11483
11484 let worktree = project.update(cx, |project, cx| {
11485 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
11486 assert_eq!(worktrees.len(), 1);
11487 worktrees.pop().unwrap()
11488 });
11489 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11490
11491 let buffer_1 = project
11492 .update(cx, |project, cx| {
11493 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
11494 })
11495 .await
11496 .unwrap();
11497 let buffer_2 = project
11498 .update(cx, |project, cx| {
11499 project.open_buffer((worktree_id, rel_path("other.rs")), cx)
11500 })
11501 .await
11502 .unwrap();
11503 let buffer_3 = project
11504 .update(cx, |project, cx| {
11505 project.open_buffer((worktree_id, rel_path("lib.rs")), cx)
11506 })
11507 .await
11508 .unwrap();
11509
11510 let multi_buffer = cx.new(|cx| {
11511 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11512 multi_buffer.push_excerpts(
11513 buffer_1.clone(),
11514 [
11515 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11516 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11517 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11518 ],
11519 cx,
11520 );
11521 multi_buffer.push_excerpts(
11522 buffer_2.clone(),
11523 [
11524 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11525 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11526 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11527 ],
11528 cx,
11529 );
11530 multi_buffer.push_excerpts(
11531 buffer_3.clone(),
11532 [
11533 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11534 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11535 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11536 ],
11537 cx,
11538 );
11539 multi_buffer
11540 });
11541 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
11542 Editor::new(
11543 EditorMode::full(),
11544 multi_buffer,
11545 Some(project.clone()),
11546 window,
11547 cx,
11548 )
11549 });
11550
11551 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11552 editor.change_selections(
11553 SelectionEffects::scroll(Autoscroll::Next),
11554 window,
11555 cx,
11556 |s| s.select_ranges(Some(1..2)),
11557 );
11558 editor.insert("|one|two|three|", window, cx);
11559 });
11560 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11561 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11562 editor.change_selections(
11563 SelectionEffects::scroll(Autoscroll::Next),
11564 window,
11565 cx,
11566 |s| s.select_ranges(Some(60..70)),
11567 );
11568 editor.insert("|four|five|six|", window, cx);
11569 });
11570 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11571
11572 // First two buffers should be edited, but not the third one.
11573 assert_eq!(
11574 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11575 "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}",
11576 );
11577 buffer_1.update(cx, |buffer, _| {
11578 assert!(buffer.is_dirty());
11579 assert_eq!(
11580 buffer.text(),
11581 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
11582 )
11583 });
11584 buffer_2.update(cx, |buffer, _| {
11585 assert!(buffer.is_dirty());
11586 assert_eq!(
11587 buffer.text(),
11588 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
11589 )
11590 });
11591 buffer_3.update(cx, |buffer, _| {
11592 assert!(!buffer.is_dirty());
11593 assert_eq!(buffer.text(), sample_text_3,)
11594 });
11595 cx.executor().run_until_parked();
11596
11597 cx.executor().start_waiting();
11598 let save = multi_buffer_editor
11599 .update_in(cx, |editor, window, cx| {
11600 editor.save(
11601 SaveOptions {
11602 format: true,
11603 autosave: false,
11604 },
11605 project.clone(),
11606 window,
11607 cx,
11608 )
11609 })
11610 .unwrap();
11611
11612 let fake_server = fake_servers.next().await.unwrap();
11613 fake_server
11614 .server
11615 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
11616 Ok(Some(vec![lsp::TextEdit::new(
11617 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11618 format!("[{} formatted]", params.text_document.uri),
11619 )]))
11620 })
11621 .detach();
11622 save.await;
11623
11624 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
11625 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
11626 assert_eq!(
11627 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11628 uri!(
11629 "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}"
11630 ),
11631 );
11632 buffer_1.update(cx, |buffer, _| {
11633 assert!(!buffer.is_dirty());
11634 assert_eq!(
11635 buffer.text(),
11636 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
11637 )
11638 });
11639 buffer_2.update(cx, |buffer, _| {
11640 assert!(!buffer.is_dirty());
11641 assert_eq!(
11642 buffer.text(),
11643 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
11644 )
11645 });
11646 buffer_3.update(cx, |buffer, _| {
11647 assert!(!buffer.is_dirty());
11648 assert_eq!(buffer.text(), sample_text_3,)
11649 });
11650}
11651
11652#[gpui::test]
11653async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
11654 init_test(cx, |_| {});
11655
11656 let fs = FakeFs::new(cx.executor());
11657 fs.insert_tree(
11658 path!("/dir"),
11659 json!({
11660 "file1.rs": "fn main() { println!(\"hello\"); }",
11661 "file2.rs": "fn test() { println!(\"test\"); }",
11662 "file3.rs": "fn other() { println!(\"other\"); }\n",
11663 }),
11664 )
11665 .await;
11666
11667 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
11668 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11669 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11670
11671 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11672 language_registry.add(rust_lang());
11673
11674 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
11675 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11676
11677 // Open three buffers
11678 let buffer_1 = project
11679 .update(cx, |project, cx| {
11680 project.open_buffer((worktree_id, rel_path("file1.rs")), cx)
11681 })
11682 .await
11683 .unwrap();
11684 let buffer_2 = project
11685 .update(cx, |project, cx| {
11686 project.open_buffer((worktree_id, rel_path("file2.rs")), cx)
11687 })
11688 .await
11689 .unwrap();
11690 let buffer_3 = project
11691 .update(cx, |project, cx| {
11692 project.open_buffer((worktree_id, rel_path("file3.rs")), cx)
11693 })
11694 .await
11695 .unwrap();
11696
11697 // Create a multi-buffer with all three buffers
11698 let multi_buffer = cx.new(|cx| {
11699 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11700 multi_buffer.push_excerpts(
11701 buffer_1.clone(),
11702 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11703 cx,
11704 );
11705 multi_buffer.push_excerpts(
11706 buffer_2.clone(),
11707 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11708 cx,
11709 );
11710 multi_buffer.push_excerpts(
11711 buffer_3.clone(),
11712 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11713 cx,
11714 );
11715 multi_buffer
11716 });
11717
11718 let editor = cx.new_window_entity(|window, cx| {
11719 Editor::new(
11720 EditorMode::full(),
11721 multi_buffer,
11722 Some(project.clone()),
11723 window,
11724 cx,
11725 )
11726 });
11727
11728 // Edit only the first buffer
11729 editor.update_in(cx, |editor, window, cx| {
11730 editor.change_selections(
11731 SelectionEffects::scroll(Autoscroll::Next),
11732 window,
11733 cx,
11734 |s| s.select_ranges(Some(10..10)),
11735 );
11736 editor.insert("// edited", window, cx);
11737 });
11738
11739 // Verify that only buffer 1 is dirty
11740 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
11741 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11742 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11743
11744 // Get write counts after file creation (files were created with initial content)
11745 // We expect each file to have been written once during creation
11746 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
11747 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
11748 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
11749
11750 // Perform autosave
11751 let save_task = editor.update_in(cx, |editor, window, cx| {
11752 editor.save(
11753 SaveOptions {
11754 format: true,
11755 autosave: true,
11756 },
11757 project.clone(),
11758 window,
11759 cx,
11760 )
11761 });
11762 save_task.await.unwrap();
11763
11764 // Only the dirty buffer should have been saved
11765 assert_eq!(
11766 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11767 1,
11768 "Buffer 1 was dirty, so it should have been written once during autosave"
11769 );
11770 assert_eq!(
11771 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11772 0,
11773 "Buffer 2 was clean, so it should not have been written during autosave"
11774 );
11775 assert_eq!(
11776 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11777 0,
11778 "Buffer 3 was clean, so it should not have been written during autosave"
11779 );
11780
11781 // Verify buffer states after autosave
11782 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11783 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11784 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11785
11786 // Now perform a manual save (format = true)
11787 let save_task = editor.update_in(cx, |editor, window, cx| {
11788 editor.save(
11789 SaveOptions {
11790 format: true,
11791 autosave: false,
11792 },
11793 project.clone(),
11794 window,
11795 cx,
11796 )
11797 });
11798 save_task.await.unwrap();
11799
11800 // During manual save, clean buffers don't get written to disk
11801 // They just get did_save called for language server notifications
11802 assert_eq!(
11803 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11804 1,
11805 "Buffer 1 should only have been written once total (during autosave, not manual save)"
11806 );
11807 assert_eq!(
11808 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11809 0,
11810 "Buffer 2 should not have been written at all"
11811 );
11812 assert_eq!(
11813 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11814 0,
11815 "Buffer 3 should not have been written at all"
11816 );
11817}
11818
11819async fn setup_range_format_test(
11820 cx: &mut TestAppContext,
11821) -> (
11822 Entity<Project>,
11823 Entity<Editor>,
11824 &mut gpui::VisualTestContext,
11825 lsp::FakeLanguageServer,
11826) {
11827 init_test(cx, |_| {});
11828
11829 let fs = FakeFs::new(cx.executor());
11830 fs.insert_file(path!("/file.rs"), Default::default()).await;
11831
11832 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11833
11834 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11835 language_registry.add(rust_lang());
11836 let mut fake_servers = language_registry.register_fake_lsp(
11837 "Rust",
11838 FakeLspAdapter {
11839 capabilities: lsp::ServerCapabilities {
11840 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
11841 ..lsp::ServerCapabilities::default()
11842 },
11843 ..FakeLspAdapter::default()
11844 },
11845 );
11846
11847 let buffer = project
11848 .update(cx, |project, cx| {
11849 project.open_local_buffer(path!("/file.rs"), cx)
11850 })
11851 .await
11852 .unwrap();
11853
11854 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11855 let (editor, cx) = cx.add_window_view(|window, cx| {
11856 build_editor_with_project(project.clone(), buffer, window, cx)
11857 });
11858
11859 cx.executor().start_waiting();
11860 let fake_server = fake_servers.next().await.unwrap();
11861
11862 (project, editor, cx, fake_server)
11863}
11864
11865#[gpui::test]
11866async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
11867 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11868
11869 editor.update_in(cx, |editor, window, cx| {
11870 editor.set_text("one\ntwo\nthree\n", window, cx)
11871 });
11872 assert!(cx.read(|cx| editor.is_dirty(cx)));
11873
11874 let save = editor
11875 .update_in(cx, |editor, window, cx| {
11876 editor.save(
11877 SaveOptions {
11878 format: true,
11879 autosave: false,
11880 },
11881 project.clone(),
11882 window,
11883 cx,
11884 )
11885 })
11886 .unwrap();
11887 fake_server
11888 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
11889 assert_eq!(
11890 params.text_document.uri,
11891 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11892 );
11893 assert_eq!(params.options.tab_size, 4);
11894 Ok(Some(vec![lsp::TextEdit::new(
11895 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11896 ", ".to_string(),
11897 )]))
11898 })
11899 .next()
11900 .await;
11901 cx.executor().start_waiting();
11902 save.await;
11903 assert_eq!(
11904 editor.update(cx, |editor, cx| editor.text(cx)),
11905 "one, two\nthree\n"
11906 );
11907 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11908}
11909
11910#[gpui::test]
11911async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
11912 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11913
11914 editor.update_in(cx, |editor, window, cx| {
11915 editor.set_text("one\ntwo\nthree\n", window, cx)
11916 });
11917 assert!(cx.read(|cx| editor.is_dirty(cx)));
11918
11919 // Test that save still works when formatting hangs
11920 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
11921 move |params, _| async move {
11922 assert_eq!(
11923 params.text_document.uri,
11924 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11925 );
11926 futures::future::pending::<()>().await;
11927 unreachable!()
11928 },
11929 );
11930 let save = editor
11931 .update_in(cx, |editor, window, cx| {
11932 editor.save(
11933 SaveOptions {
11934 format: true,
11935 autosave: false,
11936 },
11937 project.clone(),
11938 window,
11939 cx,
11940 )
11941 })
11942 .unwrap();
11943 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11944 cx.executor().start_waiting();
11945 save.await;
11946 assert_eq!(
11947 editor.update(cx, |editor, cx| editor.text(cx)),
11948 "one\ntwo\nthree\n"
11949 );
11950 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11951}
11952
11953#[gpui::test]
11954async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
11955 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11956
11957 // Buffer starts clean, no formatting should be requested
11958 let save = editor
11959 .update_in(cx, |editor, window, cx| {
11960 editor.save(
11961 SaveOptions {
11962 format: false,
11963 autosave: false,
11964 },
11965 project.clone(),
11966 window,
11967 cx,
11968 )
11969 })
11970 .unwrap();
11971 let _pending_format_request = fake_server
11972 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
11973 panic!("Should not be invoked");
11974 })
11975 .next();
11976 cx.executor().start_waiting();
11977 save.await;
11978 cx.run_until_parked();
11979}
11980
11981#[gpui::test]
11982async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
11983 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11984
11985 // Set Rust language override and assert overridden tabsize is sent to language server
11986 update_test_language_settings(cx, |settings| {
11987 settings.languages.0.insert(
11988 "Rust".into(),
11989 LanguageSettingsContent {
11990 tab_size: NonZeroU32::new(8),
11991 ..Default::default()
11992 },
11993 );
11994 });
11995
11996 editor.update_in(cx, |editor, window, cx| {
11997 editor.set_text("something_new\n", window, cx)
11998 });
11999 assert!(cx.read(|cx| editor.is_dirty(cx)));
12000 let save = editor
12001 .update_in(cx, |editor, window, cx| {
12002 editor.save(
12003 SaveOptions {
12004 format: true,
12005 autosave: false,
12006 },
12007 project.clone(),
12008 window,
12009 cx,
12010 )
12011 })
12012 .unwrap();
12013 fake_server
12014 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
12015 assert_eq!(
12016 params.text_document.uri,
12017 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12018 );
12019 assert_eq!(params.options.tab_size, 8);
12020 Ok(Some(Vec::new()))
12021 })
12022 .next()
12023 .await;
12024 save.await;
12025}
12026
12027#[gpui::test]
12028async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
12029 init_test(cx, |settings| {
12030 settings.defaults.formatter = Some(FormatterList::Single(Formatter::LanguageServer(
12031 settings::LanguageServerFormatterSpecifier::Current,
12032 )))
12033 });
12034
12035 let fs = FakeFs::new(cx.executor());
12036 fs.insert_file(path!("/file.rs"), Default::default()).await;
12037
12038 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12039
12040 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12041 language_registry.add(Arc::new(Language::new(
12042 LanguageConfig {
12043 name: "Rust".into(),
12044 matcher: LanguageMatcher {
12045 path_suffixes: vec!["rs".to_string()],
12046 ..Default::default()
12047 },
12048 ..LanguageConfig::default()
12049 },
12050 Some(tree_sitter_rust::LANGUAGE.into()),
12051 )));
12052 update_test_language_settings(cx, |settings| {
12053 // Enable Prettier formatting for the same buffer, and ensure
12054 // LSP is called instead of Prettier.
12055 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
12056 });
12057 let mut fake_servers = language_registry.register_fake_lsp(
12058 "Rust",
12059 FakeLspAdapter {
12060 capabilities: lsp::ServerCapabilities {
12061 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12062 ..Default::default()
12063 },
12064 ..Default::default()
12065 },
12066 );
12067
12068 let buffer = project
12069 .update(cx, |project, cx| {
12070 project.open_local_buffer(path!("/file.rs"), cx)
12071 })
12072 .await
12073 .unwrap();
12074
12075 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12076 let (editor, cx) = cx.add_window_view(|window, cx| {
12077 build_editor_with_project(project.clone(), buffer, window, cx)
12078 });
12079 editor.update_in(cx, |editor, window, cx| {
12080 editor.set_text("one\ntwo\nthree\n", window, cx)
12081 });
12082
12083 cx.executor().start_waiting();
12084 let fake_server = fake_servers.next().await.unwrap();
12085
12086 let format = editor
12087 .update_in(cx, |editor, window, cx| {
12088 editor.perform_format(
12089 project.clone(),
12090 FormatTrigger::Manual,
12091 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12092 window,
12093 cx,
12094 )
12095 })
12096 .unwrap();
12097 fake_server
12098 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
12099 assert_eq!(
12100 params.text_document.uri,
12101 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12102 );
12103 assert_eq!(params.options.tab_size, 4);
12104 Ok(Some(vec![lsp::TextEdit::new(
12105 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12106 ", ".to_string(),
12107 )]))
12108 })
12109 .next()
12110 .await;
12111 cx.executor().start_waiting();
12112 format.await;
12113 assert_eq!(
12114 editor.update(cx, |editor, cx| editor.text(cx)),
12115 "one, two\nthree\n"
12116 );
12117
12118 editor.update_in(cx, |editor, window, cx| {
12119 editor.set_text("one\ntwo\nthree\n", window, cx)
12120 });
12121 // Ensure we don't lock if formatting hangs.
12122 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12123 move |params, _| async move {
12124 assert_eq!(
12125 params.text_document.uri,
12126 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12127 );
12128 futures::future::pending::<()>().await;
12129 unreachable!()
12130 },
12131 );
12132 let format = editor
12133 .update_in(cx, |editor, window, cx| {
12134 editor.perform_format(
12135 project,
12136 FormatTrigger::Manual,
12137 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12138 window,
12139 cx,
12140 )
12141 })
12142 .unwrap();
12143 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
12144 cx.executor().start_waiting();
12145 format.await;
12146 assert_eq!(
12147 editor.update(cx, |editor, cx| editor.text(cx)),
12148 "one\ntwo\nthree\n"
12149 );
12150}
12151
12152#[gpui::test]
12153async fn test_multiple_formatters(cx: &mut TestAppContext) {
12154 init_test(cx, |settings| {
12155 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
12156 settings.defaults.formatter = Some(FormatterList::Vec(vec![
12157 Formatter::LanguageServer(settings::LanguageServerFormatterSpecifier::Current),
12158 Formatter::CodeAction("code-action-1".into()),
12159 Formatter::CodeAction("code-action-2".into()),
12160 ]))
12161 });
12162
12163 let fs = FakeFs::new(cx.executor());
12164 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
12165 .await;
12166
12167 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12168 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12169 language_registry.add(rust_lang());
12170
12171 let mut fake_servers = language_registry.register_fake_lsp(
12172 "Rust",
12173 FakeLspAdapter {
12174 capabilities: lsp::ServerCapabilities {
12175 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12176 execute_command_provider: Some(lsp::ExecuteCommandOptions {
12177 commands: vec!["the-command-for-code-action-1".into()],
12178 ..Default::default()
12179 }),
12180 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12181 ..Default::default()
12182 },
12183 ..Default::default()
12184 },
12185 );
12186
12187 let buffer = project
12188 .update(cx, |project, cx| {
12189 project.open_local_buffer(path!("/file.rs"), cx)
12190 })
12191 .await
12192 .unwrap();
12193
12194 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12195 let (editor, cx) = cx.add_window_view(|window, cx| {
12196 build_editor_with_project(project.clone(), buffer, window, cx)
12197 });
12198
12199 cx.executor().start_waiting();
12200
12201 let fake_server = fake_servers.next().await.unwrap();
12202 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12203 move |_params, _| async move {
12204 Ok(Some(vec![lsp::TextEdit::new(
12205 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12206 "applied-formatting\n".to_string(),
12207 )]))
12208 },
12209 );
12210 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12211 move |params, _| async move {
12212 let requested_code_actions = params.context.only.expect("Expected code action request");
12213 assert_eq!(requested_code_actions.len(), 1);
12214
12215 let uri = lsp::Uri::from_file_path(path!("/file.rs")).unwrap();
12216 let code_action = match requested_code_actions[0].as_str() {
12217 "code-action-1" => lsp::CodeAction {
12218 kind: Some("code-action-1".into()),
12219 edit: Some(lsp::WorkspaceEdit::new(
12220 [(
12221 uri,
12222 vec![lsp::TextEdit::new(
12223 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12224 "applied-code-action-1-edit\n".to_string(),
12225 )],
12226 )]
12227 .into_iter()
12228 .collect(),
12229 )),
12230 command: Some(lsp::Command {
12231 command: "the-command-for-code-action-1".into(),
12232 ..Default::default()
12233 }),
12234 ..Default::default()
12235 },
12236 "code-action-2" => lsp::CodeAction {
12237 kind: Some("code-action-2".into()),
12238 edit: Some(lsp::WorkspaceEdit::new(
12239 [(
12240 uri,
12241 vec![lsp::TextEdit::new(
12242 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12243 "applied-code-action-2-edit\n".to_string(),
12244 )],
12245 )]
12246 .into_iter()
12247 .collect(),
12248 )),
12249 ..Default::default()
12250 },
12251 req => panic!("Unexpected code action request: {:?}", req),
12252 };
12253 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12254 code_action,
12255 )]))
12256 },
12257 );
12258
12259 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
12260 move |params, _| async move { Ok(params) }
12261 });
12262
12263 let command_lock = Arc::new(futures::lock::Mutex::new(()));
12264 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
12265 let fake = fake_server.clone();
12266 let lock = command_lock.clone();
12267 move |params, _| {
12268 assert_eq!(params.command, "the-command-for-code-action-1");
12269 let fake = fake.clone();
12270 let lock = lock.clone();
12271 async move {
12272 lock.lock().await;
12273 fake.server
12274 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
12275 label: None,
12276 edit: lsp::WorkspaceEdit {
12277 changes: Some(
12278 [(
12279 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
12280 vec![lsp::TextEdit {
12281 range: lsp::Range::new(
12282 lsp::Position::new(0, 0),
12283 lsp::Position::new(0, 0),
12284 ),
12285 new_text: "applied-code-action-1-command\n".into(),
12286 }],
12287 )]
12288 .into_iter()
12289 .collect(),
12290 ),
12291 ..Default::default()
12292 },
12293 })
12294 .await
12295 .into_response()
12296 .unwrap();
12297 Ok(Some(json!(null)))
12298 }
12299 }
12300 });
12301
12302 cx.executor().start_waiting();
12303 editor
12304 .update_in(cx, |editor, window, cx| {
12305 editor.perform_format(
12306 project.clone(),
12307 FormatTrigger::Manual,
12308 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12309 window,
12310 cx,
12311 )
12312 })
12313 .unwrap()
12314 .await;
12315 editor.update(cx, |editor, cx| {
12316 assert_eq!(
12317 editor.text(cx),
12318 r#"
12319 applied-code-action-2-edit
12320 applied-code-action-1-command
12321 applied-code-action-1-edit
12322 applied-formatting
12323 one
12324 two
12325 three
12326 "#
12327 .unindent()
12328 );
12329 });
12330
12331 editor.update_in(cx, |editor, window, cx| {
12332 editor.undo(&Default::default(), window, cx);
12333 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12334 });
12335
12336 // Perform a manual edit while waiting for an LSP command
12337 // that's being run as part of a formatting code action.
12338 let lock_guard = command_lock.lock().await;
12339 let format = editor
12340 .update_in(cx, |editor, window, cx| {
12341 editor.perform_format(
12342 project.clone(),
12343 FormatTrigger::Manual,
12344 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12345 window,
12346 cx,
12347 )
12348 })
12349 .unwrap();
12350 cx.run_until_parked();
12351 editor.update(cx, |editor, cx| {
12352 assert_eq!(
12353 editor.text(cx),
12354 r#"
12355 applied-code-action-1-edit
12356 applied-formatting
12357 one
12358 two
12359 three
12360 "#
12361 .unindent()
12362 );
12363
12364 editor.buffer.update(cx, |buffer, cx| {
12365 let ix = buffer.len(cx);
12366 buffer.edit([(ix..ix, "edited\n")], None, cx);
12367 });
12368 });
12369
12370 // Allow the LSP command to proceed. Because the buffer was edited,
12371 // the second code action will not be run.
12372 drop(lock_guard);
12373 format.await;
12374 editor.update_in(cx, |editor, window, cx| {
12375 assert_eq!(
12376 editor.text(cx),
12377 r#"
12378 applied-code-action-1-command
12379 applied-code-action-1-edit
12380 applied-formatting
12381 one
12382 two
12383 three
12384 edited
12385 "#
12386 .unindent()
12387 );
12388
12389 // The manual edit is undone first, because it is the last thing the user did
12390 // (even though the command completed afterwards).
12391 editor.undo(&Default::default(), window, cx);
12392 assert_eq!(
12393 editor.text(cx),
12394 r#"
12395 applied-code-action-1-command
12396 applied-code-action-1-edit
12397 applied-formatting
12398 one
12399 two
12400 three
12401 "#
12402 .unindent()
12403 );
12404
12405 // All the formatting (including the command, which completed after the manual edit)
12406 // is undone together.
12407 editor.undo(&Default::default(), window, cx);
12408 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12409 });
12410}
12411
12412#[gpui::test]
12413async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
12414 init_test(cx, |settings| {
12415 settings.defaults.formatter = Some(FormatterList::Vec(vec![Formatter::LanguageServer(
12416 settings::LanguageServerFormatterSpecifier::Current,
12417 )]))
12418 });
12419
12420 let fs = FakeFs::new(cx.executor());
12421 fs.insert_file(path!("/file.ts"), Default::default()).await;
12422
12423 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12424
12425 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12426 language_registry.add(Arc::new(Language::new(
12427 LanguageConfig {
12428 name: "TypeScript".into(),
12429 matcher: LanguageMatcher {
12430 path_suffixes: vec!["ts".to_string()],
12431 ..Default::default()
12432 },
12433 ..LanguageConfig::default()
12434 },
12435 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12436 )));
12437 update_test_language_settings(cx, |settings| {
12438 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
12439 });
12440 let mut fake_servers = language_registry.register_fake_lsp(
12441 "TypeScript",
12442 FakeLspAdapter {
12443 capabilities: lsp::ServerCapabilities {
12444 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12445 ..Default::default()
12446 },
12447 ..Default::default()
12448 },
12449 );
12450
12451 let buffer = project
12452 .update(cx, |project, cx| {
12453 project.open_local_buffer(path!("/file.ts"), cx)
12454 })
12455 .await
12456 .unwrap();
12457
12458 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12459 let (editor, cx) = cx.add_window_view(|window, cx| {
12460 build_editor_with_project(project.clone(), buffer, window, cx)
12461 });
12462 editor.update_in(cx, |editor, window, cx| {
12463 editor.set_text(
12464 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12465 window,
12466 cx,
12467 )
12468 });
12469
12470 cx.executor().start_waiting();
12471 let fake_server = fake_servers.next().await.unwrap();
12472
12473 let format = editor
12474 .update_in(cx, |editor, window, cx| {
12475 editor.perform_code_action_kind(
12476 project.clone(),
12477 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12478 window,
12479 cx,
12480 )
12481 })
12482 .unwrap();
12483 fake_server
12484 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
12485 assert_eq!(
12486 params.text_document.uri,
12487 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12488 );
12489 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12490 lsp::CodeAction {
12491 title: "Organize Imports".to_string(),
12492 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
12493 edit: Some(lsp::WorkspaceEdit {
12494 changes: Some(
12495 [(
12496 params.text_document.uri.clone(),
12497 vec![lsp::TextEdit::new(
12498 lsp::Range::new(
12499 lsp::Position::new(1, 0),
12500 lsp::Position::new(2, 0),
12501 ),
12502 "".to_string(),
12503 )],
12504 )]
12505 .into_iter()
12506 .collect(),
12507 ),
12508 ..Default::default()
12509 }),
12510 ..Default::default()
12511 },
12512 )]))
12513 })
12514 .next()
12515 .await;
12516 cx.executor().start_waiting();
12517 format.await;
12518 assert_eq!(
12519 editor.update(cx, |editor, cx| editor.text(cx)),
12520 "import { a } from 'module';\n\nconst x = a;\n"
12521 );
12522
12523 editor.update_in(cx, |editor, window, cx| {
12524 editor.set_text(
12525 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12526 window,
12527 cx,
12528 )
12529 });
12530 // Ensure we don't lock if code action hangs.
12531 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12532 move |params, _| async move {
12533 assert_eq!(
12534 params.text_document.uri,
12535 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12536 );
12537 futures::future::pending::<()>().await;
12538 unreachable!()
12539 },
12540 );
12541 let format = editor
12542 .update_in(cx, |editor, window, cx| {
12543 editor.perform_code_action_kind(
12544 project,
12545 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12546 window,
12547 cx,
12548 )
12549 })
12550 .unwrap();
12551 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
12552 cx.executor().start_waiting();
12553 format.await;
12554 assert_eq!(
12555 editor.update(cx, |editor, cx| editor.text(cx)),
12556 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
12557 );
12558}
12559
12560#[gpui::test]
12561async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
12562 init_test(cx, |_| {});
12563
12564 let mut cx = EditorLspTestContext::new_rust(
12565 lsp::ServerCapabilities {
12566 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12567 ..Default::default()
12568 },
12569 cx,
12570 )
12571 .await;
12572
12573 cx.set_state(indoc! {"
12574 one.twoˇ
12575 "});
12576
12577 // The format request takes a long time. When it completes, it inserts
12578 // a newline and an indent before the `.`
12579 cx.lsp
12580 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
12581 let executor = cx.background_executor().clone();
12582 async move {
12583 executor.timer(Duration::from_millis(100)).await;
12584 Ok(Some(vec![lsp::TextEdit {
12585 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
12586 new_text: "\n ".into(),
12587 }]))
12588 }
12589 });
12590
12591 // Submit a format request.
12592 let format_1 = cx
12593 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12594 .unwrap();
12595 cx.executor().run_until_parked();
12596
12597 // Submit a second format request.
12598 let format_2 = cx
12599 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12600 .unwrap();
12601 cx.executor().run_until_parked();
12602
12603 // Wait for both format requests to complete
12604 cx.executor().advance_clock(Duration::from_millis(200));
12605 cx.executor().start_waiting();
12606 format_1.await.unwrap();
12607 cx.executor().start_waiting();
12608 format_2.await.unwrap();
12609
12610 // The formatting edits only happens once.
12611 cx.assert_editor_state(indoc! {"
12612 one
12613 .twoˇ
12614 "});
12615}
12616
12617#[gpui::test]
12618async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
12619 init_test(cx, |settings| {
12620 settings.defaults.formatter = Some(FormatterList::default())
12621 });
12622
12623 let mut cx = EditorLspTestContext::new_rust(
12624 lsp::ServerCapabilities {
12625 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12626 ..Default::default()
12627 },
12628 cx,
12629 )
12630 .await;
12631
12632 // Record which buffer changes have been sent to the language server
12633 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
12634 cx.lsp
12635 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
12636 let buffer_changes = buffer_changes.clone();
12637 move |params, _| {
12638 buffer_changes.lock().extend(
12639 params
12640 .content_changes
12641 .into_iter()
12642 .map(|e| (e.range.unwrap(), e.text)),
12643 );
12644 }
12645 });
12646 // Handle formatting requests to the language server.
12647 cx.lsp
12648 .set_request_handler::<lsp::request::Formatting, _, _>({
12649 let buffer_changes = buffer_changes.clone();
12650 move |_, _| {
12651 let buffer_changes = buffer_changes.clone();
12652 // Insert blank lines between each line of the buffer.
12653 async move {
12654 // When formatting is requested, trailing whitespace has already been stripped,
12655 // and the trailing newline has already been added.
12656 assert_eq!(
12657 &buffer_changes.lock()[1..],
12658 &[
12659 (
12660 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
12661 "".into()
12662 ),
12663 (
12664 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
12665 "".into()
12666 ),
12667 (
12668 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
12669 "\n".into()
12670 ),
12671 ]
12672 );
12673
12674 Ok(Some(vec![
12675 lsp::TextEdit {
12676 range: lsp::Range::new(
12677 lsp::Position::new(1, 0),
12678 lsp::Position::new(1, 0),
12679 ),
12680 new_text: "\n".into(),
12681 },
12682 lsp::TextEdit {
12683 range: lsp::Range::new(
12684 lsp::Position::new(2, 0),
12685 lsp::Position::new(2, 0),
12686 ),
12687 new_text: "\n".into(),
12688 },
12689 ]))
12690 }
12691 }
12692 });
12693
12694 // Set up a buffer white some trailing whitespace and no trailing newline.
12695 cx.set_state(
12696 &[
12697 "one ", //
12698 "twoˇ", //
12699 "three ", //
12700 "four", //
12701 ]
12702 .join("\n"),
12703 );
12704 cx.run_until_parked();
12705
12706 // Submit a format request.
12707 let format = cx
12708 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12709 .unwrap();
12710
12711 cx.run_until_parked();
12712 // After formatting the buffer, the trailing whitespace is stripped,
12713 // a newline is appended, and the edits provided by the language server
12714 // have been applied.
12715 format.await.unwrap();
12716
12717 cx.assert_editor_state(
12718 &[
12719 "one", //
12720 "", //
12721 "twoˇ", //
12722 "", //
12723 "three", //
12724 "four", //
12725 "", //
12726 ]
12727 .join("\n"),
12728 );
12729
12730 // Undoing the formatting undoes the trailing whitespace removal, the
12731 // trailing newline, and the LSP edits.
12732 cx.update_buffer(|buffer, cx| buffer.undo(cx));
12733 cx.assert_editor_state(
12734 &[
12735 "one ", //
12736 "twoˇ", //
12737 "three ", //
12738 "four", //
12739 ]
12740 .join("\n"),
12741 );
12742}
12743
12744#[gpui::test]
12745async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
12746 cx: &mut TestAppContext,
12747) {
12748 init_test(cx, |_| {});
12749
12750 cx.update(|cx| {
12751 cx.update_global::<SettingsStore, _>(|settings, cx| {
12752 settings.update_user_settings(cx, |settings| {
12753 settings.editor.auto_signature_help = Some(true);
12754 });
12755 });
12756 });
12757
12758 let mut cx = EditorLspTestContext::new_rust(
12759 lsp::ServerCapabilities {
12760 signature_help_provider: Some(lsp::SignatureHelpOptions {
12761 ..Default::default()
12762 }),
12763 ..Default::default()
12764 },
12765 cx,
12766 )
12767 .await;
12768
12769 let language = Language::new(
12770 LanguageConfig {
12771 name: "Rust".into(),
12772 brackets: BracketPairConfig {
12773 pairs: vec![
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: true,
12785 surround: true,
12786 newline: true,
12787 },
12788 BracketPair {
12789 start: "/*".to_string(),
12790 end: " */".to_string(),
12791 close: true,
12792 surround: true,
12793 newline: true,
12794 },
12795 BracketPair {
12796 start: "[".to_string(),
12797 end: "]".to_string(),
12798 close: false,
12799 surround: false,
12800 newline: true,
12801 },
12802 BracketPair {
12803 start: "\"".to_string(),
12804 end: "\"".to_string(),
12805 close: true,
12806 surround: true,
12807 newline: false,
12808 },
12809 BracketPair {
12810 start: "<".to_string(),
12811 end: ">".to_string(),
12812 close: false,
12813 surround: true,
12814 newline: true,
12815 },
12816 ],
12817 ..Default::default()
12818 },
12819 autoclose_before: "})]".to_string(),
12820 ..Default::default()
12821 },
12822 Some(tree_sitter_rust::LANGUAGE.into()),
12823 );
12824 let language = Arc::new(language);
12825
12826 cx.language_registry().add(language.clone());
12827 cx.update_buffer(|buffer, cx| {
12828 buffer.set_language(Some(language), cx);
12829 });
12830
12831 cx.set_state(
12832 &r#"
12833 fn main() {
12834 sampleˇ
12835 }
12836 "#
12837 .unindent(),
12838 );
12839
12840 cx.update_editor(|editor, window, cx| {
12841 editor.handle_input("(", window, cx);
12842 });
12843 cx.assert_editor_state(
12844 &"
12845 fn main() {
12846 sample(ˇ)
12847 }
12848 "
12849 .unindent(),
12850 );
12851
12852 let mocked_response = lsp::SignatureHelp {
12853 signatures: vec![lsp::SignatureInformation {
12854 label: "fn sample(param1: u8, param2: u8)".to_string(),
12855 documentation: None,
12856 parameters: Some(vec![
12857 lsp::ParameterInformation {
12858 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12859 documentation: None,
12860 },
12861 lsp::ParameterInformation {
12862 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12863 documentation: None,
12864 },
12865 ]),
12866 active_parameter: None,
12867 }],
12868 active_signature: Some(0),
12869 active_parameter: Some(0),
12870 };
12871 handle_signature_help_request(&mut cx, mocked_response).await;
12872
12873 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12874 .await;
12875
12876 cx.editor(|editor, _, _| {
12877 let signature_help_state = editor.signature_help_state.popover().cloned();
12878 let signature = signature_help_state.unwrap();
12879 assert_eq!(
12880 signature.signatures[signature.current_signature].label,
12881 "fn sample(param1: u8, param2: u8)"
12882 );
12883 });
12884}
12885
12886#[gpui::test]
12887async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
12888 init_test(cx, |_| {});
12889
12890 cx.update(|cx| {
12891 cx.update_global::<SettingsStore, _>(|settings, cx| {
12892 settings.update_user_settings(cx, |settings| {
12893 settings.editor.auto_signature_help = Some(false);
12894 settings.editor.show_signature_help_after_edits = Some(false);
12895 });
12896 });
12897 });
12898
12899 let mut cx = EditorLspTestContext::new_rust(
12900 lsp::ServerCapabilities {
12901 signature_help_provider: Some(lsp::SignatureHelpOptions {
12902 ..Default::default()
12903 }),
12904 ..Default::default()
12905 },
12906 cx,
12907 )
12908 .await;
12909
12910 let language = Language::new(
12911 LanguageConfig {
12912 name: "Rust".into(),
12913 brackets: BracketPairConfig {
12914 pairs: vec![
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: true,
12926 surround: true,
12927 newline: true,
12928 },
12929 BracketPair {
12930 start: "/*".to_string(),
12931 end: " */".to_string(),
12932 close: true,
12933 surround: true,
12934 newline: true,
12935 },
12936 BracketPair {
12937 start: "[".to_string(),
12938 end: "]".to_string(),
12939 close: false,
12940 surround: false,
12941 newline: true,
12942 },
12943 BracketPair {
12944 start: "\"".to_string(),
12945 end: "\"".to_string(),
12946 close: true,
12947 surround: true,
12948 newline: false,
12949 },
12950 BracketPair {
12951 start: "<".to_string(),
12952 end: ">".to_string(),
12953 close: false,
12954 surround: true,
12955 newline: true,
12956 },
12957 ],
12958 ..Default::default()
12959 },
12960 autoclose_before: "})]".to_string(),
12961 ..Default::default()
12962 },
12963 Some(tree_sitter_rust::LANGUAGE.into()),
12964 );
12965 let language = Arc::new(language);
12966
12967 cx.language_registry().add(language.clone());
12968 cx.update_buffer(|buffer, cx| {
12969 buffer.set_language(Some(language), cx);
12970 });
12971
12972 // Ensure that signature_help is not called when no signature help is enabled.
12973 cx.set_state(
12974 &r#"
12975 fn main() {
12976 sampleˇ
12977 }
12978 "#
12979 .unindent(),
12980 );
12981 cx.update_editor(|editor, window, cx| {
12982 editor.handle_input("(", window, cx);
12983 });
12984 cx.assert_editor_state(
12985 &"
12986 fn main() {
12987 sample(ˇ)
12988 }
12989 "
12990 .unindent(),
12991 );
12992 cx.editor(|editor, _, _| {
12993 assert!(editor.signature_help_state.task().is_none());
12994 });
12995
12996 let mocked_response = lsp::SignatureHelp {
12997 signatures: vec![lsp::SignatureInformation {
12998 label: "fn sample(param1: u8, param2: u8)".to_string(),
12999 documentation: None,
13000 parameters: Some(vec![
13001 lsp::ParameterInformation {
13002 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13003 documentation: None,
13004 },
13005 lsp::ParameterInformation {
13006 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13007 documentation: None,
13008 },
13009 ]),
13010 active_parameter: None,
13011 }],
13012 active_signature: Some(0),
13013 active_parameter: Some(0),
13014 };
13015
13016 // Ensure that signature_help is called when enabled afte edits
13017 cx.update(|_, cx| {
13018 cx.update_global::<SettingsStore, _>(|settings, cx| {
13019 settings.update_user_settings(cx, |settings| {
13020 settings.editor.auto_signature_help = Some(false);
13021 settings.editor.show_signature_help_after_edits = Some(true);
13022 });
13023 });
13024 });
13025 cx.set_state(
13026 &r#"
13027 fn main() {
13028 sampleˇ
13029 }
13030 "#
13031 .unindent(),
13032 );
13033 cx.update_editor(|editor, window, cx| {
13034 editor.handle_input("(", window, cx);
13035 });
13036 cx.assert_editor_state(
13037 &"
13038 fn main() {
13039 sample(ˇ)
13040 }
13041 "
13042 .unindent(),
13043 );
13044 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13045 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13046 .await;
13047 cx.update_editor(|editor, _, _| {
13048 let signature_help_state = editor.signature_help_state.popover().cloned();
13049 assert!(signature_help_state.is_some());
13050 let signature = signature_help_state.unwrap();
13051 assert_eq!(
13052 signature.signatures[signature.current_signature].label,
13053 "fn sample(param1: u8, param2: u8)"
13054 );
13055 editor.signature_help_state = SignatureHelpState::default();
13056 });
13057
13058 // Ensure that signature_help is called when auto signature help override is enabled
13059 cx.update(|_, cx| {
13060 cx.update_global::<SettingsStore, _>(|settings, cx| {
13061 settings.update_user_settings(cx, |settings| {
13062 settings.editor.auto_signature_help = Some(true);
13063 settings.editor.show_signature_help_after_edits = Some(false);
13064 });
13065 });
13066 });
13067 cx.set_state(
13068 &r#"
13069 fn main() {
13070 sampleˇ
13071 }
13072 "#
13073 .unindent(),
13074 );
13075 cx.update_editor(|editor, window, cx| {
13076 editor.handle_input("(", window, cx);
13077 });
13078 cx.assert_editor_state(
13079 &"
13080 fn main() {
13081 sample(ˇ)
13082 }
13083 "
13084 .unindent(),
13085 );
13086 handle_signature_help_request(&mut cx, mocked_response).await;
13087 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13088 .await;
13089 cx.editor(|editor, _, _| {
13090 let signature_help_state = editor.signature_help_state.popover().cloned();
13091 assert!(signature_help_state.is_some());
13092 let signature = signature_help_state.unwrap();
13093 assert_eq!(
13094 signature.signatures[signature.current_signature].label,
13095 "fn sample(param1: u8, param2: u8)"
13096 );
13097 });
13098}
13099
13100#[gpui::test]
13101async fn test_signature_help(cx: &mut TestAppContext) {
13102 init_test(cx, |_| {});
13103 cx.update(|cx| {
13104 cx.update_global::<SettingsStore, _>(|settings, cx| {
13105 settings.update_user_settings(cx, |settings| {
13106 settings.editor.auto_signature_help = Some(true);
13107 });
13108 });
13109 });
13110
13111 let mut cx = EditorLspTestContext::new_rust(
13112 lsp::ServerCapabilities {
13113 signature_help_provider: Some(lsp::SignatureHelpOptions {
13114 ..Default::default()
13115 }),
13116 ..Default::default()
13117 },
13118 cx,
13119 )
13120 .await;
13121
13122 // A test that directly calls `show_signature_help`
13123 cx.update_editor(|editor, window, cx| {
13124 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13125 });
13126
13127 let mocked_response = lsp::SignatureHelp {
13128 signatures: vec![lsp::SignatureInformation {
13129 label: "fn sample(param1: u8, param2: u8)".to_string(),
13130 documentation: None,
13131 parameters: Some(vec![
13132 lsp::ParameterInformation {
13133 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13134 documentation: None,
13135 },
13136 lsp::ParameterInformation {
13137 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13138 documentation: None,
13139 },
13140 ]),
13141 active_parameter: None,
13142 }],
13143 active_signature: Some(0),
13144 active_parameter: Some(0),
13145 };
13146 handle_signature_help_request(&mut cx, mocked_response).await;
13147
13148 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13149 .await;
13150
13151 cx.editor(|editor, _, _| {
13152 let signature_help_state = editor.signature_help_state.popover().cloned();
13153 assert!(signature_help_state.is_some());
13154 let signature = signature_help_state.unwrap();
13155 assert_eq!(
13156 signature.signatures[signature.current_signature].label,
13157 "fn sample(param1: u8, param2: u8)"
13158 );
13159 });
13160
13161 // When exiting outside from inside the brackets, `signature_help` is closed.
13162 cx.set_state(indoc! {"
13163 fn main() {
13164 sample(ˇ);
13165 }
13166
13167 fn sample(param1: u8, param2: u8) {}
13168 "});
13169
13170 cx.update_editor(|editor, window, cx| {
13171 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13172 s.select_ranges([0..0])
13173 });
13174 });
13175
13176 let mocked_response = lsp::SignatureHelp {
13177 signatures: Vec::new(),
13178 active_signature: None,
13179 active_parameter: None,
13180 };
13181 handle_signature_help_request(&mut cx, mocked_response).await;
13182
13183 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13184 .await;
13185
13186 cx.editor(|editor, _, _| {
13187 assert!(!editor.signature_help_state.is_shown());
13188 });
13189
13190 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
13191 cx.set_state(indoc! {"
13192 fn main() {
13193 sample(ˇ);
13194 }
13195
13196 fn sample(param1: u8, param2: u8) {}
13197 "});
13198
13199 let mocked_response = lsp::SignatureHelp {
13200 signatures: vec![lsp::SignatureInformation {
13201 label: "fn sample(param1: u8, param2: u8)".to_string(),
13202 documentation: None,
13203 parameters: Some(vec![
13204 lsp::ParameterInformation {
13205 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13206 documentation: None,
13207 },
13208 lsp::ParameterInformation {
13209 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13210 documentation: None,
13211 },
13212 ]),
13213 active_parameter: None,
13214 }],
13215 active_signature: Some(0),
13216 active_parameter: Some(0),
13217 };
13218 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13219 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13220 .await;
13221 cx.editor(|editor, _, _| {
13222 assert!(editor.signature_help_state.is_shown());
13223 });
13224
13225 // Restore the popover with more parameter input
13226 cx.set_state(indoc! {"
13227 fn main() {
13228 sample(param1, param2ˇ);
13229 }
13230
13231 fn sample(param1: u8, param2: u8) {}
13232 "});
13233
13234 let mocked_response = lsp::SignatureHelp {
13235 signatures: vec![lsp::SignatureInformation {
13236 label: "fn sample(param1: u8, param2: u8)".to_string(),
13237 documentation: None,
13238 parameters: Some(vec![
13239 lsp::ParameterInformation {
13240 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13241 documentation: None,
13242 },
13243 lsp::ParameterInformation {
13244 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13245 documentation: None,
13246 },
13247 ]),
13248 active_parameter: None,
13249 }],
13250 active_signature: Some(0),
13251 active_parameter: Some(1),
13252 };
13253 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13254 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13255 .await;
13256
13257 // When selecting a range, the popover is gone.
13258 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
13259 cx.update_editor(|editor, window, cx| {
13260 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13261 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13262 })
13263 });
13264 cx.assert_editor_state(indoc! {"
13265 fn main() {
13266 sample(param1, «ˇparam2»);
13267 }
13268
13269 fn sample(param1: u8, param2: u8) {}
13270 "});
13271 cx.editor(|editor, _, _| {
13272 assert!(!editor.signature_help_state.is_shown());
13273 });
13274
13275 // When unselecting again, the popover is back if within the brackets.
13276 cx.update_editor(|editor, window, cx| {
13277 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13278 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13279 })
13280 });
13281 cx.assert_editor_state(indoc! {"
13282 fn main() {
13283 sample(param1, ˇparam2);
13284 }
13285
13286 fn sample(param1: u8, param2: u8) {}
13287 "});
13288 handle_signature_help_request(&mut cx, mocked_response).await;
13289 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13290 .await;
13291 cx.editor(|editor, _, _| {
13292 assert!(editor.signature_help_state.is_shown());
13293 });
13294
13295 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
13296 cx.update_editor(|editor, window, cx| {
13297 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13298 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
13299 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13300 })
13301 });
13302 cx.assert_editor_state(indoc! {"
13303 fn main() {
13304 sample(param1, ˇparam2);
13305 }
13306
13307 fn sample(param1: u8, param2: u8) {}
13308 "});
13309
13310 let mocked_response = lsp::SignatureHelp {
13311 signatures: vec![lsp::SignatureInformation {
13312 label: "fn sample(param1: u8, param2: u8)".to_string(),
13313 documentation: None,
13314 parameters: Some(vec![
13315 lsp::ParameterInformation {
13316 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13317 documentation: None,
13318 },
13319 lsp::ParameterInformation {
13320 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13321 documentation: None,
13322 },
13323 ]),
13324 active_parameter: None,
13325 }],
13326 active_signature: Some(0),
13327 active_parameter: Some(1),
13328 };
13329 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13330 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13331 .await;
13332 cx.update_editor(|editor, _, cx| {
13333 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
13334 });
13335 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13336 .await;
13337 cx.update_editor(|editor, window, cx| {
13338 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13339 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13340 })
13341 });
13342 cx.assert_editor_state(indoc! {"
13343 fn main() {
13344 sample(param1, «ˇparam2»);
13345 }
13346
13347 fn sample(param1: u8, param2: u8) {}
13348 "});
13349 cx.update_editor(|editor, window, cx| {
13350 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13351 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13352 })
13353 });
13354 cx.assert_editor_state(indoc! {"
13355 fn main() {
13356 sample(param1, ˇparam2);
13357 }
13358
13359 fn sample(param1: u8, param2: u8) {}
13360 "});
13361 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
13362 .await;
13363}
13364
13365#[gpui::test]
13366async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
13367 init_test(cx, |_| {});
13368
13369 let mut cx = EditorLspTestContext::new_rust(
13370 lsp::ServerCapabilities {
13371 signature_help_provider: Some(lsp::SignatureHelpOptions {
13372 ..Default::default()
13373 }),
13374 ..Default::default()
13375 },
13376 cx,
13377 )
13378 .await;
13379
13380 cx.set_state(indoc! {"
13381 fn main() {
13382 overloadedˇ
13383 }
13384 "});
13385
13386 cx.update_editor(|editor, window, cx| {
13387 editor.handle_input("(", window, cx);
13388 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13389 });
13390
13391 // Mock response with 3 signatures
13392 let mocked_response = lsp::SignatureHelp {
13393 signatures: vec![
13394 lsp::SignatureInformation {
13395 label: "fn overloaded(x: i32)".to_string(),
13396 documentation: None,
13397 parameters: Some(vec![lsp::ParameterInformation {
13398 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13399 documentation: None,
13400 }]),
13401 active_parameter: None,
13402 },
13403 lsp::SignatureInformation {
13404 label: "fn overloaded(x: i32, y: i32)".to_string(),
13405 documentation: None,
13406 parameters: Some(vec![
13407 lsp::ParameterInformation {
13408 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13409 documentation: None,
13410 },
13411 lsp::ParameterInformation {
13412 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13413 documentation: None,
13414 },
13415 ]),
13416 active_parameter: None,
13417 },
13418 lsp::SignatureInformation {
13419 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
13420 documentation: None,
13421 parameters: Some(vec![
13422 lsp::ParameterInformation {
13423 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13424 documentation: None,
13425 },
13426 lsp::ParameterInformation {
13427 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13428 documentation: None,
13429 },
13430 lsp::ParameterInformation {
13431 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
13432 documentation: None,
13433 },
13434 ]),
13435 active_parameter: None,
13436 },
13437 ],
13438 active_signature: Some(1),
13439 active_parameter: Some(0),
13440 };
13441 handle_signature_help_request(&mut cx, mocked_response).await;
13442
13443 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13444 .await;
13445
13446 // Verify we have multiple signatures and the right one is selected
13447 cx.editor(|editor, _, _| {
13448 let popover = editor.signature_help_state.popover().cloned().unwrap();
13449 assert_eq!(popover.signatures.len(), 3);
13450 // active_signature was 1, so that should be the current
13451 assert_eq!(popover.current_signature, 1);
13452 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
13453 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
13454 assert_eq!(
13455 popover.signatures[2].label,
13456 "fn overloaded(x: i32, y: i32, z: i32)"
13457 );
13458 });
13459
13460 // Test navigation functionality
13461 cx.update_editor(|editor, window, cx| {
13462 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13463 });
13464
13465 cx.editor(|editor, _, _| {
13466 let popover = editor.signature_help_state.popover().cloned().unwrap();
13467 assert_eq!(popover.current_signature, 2);
13468 });
13469
13470 // Test wrap around
13471 cx.update_editor(|editor, window, cx| {
13472 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13473 });
13474
13475 cx.editor(|editor, _, _| {
13476 let popover = editor.signature_help_state.popover().cloned().unwrap();
13477 assert_eq!(popover.current_signature, 0);
13478 });
13479
13480 // Test previous navigation
13481 cx.update_editor(|editor, window, cx| {
13482 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
13483 });
13484
13485 cx.editor(|editor, _, _| {
13486 let popover = editor.signature_help_state.popover().cloned().unwrap();
13487 assert_eq!(popover.current_signature, 2);
13488 });
13489}
13490
13491#[gpui::test]
13492async fn test_completion_mode(cx: &mut TestAppContext) {
13493 init_test(cx, |_| {});
13494 let mut cx = EditorLspTestContext::new_rust(
13495 lsp::ServerCapabilities {
13496 completion_provider: Some(lsp::CompletionOptions {
13497 resolve_provider: Some(true),
13498 ..Default::default()
13499 }),
13500 ..Default::default()
13501 },
13502 cx,
13503 )
13504 .await;
13505
13506 struct Run {
13507 run_description: &'static str,
13508 initial_state: String,
13509 buffer_marked_text: String,
13510 completion_label: &'static str,
13511 completion_text: &'static str,
13512 expected_with_insert_mode: String,
13513 expected_with_replace_mode: String,
13514 expected_with_replace_subsequence_mode: String,
13515 expected_with_replace_suffix_mode: String,
13516 }
13517
13518 let runs = [
13519 Run {
13520 run_description: "Start of word matches completion text",
13521 initial_state: "before ediˇ after".into(),
13522 buffer_marked_text: "before <edi|> after".into(),
13523 completion_label: "editor",
13524 completion_text: "editor",
13525 expected_with_insert_mode: "before editorˇ after".into(),
13526 expected_with_replace_mode: "before editorˇ after".into(),
13527 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13528 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13529 },
13530 Run {
13531 run_description: "Accept same text at the middle of the word",
13532 initial_state: "before ediˇtor after".into(),
13533 buffer_marked_text: "before <edi|tor> after".into(),
13534 completion_label: "editor",
13535 completion_text: "editor",
13536 expected_with_insert_mode: "before editorˇtor after".into(),
13537 expected_with_replace_mode: "before editorˇ after".into(),
13538 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13539 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13540 },
13541 Run {
13542 run_description: "End of word matches completion text -- cursor at end",
13543 initial_state: "before torˇ after".into(),
13544 buffer_marked_text: "before <tor|> after".into(),
13545 completion_label: "editor",
13546 completion_text: "editor",
13547 expected_with_insert_mode: "before editorˇ after".into(),
13548 expected_with_replace_mode: "before editorˇ after".into(),
13549 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13550 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13551 },
13552 Run {
13553 run_description: "End of word matches completion text -- cursor at start",
13554 initial_state: "before ˇtor after".into(),
13555 buffer_marked_text: "before <|tor> after".into(),
13556 completion_label: "editor",
13557 completion_text: "editor",
13558 expected_with_insert_mode: "before editorˇtor after".into(),
13559 expected_with_replace_mode: "before editorˇ after".into(),
13560 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13561 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13562 },
13563 Run {
13564 run_description: "Prepend text containing whitespace",
13565 initial_state: "pˇfield: bool".into(),
13566 buffer_marked_text: "<p|field>: bool".into(),
13567 completion_label: "pub ",
13568 completion_text: "pub ",
13569 expected_with_insert_mode: "pub ˇfield: bool".into(),
13570 expected_with_replace_mode: "pub ˇ: bool".into(),
13571 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
13572 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
13573 },
13574 Run {
13575 run_description: "Add element to start of list",
13576 initial_state: "[element_ˇelement_2]".into(),
13577 buffer_marked_text: "[<element_|element_2>]".into(),
13578 completion_label: "element_1",
13579 completion_text: "element_1",
13580 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
13581 expected_with_replace_mode: "[element_1ˇ]".into(),
13582 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
13583 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
13584 },
13585 Run {
13586 run_description: "Add element to start of list -- first and second elements are equal",
13587 initial_state: "[elˇelement]".into(),
13588 buffer_marked_text: "[<el|element>]".into(),
13589 completion_label: "element",
13590 completion_text: "element",
13591 expected_with_insert_mode: "[elementˇelement]".into(),
13592 expected_with_replace_mode: "[elementˇ]".into(),
13593 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
13594 expected_with_replace_suffix_mode: "[elementˇ]".into(),
13595 },
13596 Run {
13597 run_description: "Ends with matching suffix",
13598 initial_state: "SubˇError".into(),
13599 buffer_marked_text: "<Sub|Error>".into(),
13600 completion_label: "SubscriptionError",
13601 completion_text: "SubscriptionError",
13602 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
13603 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13604 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13605 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
13606 },
13607 Run {
13608 run_description: "Suffix is a subsequence -- contiguous",
13609 initial_state: "SubˇErr".into(),
13610 buffer_marked_text: "<Sub|Err>".into(),
13611 completion_label: "SubscriptionError",
13612 completion_text: "SubscriptionError",
13613 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
13614 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13615 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13616 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
13617 },
13618 Run {
13619 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
13620 initial_state: "Suˇscrirr".into(),
13621 buffer_marked_text: "<Su|scrirr>".into(),
13622 completion_label: "SubscriptionError",
13623 completion_text: "SubscriptionError",
13624 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
13625 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13626 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13627 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
13628 },
13629 Run {
13630 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
13631 initial_state: "foo(indˇix)".into(),
13632 buffer_marked_text: "foo(<ind|ix>)".into(),
13633 completion_label: "node_index",
13634 completion_text: "node_index",
13635 expected_with_insert_mode: "foo(node_indexˇix)".into(),
13636 expected_with_replace_mode: "foo(node_indexˇ)".into(),
13637 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
13638 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
13639 },
13640 Run {
13641 run_description: "Replace range ends before cursor - should extend to cursor",
13642 initial_state: "before editˇo after".into(),
13643 buffer_marked_text: "before <{ed}>it|o after".into(),
13644 completion_label: "editor",
13645 completion_text: "editor",
13646 expected_with_insert_mode: "before editorˇo after".into(),
13647 expected_with_replace_mode: "before editorˇo after".into(),
13648 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
13649 expected_with_replace_suffix_mode: "before editorˇo after".into(),
13650 },
13651 Run {
13652 run_description: "Uses label for suffix matching",
13653 initial_state: "before ediˇtor after".into(),
13654 buffer_marked_text: "before <edi|tor> after".into(),
13655 completion_label: "editor",
13656 completion_text: "editor()",
13657 expected_with_insert_mode: "before editor()ˇtor after".into(),
13658 expected_with_replace_mode: "before editor()ˇ after".into(),
13659 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
13660 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
13661 },
13662 Run {
13663 run_description: "Case insensitive subsequence and suffix matching",
13664 initial_state: "before EDiˇtoR after".into(),
13665 buffer_marked_text: "before <EDi|toR> after".into(),
13666 completion_label: "editor",
13667 completion_text: "editor",
13668 expected_with_insert_mode: "before editorˇtoR after".into(),
13669 expected_with_replace_mode: "before editorˇ after".into(),
13670 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13671 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13672 },
13673 ];
13674
13675 for run in runs {
13676 let run_variations = [
13677 (LspInsertMode::Insert, run.expected_with_insert_mode),
13678 (LspInsertMode::Replace, run.expected_with_replace_mode),
13679 (
13680 LspInsertMode::ReplaceSubsequence,
13681 run.expected_with_replace_subsequence_mode,
13682 ),
13683 (
13684 LspInsertMode::ReplaceSuffix,
13685 run.expected_with_replace_suffix_mode,
13686 ),
13687 ];
13688
13689 for (lsp_insert_mode, expected_text) in run_variations {
13690 eprintln!(
13691 "run = {:?}, mode = {lsp_insert_mode:.?}",
13692 run.run_description,
13693 );
13694
13695 update_test_language_settings(&mut cx, |settings| {
13696 settings.defaults.completions = Some(CompletionSettingsContent {
13697 lsp_insert_mode: Some(lsp_insert_mode),
13698 words: Some(WordsCompletionMode::Disabled),
13699 words_min_length: Some(0),
13700 ..Default::default()
13701 });
13702 });
13703
13704 cx.set_state(&run.initial_state);
13705 cx.update_editor(|editor, window, cx| {
13706 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13707 });
13708
13709 let counter = Arc::new(AtomicUsize::new(0));
13710 handle_completion_request_with_insert_and_replace(
13711 &mut cx,
13712 &run.buffer_marked_text,
13713 vec![(run.completion_label, run.completion_text)],
13714 counter.clone(),
13715 )
13716 .await;
13717 cx.condition(|editor, _| editor.context_menu_visible())
13718 .await;
13719 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13720
13721 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13722 editor
13723 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13724 .unwrap()
13725 });
13726 cx.assert_editor_state(&expected_text);
13727 handle_resolve_completion_request(&mut cx, None).await;
13728 apply_additional_edits.await.unwrap();
13729 }
13730 }
13731}
13732
13733#[gpui::test]
13734async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
13735 init_test(cx, |_| {});
13736 let mut cx = EditorLspTestContext::new_rust(
13737 lsp::ServerCapabilities {
13738 completion_provider: Some(lsp::CompletionOptions {
13739 resolve_provider: Some(true),
13740 ..Default::default()
13741 }),
13742 ..Default::default()
13743 },
13744 cx,
13745 )
13746 .await;
13747
13748 let initial_state = "SubˇError";
13749 let buffer_marked_text = "<Sub|Error>";
13750 let completion_text = "SubscriptionError";
13751 let expected_with_insert_mode = "SubscriptionErrorˇError";
13752 let expected_with_replace_mode = "SubscriptionErrorˇ";
13753
13754 update_test_language_settings(&mut cx, |settings| {
13755 settings.defaults.completions = Some(CompletionSettingsContent {
13756 words: Some(WordsCompletionMode::Disabled),
13757 words_min_length: Some(0),
13758 // set the opposite here to ensure that the action is overriding the default behavior
13759 lsp_insert_mode: Some(LspInsertMode::Insert),
13760 ..Default::default()
13761 });
13762 });
13763
13764 cx.set_state(initial_state);
13765 cx.update_editor(|editor, window, cx| {
13766 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13767 });
13768
13769 let counter = Arc::new(AtomicUsize::new(0));
13770 handle_completion_request_with_insert_and_replace(
13771 &mut cx,
13772 buffer_marked_text,
13773 vec![(completion_text, completion_text)],
13774 counter.clone(),
13775 )
13776 .await;
13777 cx.condition(|editor, _| editor.context_menu_visible())
13778 .await;
13779 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13780
13781 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13782 editor
13783 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13784 .unwrap()
13785 });
13786 cx.assert_editor_state(expected_with_replace_mode);
13787 handle_resolve_completion_request(&mut cx, None).await;
13788 apply_additional_edits.await.unwrap();
13789
13790 update_test_language_settings(&mut cx, |settings| {
13791 settings.defaults.completions = Some(CompletionSettingsContent {
13792 words: Some(WordsCompletionMode::Disabled),
13793 words_min_length: Some(0),
13794 // set the opposite here to ensure that the action is overriding the default behavior
13795 lsp_insert_mode: Some(LspInsertMode::Replace),
13796 ..Default::default()
13797 });
13798 });
13799
13800 cx.set_state(initial_state);
13801 cx.update_editor(|editor, window, cx| {
13802 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13803 });
13804 handle_completion_request_with_insert_and_replace(
13805 &mut cx,
13806 buffer_marked_text,
13807 vec![(completion_text, completion_text)],
13808 counter.clone(),
13809 )
13810 .await;
13811 cx.condition(|editor, _| editor.context_menu_visible())
13812 .await;
13813 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
13814
13815 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13816 editor
13817 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
13818 .unwrap()
13819 });
13820 cx.assert_editor_state(expected_with_insert_mode);
13821 handle_resolve_completion_request(&mut cx, None).await;
13822 apply_additional_edits.await.unwrap();
13823}
13824
13825#[gpui::test]
13826async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
13827 init_test(cx, |_| {});
13828 let mut cx = EditorLspTestContext::new_rust(
13829 lsp::ServerCapabilities {
13830 completion_provider: Some(lsp::CompletionOptions {
13831 resolve_provider: Some(true),
13832 ..Default::default()
13833 }),
13834 ..Default::default()
13835 },
13836 cx,
13837 )
13838 .await;
13839
13840 // scenario: surrounding text matches completion text
13841 let completion_text = "to_offset";
13842 let initial_state = indoc! {"
13843 1. buf.to_offˇsuffix
13844 2. buf.to_offˇsuf
13845 3. buf.to_offˇfix
13846 4. buf.to_offˇ
13847 5. into_offˇensive
13848 6. ˇsuffix
13849 7. let ˇ //
13850 8. aaˇzz
13851 9. buf.to_off«zzzzzˇ»suffix
13852 10. buf.«ˇzzzzz»suffix
13853 11. to_off«ˇzzzzz»
13854
13855 buf.to_offˇsuffix // newest cursor
13856 "};
13857 let completion_marked_buffer = indoc! {"
13858 1. buf.to_offsuffix
13859 2. buf.to_offsuf
13860 3. buf.to_offfix
13861 4. buf.to_off
13862 5. into_offensive
13863 6. suffix
13864 7. let //
13865 8. aazz
13866 9. buf.to_offzzzzzsuffix
13867 10. buf.zzzzzsuffix
13868 11. to_offzzzzz
13869
13870 buf.<to_off|suffix> // newest cursor
13871 "};
13872 let expected = indoc! {"
13873 1. buf.to_offsetˇ
13874 2. buf.to_offsetˇsuf
13875 3. buf.to_offsetˇfix
13876 4. buf.to_offsetˇ
13877 5. into_offsetˇensive
13878 6. to_offsetˇsuffix
13879 7. let to_offsetˇ //
13880 8. aato_offsetˇzz
13881 9. buf.to_offsetˇ
13882 10. buf.to_offsetˇsuffix
13883 11. to_offsetˇ
13884
13885 buf.to_offsetˇ // newest cursor
13886 "};
13887 cx.set_state(initial_state);
13888 cx.update_editor(|editor, window, cx| {
13889 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13890 });
13891 handle_completion_request_with_insert_and_replace(
13892 &mut cx,
13893 completion_marked_buffer,
13894 vec![(completion_text, completion_text)],
13895 Arc::new(AtomicUsize::new(0)),
13896 )
13897 .await;
13898 cx.condition(|editor, _| editor.context_menu_visible())
13899 .await;
13900 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13901 editor
13902 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13903 .unwrap()
13904 });
13905 cx.assert_editor_state(expected);
13906 handle_resolve_completion_request(&mut cx, None).await;
13907 apply_additional_edits.await.unwrap();
13908
13909 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
13910 let completion_text = "foo_and_bar";
13911 let initial_state = indoc! {"
13912 1. ooanbˇ
13913 2. zooanbˇ
13914 3. ooanbˇz
13915 4. zooanbˇz
13916 5. ooanˇ
13917 6. oanbˇ
13918
13919 ooanbˇ
13920 "};
13921 let completion_marked_buffer = indoc! {"
13922 1. ooanb
13923 2. zooanb
13924 3. ooanbz
13925 4. zooanbz
13926 5. ooan
13927 6. oanb
13928
13929 <ooanb|>
13930 "};
13931 let expected = indoc! {"
13932 1. foo_and_barˇ
13933 2. zfoo_and_barˇ
13934 3. foo_and_barˇz
13935 4. zfoo_and_barˇz
13936 5. ooanfoo_and_barˇ
13937 6. oanbfoo_and_barˇ
13938
13939 foo_and_barˇ
13940 "};
13941 cx.set_state(initial_state);
13942 cx.update_editor(|editor, window, cx| {
13943 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13944 });
13945 handle_completion_request_with_insert_and_replace(
13946 &mut cx,
13947 completion_marked_buffer,
13948 vec![(completion_text, completion_text)],
13949 Arc::new(AtomicUsize::new(0)),
13950 )
13951 .await;
13952 cx.condition(|editor, _| editor.context_menu_visible())
13953 .await;
13954 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13955 editor
13956 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13957 .unwrap()
13958 });
13959 cx.assert_editor_state(expected);
13960 handle_resolve_completion_request(&mut cx, None).await;
13961 apply_additional_edits.await.unwrap();
13962
13963 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
13964 // (expects the same as if it was inserted at the end)
13965 let completion_text = "foo_and_bar";
13966 let initial_state = indoc! {"
13967 1. ooˇanb
13968 2. zooˇanb
13969 3. ooˇanbz
13970 4. zooˇanbz
13971
13972 ooˇanb
13973 "};
13974 let completion_marked_buffer = indoc! {"
13975 1. ooanb
13976 2. zooanb
13977 3. ooanbz
13978 4. zooanbz
13979
13980 <oo|anb>
13981 "};
13982 let expected = indoc! {"
13983 1. foo_and_barˇ
13984 2. zfoo_and_barˇ
13985 3. foo_and_barˇz
13986 4. zfoo_and_barˇz
13987
13988 foo_and_barˇ
13989 "};
13990 cx.set_state(initial_state);
13991 cx.update_editor(|editor, window, cx| {
13992 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13993 });
13994 handle_completion_request_with_insert_and_replace(
13995 &mut cx,
13996 completion_marked_buffer,
13997 vec![(completion_text, completion_text)],
13998 Arc::new(AtomicUsize::new(0)),
13999 )
14000 .await;
14001 cx.condition(|editor, _| editor.context_menu_visible())
14002 .await;
14003 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14004 editor
14005 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14006 .unwrap()
14007 });
14008 cx.assert_editor_state(expected);
14009 handle_resolve_completion_request(&mut cx, None).await;
14010 apply_additional_edits.await.unwrap();
14011}
14012
14013// This used to crash
14014#[gpui::test]
14015async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
14016 init_test(cx, |_| {});
14017
14018 let buffer_text = indoc! {"
14019 fn main() {
14020 10.satu;
14021
14022 //
14023 // separate cursors so they open in different excerpts (manually reproducible)
14024 //
14025
14026 10.satu20;
14027 }
14028 "};
14029 let multibuffer_text_with_selections = indoc! {"
14030 fn main() {
14031 10.satuˇ;
14032
14033 //
14034
14035 //
14036
14037 10.satuˇ20;
14038 }
14039 "};
14040 let expected_multibuffer = indoc! {"
14041 fn main() {
14042 10.saturating_sub()ˇ;
14043
14044 //
14045
14046 //
14047
14048 10.saturating_sub()ˇ;
14049 }
14050 "};
14051
14052 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
14053 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
14054
14055 let fs = FakeFs::new(cx.executor());
14056 fs.insert_tree(
14057 path!("/a"),
14058 json!({
14059 "main.rs": buffer_text,
14060 }),
14061 )
14062 .await;
14063
14064 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14065 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14066 language_registry.add(rust_lang());
14067 let mut fake_servers = language_registry.register_fake_lsp(
14068 "Rust",
14069 FakeLspAdapter {
14070 capabilities: lsp::ServerCapabilities {
14071 completion_provider: Some(lsp::CompletionOptions {
14072 resolve_provider: None,
14073 ..lsp::CompletionOptions::default()
14074 }),
14075 ..lsp::ServerCapabilities::default()
14076 },
14077 ..FakeLspAdapter::default()
14078 },
14079 );
14080 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14081 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14082 let buffer = project
14083 .update(cx, |project, cx| {
14084 project.open_local_buffer(path!("/a/main.rs"), cx)
14085 })
14086 .await
14087 .unwrap();
14088
14089 let multi_buffer = cx.new(|cx| {
14090 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
14091 multi_buffer.push_excerpts(
14092 buffer.clone(),
14093 [ExcerptRange::new(0..first_excerpt_end)],
14094 cx,
14095 );
14096 multi_buffer.push_excerpts(
14097 buffer.clone(),
14098 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
14099 cx,
14100 );
14101 multi_buffer
14102 });
14103
14104 let editor = workspace
14105 .update(cx, |_, window, cx| {
14106 cx.new(|cx| {
14107 Editor::new(
14108 EditorMode::Full {
14109 scale_ui_elements_with_buffer_font_size: false,
14110 show_active_line_background: false,
14111 sizing_behavior: SizingBehavior::Default,
14112 },
14113 multi_buffer.clone(),
14114 Some(project.clone()),
14115 window,
14116 cx,
14117 )
14118 })
14119 })
14120 .unwrap();
14121
14122 let pane = workspace
14123 .update(cx, |workspace, _, _| workspace.active_pane().clone())
14124 .unwrap();
14125 pane.update_in(cx, |pane, window, cx| {
14126 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
14127 });
14128
14129 let fake_server = fake_servers.next().await.unwrap();
14130
14131 editor.update_in(cx, |editor, window, cx| {
14132 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14133 s.select_ranges([
14134 Point::new(1, 11)..Point::new(1, 11),
14135 Point::new(7, 11)..Point::new(7, 11),
14136 ])
14137 });
14138
14139 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
14140 });
14141
14142 editor.update_in(cx, |editor, window, cx| {
14143 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14144 });
14145
14146 fake_server
14147 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14148 let completion_item = lsp::CompletionItem {
14149 label: "saturating_sub()".into(),
14150 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
14151 lsp::InsertReplaceEdit {
14152 new_text: "saturating_sub()".to_owned(),
14153 insert: lsp::Range::new(
14154 lsp::Position::new(7, 7),
14155 lsp::Position::new(7, 11),
14156 ),
14157 replace: lsp::Range::new(
14158 lsp::Position::new(7, 7),
14159 lsp::Position::new(7, 13),
14160 ),
14161 },
14162 )),
14163 ..lsp::CompletionItem::default()
14164 };
14165
14166 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
14167 })
14168 .next()
14169 .await
14170 .unwrap();
14171
14172 cx.condition(&editor, |editor, _| editor.context_menu_visible())
14173 .await;
14174
14175 editor
14176 .update_in(cx, |editor, window, cx| {
14177 editor
14178 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14179 .unwrap()
14180 })
14181 .await
14182 .unwrap();
14183
14184 editor.update(cx, |editor, cx| {
14185 assert_text_with_selections(editor, expected_multibuffer, cx);
14186 })
14187}
14188
14189#[gpui::test]
14190async fn test_completion(cx: &mut TestAppContext) {
14191 init_test(cx, |_| {});
14192
14193 let mut cx = EditorLspTestContext::new_rust(
14194 lsp::ServerCapabilities {
14195 completion_provider: Some(lsp::CompletionOptions {
14196 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14197 resolve_provider: Some(true),
14198 ..Default::default()
14199 }),
14200 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14201 ..Default::default()
14202 },
14203 cx,
14204 )
14205 .await;
14206 let counter = Arc::new(AtomicUsize::new(0));
14207
14208 cx.set_state(indoc! {"
14209 oneˇ
14210 two
14211 three
14212 "});
14213 cx.simulate_keystroke(".");
14214 handle_completion_request(
14215 indoc! {"
14216 one.|<>
14217 two
14218 three
14219 "},
14220 vec!["first_completion", "second_completion"],
14221 true,
14222 counter.clone(),
14223 &mut cx,
14224 )
14225 .await;
14226 cx.condition(|editor, _| editor.context_menu_visible())
14227 .await;
14228 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14229
14230 let _handler = handle_signature_help_request(
14231 &mut cx,
14232 lsp::SignatureHelp {
14233 signatures: vec![lsp::SignatureInformation {
14234 label: "test signature".to_string(),
14235 documentation: None,
14236 parameters: Some(vec![lsp::ParameterInformation {
14237 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
14238 documentation: None,
14239 }]),
14240 active_parameter: None,
14241 }],
14242 active_signature: None,
14243 active_parameter: None,
14244 },
14245 );
14246 cx.update_editor(|editor, window, cx| {
14247 assert!(
14248 !editor.signature_help_state.is_shown(),
14249 "No signature help was called for"
14250 );
14251 editor.show_signature_help(&ShowSignatureHelp, window, cx);
14252 });
14253 cx.run_until_parked();
14254 cx.update_editor(|editor, _, _| {
14255 assert!(
14256 !editor.signature_help_state.is_shown(),
14257 "No signature help should be shown when completions menu is open"
14258 );
14259 });
14260
14261 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14262 editor.context_menu_next(&Default::default(), window, cx);
14263 editor
14264 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14265 .unwrap()
14266 });
14267 cx.assert_editor_state(indoc! {"
14268 one.second_completionˇ
14269 two
14270 three
14271 "});
14272
14273 handle_resolve_completion_request(
14274 &mut cx,
14275 Some(vec![
14276 (
14277 //This overlaps with the primary completion edit which is
14278 //misbehavior from the LSP spec, test that we filter it out
14279 indoc! {"
14280 one.second_ˇcompletion
14281 two
14282 threeˇ
14283 "},
14284 "overlapping additional edit",
14285 ),
14286 (
14287 indoc! {"
14288 one.second_completion
14289 two
14290 threeˇ
14291 "},
14292 "\nadditional edit",
14293 ),
14294 ]),
14295 )
14296 .await;
14297 apply_additional_edits.await.unwrap();
14298 cx.assert_editor_state(indoc! {"
14299 one.second_completionˇ
14300 two
14301 three
14302 additional edit
14303 "});
14304
14305 cx.set_state(indoc! {"
14306 one.second_completion
14307 twoˇ
14308 threeˇ
14309 additional edit
14310 "});
14311 cx.simulate_keystroke(" ");
14312 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14313 cx.simulate_keystroke("s");
14314 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14315
14316 cx.assert_editor_state(indoc! {"
14317 one.second_completion
14318 two sˇ
14319 three sˇ
14320 additional edit
14321 "});
14322 handle_completion_request(
14323 indoc! {"
14324 one.second_completion
14325 two s
14326 three <s|>
14327 additional edit
14328 "},
14329 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14330 true,
14331 counter.clone(),
14332 &mut cx,
14333 )
14334 .await;
14335 cx.condition(|editor, _| editor.context_menu_visible())
14336 .await;
14337 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14338
14339 cx.simulate_keystroke("i");
14340
14341 handle_completion_request(
14342 indoc! {"
14343 one.second_completion
14344 two si
14345 three <si|>
14346 additional edit
14347 "},
14348 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14349 true,
14350 counter.clone(),
14351 &mut cx,
14352 )
14353 .await;
14354 cx.condition(|editor, _| editor.context_menu_visible())
14355 .await;
14356 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14357
14358 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14359 editor
14360 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14361 .unwrap()
14362 });
14363 cx.assert_editor_state(indoc! {"
14364 one.second_completion
14365 two sixth_completionˇ
14366 three sixth_completionˇ
14367 additional edit
14368 "});
14369
14370 apply_additional_edits.await.unwrap();
14371
14372 update_test_language_settings(&mut cx, |settings| {
14373 settings.defaults.show_completions_on_input = Some(false);
14374 });
14375 cx.set_state("editorˇ");
14376 cx.simulate_keystroke(".");
14377 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14378 cx.simulate_keystrokes("c l o");
14379 cx.assert_editor_state("editor.cloˇ");
14380 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14381 cx.update_editor(|editor, window, cx| {
14382 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14383 });
14384 handle_completion_request(
14385 "editor.<clo|>",
14386 vec!["close", "clobber"],
14387 true,
14388 counter.clone(),
14389 &mut cx,
14390 )
14391 .await;
14392 cx.condition(|editor, _| editor.context_menu_visible())
14393 .await;
14394 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
14395
14396 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14397 editor
14398 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14399 .unwrap()
14400 });
14401 cx.assert_editor_state("editor.clobberˇ");
14402 handle_resolve_completion_request(&mut cx, None).await;
14403 apply_additional_edits.await.unwrap();
14404}
14405
14406#[gpui::test]
14407async fn test_completion_reuse(cx: &mut TestAppContext) {
14408 init_test(cx, |_| {});
14409
14410 let mut cx = EditorLspTestContext::new_rust(
14411 lsp::ServerCapabilities {
14412 completion_provider: Some(lsp::CompletionOptions {
14413 trigger_characters: Some(vec![".".to_string()]),
14414 ..Default::default()
14415 }),
14416 ..Default::default()
14417 },
14418 cx,
14419 )
14420 .await;
14421
14422 let counter = Arc::new(AtomicUsize::new(0));
14423 cx.set_state("objˇ");
14424 cx.simulate_keystroke(".");
14425
14426 // Initial completion request returns complete results
14427 let is_incomplete = false;
14428 handle_completion_request(
14429 "obj.|<>",
14430 vec!["a", "ab", "abc"],
14431 is_incomplete,
14432 counter.clone(),
14433 &mut cx,
14434 )
14435 .await;
14436 cx.run_until_parked();
14437 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14438 cx.assert_editor_state("obj.ˇ");
14439 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14440
14441 // Type "a" - filters existing completions
14442 cx.simulate_keystroke("a");
14443 cx.run_until_parked();
14444 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14445 cx.assert_editor_state("obj.aˇ");
14446 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14447
14448 // Type "b" - filters existing completions
14449 cx.simulate_keystroke("b");
14450 cx.run_until_parked();
14451 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14452 cx.assert_editor_state("obj.abˇ");
14453 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14454
14455 // Type "c" - filters existing completions
14456 cx.simulate_keystroke("c");
14457 cx.run_until_parked();
14458 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14459 cx.assert_editor_state("obj.abcˇ");
14460 check_displayed_completions(vec!["abc"], &mut cx);
14461
14462 // Backspace to delete "c" - filters existing completions
14463 cx.update_editor(|editor, window, cx| {
14464 editor.backspace(&Backspace, window, cx);
14465 });
14466 cx.run_until_parked();
14467 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14468 cx.assert_editor_state("obj.abˇ");
14469 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14470
14471 // Moving cursor to the left dismisses menu.
14472 cx.update_editor(|editor, window, cx| {
14473 editor.move_left(&MoveLeft, window, cx);
14474 });
14475 cx.run_until_parked();
14476 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14477 cx.assert_editor_state("obj.aˇb");
14478 cx.update_editor(|editor, _, _| {
14479 assert_eq!(editor.context_menu_visible(), false);
14480 });
14481
14482 // Type "b" - new request
14483 cx.simulate_keystroke("b");
14484 let is_incomplete = false;
14485 handle_completion_request(
14486 "obj.<ab|>a",
14487 vec!["ab", "abc"],
14488 is_incomplete,
14489 counter.clone(),
14490 &mut cx,
14491 )
14492 .await;
14493 cx.run_until_parked();
14494 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14495 cx.assert_editor_state("obj.abˇb");
14496 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14497
14498 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
14499 cx.update_editor(|editor, window, cx| {
14500 editor.backspace(&Backspace, window, cx);
14501 });
14502 let is_incomplete = false;
14503 handle_completion_request(
14504 "obj.<a|>b",
14505 vec!["a", "ab", "abc"],
14506 is_incomplete,
14507 counter.clone(),
14508 &mut cx,
14509 )
14510 .await;
14511 cx.run_until_parked();
14512 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14513 cx.assert_editor_state("obj.aˇb");
14514 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14515
14516 // Backspace to delete "a" - dismisses menu.
14517 cx.update_editor(|editor, window, cx| {
14518 editor.backspace(&Backspace, window, cx);
14519 });
14520 cx.run_until_parked();
14521 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14522 cx.assert_editor_state("obj.ˇb");
14523 cx.update_editor(|editor, _, _| {
14524 assert_eq!(editor.context_menu_visible(), false);
14525 });
14526}
14527
14528#[gpui::test]
14529async fn test_word_completion(cx: &mut TestAppContext) {
14530 let lsp_fetch_timeout_ms = 10;
14531 init_test(cx, |language_settings| {
14532 language_settings.defaults.completions = Some(CompletionSettingsContent {
14533 words_min_length: Some(0),
14534 lsp_fetch_timeout_ms: Some(10),
14535 lsp_insert_mode: Some(LspInsertMode::Insert),
14536 ..Default::default()
14537 });
14538 });
14539
14540 let mut cx = EditorLspTestContext::new_rust(
14541 lsp::ServerCapabilities {
14542 completion_provider: Some(lsp::CompletionOptions {
14543 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14544 ..lsp::CompletionOptions::default()
14545 }),
14546 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14547 ..lsp::ServerCapabilities::default()
14548 },
14549 cx,
14550 )
14551 .await;
14552
14553 let throttle_completions = Arc::new(AtomicBool::new(false));
14554
14555 let lsp_throttle_completions = throttle_completions.clone();
14556 let _completion_requests_handler =
14557 cx.lsp
14558 .server
14559 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
14560 let lsp_throttle_completions = lsp_throttle_completions.clone();
14561 let cx = cx.clone();
14562 async move {
14563 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
14564 cx.background_executor()
14565 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
14566 .await;
14567 }
14568 Ok(Some(lsp::CompletionResponse::Array(vec![
14569 lsp::CompletionItem {
14570 label: "first".into(),
14571 ..lsp::CompletionItem::default()
14572 },
14573 lsp::CompletionItem {
14574 label: "last".into(),
14575 ..lsp::CompletionItem::default()
14576 },
14577 ])))
14578 }
14579 });
14580
14581 cx.set_state(indoc! {"
14582 oneˇ
14583 two
14584 three
14585 "});
14586 cx.simulate_keystroke(".");
14587 cx.executor().run_until_parked();
14588 cx.condition(|editor, _| editor.context_menu_visible())
14589 .await;
14590 cx.update_editor(|editor, window, cx| {
14591 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14592 {
14593 assert_eq!(
14594 completion_menu_entries(menu),
14595 &["first", "last"],
14596 "When LSP server is fast to reply, no fallback word completions are used"
14597 );
14598 } else {
14599 panic!("expected completion menu to be open");
14600 }
14601 editor.cancel(&Cancel, window, cx);
14602 });
14603 cx.executor().run_until_parked();
14604 cx.condition(|editor, _| !editor.context_menu_visible())
14605 .await;
14606
14607 throttle_completions.store(true, atomic::Ordering::Release);
14608 cx.simulate_keystroke(".");
14609 cx.executor()
14610 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
14611 cx.executor().run_until_parked();
14612 cx.condition(|editor, _| editor.context_menu_visible())
14613 .await;
14614 cx.update_editor(|editor, _, _| {
14615 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14616 {
14617 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
14618 "When LSP server is slow, document words can be shown instead, if configured accordingly");
14619 } else {
14620 panic!("expected completion menu to be open");
14621 }
14622 });
14623}
14624
14625#[gpui::test]
14626async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
14627 init_test(cx, |language_settings| {
14628 language_settings.defaults.completions = Some(CompletionSettingsContent {
14629 words: Some(WordsCompletionMode::Enabled),
14630 words_min_length: Some(0),
14631 lsp_insert_mode: Some(LspInsertMode::Insert),
14632 ..Default::default()
14633 });
14634 });
14635
14636 let mut cx = EditorLspTestContext::new_rust(
14637 lsp::ServerCapabilities {
14638 completion_provider: Some(lsp::CompletionOptions {
14639 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14640 ..lsp::CompletionOptions::default()
14641 }),
14642 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14643 ..lsp::ServerCapabilities::default()
14644 },
14645 cx,
14646 )
14647 .await;
14648
14649 let _completion_requests_handler =
14650 cx.lsp
14651 .server
14652 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14653 Ok(Some(lsp::CompletionResponse::Array(vec![
14654 lsp::CompletionItem {
14655 label: "first".into(),
14656 ..lsp::CompletionItem::default()
14657 },
14658 lsp::CompletionItem {
14659 label: "last".into(),
14660 ..lsp::CompletionItem::default()
14661 },
14662 ])))
14663 });
14664
14665 cx.set_state(indoc! {"ˇ
14666 first
14667 last
14668 second
14669 "});
14670 cx.simulate_keystroke(".");
14671 cx.executor().run_until_parked();
14672 cx.condition(|editor, _| editor.context_menu_visible())
14673 .await;
14674 cx.update_editor(|editor, _, _| {
14675 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14676 {
14677 assert_eq!(
14678 completion_menu_entries(menu),
14679 &["first", "last", "second"],
14680 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
14681 );
14682 } else {
14683 panic!("expected completion menu to be open");
14684 }
14685 });
14686}
14687
14688#[gpui::test]
14689async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
14690 init_test(cx, |language_settings| {
14691 language_settings.defaults.completions = Some(CompletionSettingsContent {
14692 words: Some(WordsCompletionMode::Disabled),
14693 words_min_length: Some(0),
14694 lsp_insert_mode: Some(LspInsertMode::Insert),
14695 ..Default::default()
14696 });
14697 });
14698
14699 let mut cx = EditorLspTestContext::new_rust(
14700 lsp::ServerCapabilities {
14701 completion_provider: Some(lsp::CompletionOptions {
14702 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14703 ..lsp::CompletionOptions::default()
14704 }),
14705 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14706 ..lsp::ServerCapabilities::default()
14707 },
14708 cx,
14709 )
14710 .await;
14711
14712 let _completion_requests_handler =
14713 cx.lsp
14714 .server
14715 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14716 panic!("LSP completions should not be queried when dealing with word completions")
14717 });
14718
14719 cx.set_state(indoc! {"ˇ
14720 first
14721 last
14722 second
14723 "});
14724 cx.update_editor(|editor, window, cx| {
14725 editor.show_word_completions(&ShowWordCompletions, window, cx);
14726 });
14727 cx.executor().run_until_parked();
14728 cx.condition(|editor, _| editor.context_menu_visible())
14729 .await;
14730 cx.update_editor(|editor, _, _| {
14731 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14732 {
14733 assert_eq!(
14734 completion_menu_entries(menu),
14735 &["first", "last", "second"],
14736 "`ShowWordCompletions` action should show word completions"
14737 );
14738 } else {
14739 panic!("expected completion menu to be open");
14740 }
14741 });
14742
14743 cx.simulate_keystroke("l");
14744 cx.executor().run_until_parked();
14745 cx.condition(|editor, _| editor.context_menu_visible())
14746 .await;
14747 cx.update_editor(|editor, _, _| {
14748 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14749 {
14750 assert_eq!(
14751 completion_menu_entries(menu),
14752 &["last"],
14753 "After showing word completions, further editing should filter them and not query the LSP"
14754 );
14755 } else {
14756 panic!("expected completion menu to be open");
14757 }
14758 });
14759}
14760
14761#[gpui::test]
14762async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
14763 init_test(cx, |language_settings| {
14764 language_settings.defaults.completions = Some(CompletionSettingsContent {
14765 words_min_length: Some(0),
14766 lsp: Some(false),
14767 lsp_insert_mode: Some(LspInsertMode::Insert),
14768 ..Default::default()
14769 });
14770 });
14771
14772 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14773
14774 cx.set_state(indoc! {"ˇ
14775 0_usize
14776 let
14777 33
14778 4.5f32
14779 "});
14780 cx.update_editor(|editor, window, cx| {
14781 editor.show_completions(&ShowCompletions::default(), window, cx);
14782 });
14783 cx.executor().run_until_parked();
14784 cx.condition(|editor, _| editor.context_menu_visible())
14785 .await;
14786 cx.update_editor(|editor, window, cx| {
14787 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14788 {
14789 assert_eq!(
14790 completion_menu_entries(menu),
14791 &["let"],
14792 "With no digits in the completion query, no digits should be in the word completions"
14793 );
14794 } else {
14795 panic!("expected completion menu to be open");
14796 }
14797 editor.cancel(&Cancel, window, cx);
14798 });
14799
14800 cx.set_state(indoc! {"3ˇ
14801 0_usize
14802 let
14803 3
14804 33.35f32
14805 "});
14806 cx.update_editor(|editor, window, cx| {
14807 editor.show_completions(&ShowCompletions::default(), window, cx);
14808 });
14809 cx.executor().run_until_parked();
14810 cx.condition(|editor, _| editor.context_menu_visible())
14811 .await;
14812 cx.update_editor(|editor, _, _| {
14813 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14814 {
14815 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
14816 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
14817 } else {
14818 panic!("expected completion menu to be open");
14819 }
14820 });
14821}
14822
14823#[gpui::test]
14824async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
14825 init_test(cx, |language_settings| {
14826 language_settings.defaults.completions = Some(CompletionSettingsContent {
14827 words: Some(WordsCompletionMode::Enabled),
14828 words_min_length: Some(3),
14829 lsp_insert_mode: Some(LspInsertMode::Insert),
14830 ..Default::default()
14831 });
14832 });
14833
14834 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14835 cx.set_state(indoc! {"ˇ
14836 wow
14837 wowen
14838 wowser
14839 "});
14840 cx.simulate_keystroke("w");
14841 cx.executor().run_until_parked();
14842 cx.update_editor(|editor, _, _| {
14843 if editor.context_menu.borrow_mut().is_some() {
14844 panic!(
14845 "expected completion menu to be hidden, as words completion threshold is not met"
14846 );
14847 }
14848 });
14849
14850 cx.update_editor(|editor, window, cx| {
14851 editor.show_word_completions(&ShowWordCompletions, window, cx);
14852 });
14853 cx.executor().run_until_parked();
14854 cx.update_editor(|editor, window, cx| {
14855 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14856 {
14857 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");
14858 } else {
14859 panic!("expected completion menu to be open after the word completions are called with an action");
14860 }
14861
14862 editor.cancel(&Cancel, window, cx);
14863 });
14864 cx.update_editor(|editor, _, _| {
14865 if editor.context_menu.borrow_mut().is_some() {
14866 panic!("expected completion menu to be hidden after canceling");
14867 }
14868 });
14869
14870 cx.simulate_keystroke("o");
14871 cx.executor().run_until_parked();
14872 cx.update_editor(|editor, _, _| {
14873 if editor.context_menu.borrow_mut().is_some() {
14874 panic!(
14875 "expected completion menu to be hidden, as words completion threshold is not met still"
14876 );
14877 }
14878 });
14879
14880 cx.simulate_keystroke("w");
14881 cx.executor().run_until_parked();
14882 cx.update_editor(|editor, _, _| {
14883 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14884 {
14885 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
14886 } else {
14887 panic!("expected completion menu to be open after the word completions threshold is met");
14888 }
14889 });
14890}
14891
14892#[gpui::test]
14893async fn test_word_completions_disabled(cx: &mut TestAppContext) {
14894 init_test(cx, |language_settings| {
14895 language_settings.defaults.completions = Some(CompletionSettingsContent {
14896 words: Some(WordsCompletionMode::Enabled),
14897 words_min_length: Some(0),
14898 lsp_insert_mode: Some(LspInsertMode::Insert),
14899 ..Default::default()
14900 });
14901 });
14902
14903 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14904 cx.update_editor(|editor, _, _| {
14905 editor.disable_word_completions();
14906 });
14907 cx.set_state(indoc! {"ˇ
14908 wow
14909 wowen
14910 wowser
14911 "});
14912 cx.simulate_keystroke("w");
14913 cx.executor().run_until_parked();
14914 cx.update_editor(|editor, _, _| {
14915 if editor.context_menu.borrow_mut().is_some() {
14916 panic!(
14917 "expected completion menu to be hidden, as words completion are disabled for this editor"
14918 );
14919 }
14920 });
14921
14922 cx.update_editor(|editor, window, cx| {
14923 editor.show_word_completions(&ShowWordCompletions, window, cx);
14924 });
14925 cx.executor().run_until_parked();
14926 cx.update_editor(|editor, _, _| {
14927 if editor.context_menu.borrow_mut().is_some() {
14928 panic!(
14929 "expected completion menu to be hidden even if called for explicitly, as words completion are disabled for this editor"
14930 );
14931 }
14932 });
14933}
14934
14935fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
14936 let position = || lsp::Position {
14937 line: params.text_document_position.position.line,
14938 character: params.text_document_position.position.character,
14939 };
14940 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14941 range: lsp::Range {
14942 start: position(),
14943 end: position(),
14944 },
14945 new_text: text.to_string(),
14946 }))
14947}
14948
14949#[gpui::test]
14950async fn test_multiline_completion(cx: &mut TestAppContext) {
14951 init_test(cx, |_| {});
14952
14953 let fs = FakeFs::new(cx.executor());
14954 fs.insert_tree(
14955 path!("/a"),
14956 json!({
14957 "main.ts": "a",
14958 }),
14959 )
14960 .await;
14961
14962 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14963 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14964 let typescript_language = Arc::new(Language::new(
14965 LanguageConfig {
14966 name: "TypeScript".into(),
14967 matcher: LanguageMatcher {
14968 path_suffixes: vec!["ts".to_string()],
14969 ..LanguageMatcher::default()
14970 },
14971 line_comments: vec!["// ".into()],
14972 ..LanguageConfig::default()
14973 },
14974 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
14975 ));
14976 language_registry.add(typescript_language.clone());
14977 let mut fake_servers = language_registry.register_fake_lsp(
14978 "TypeScript",
14979 FakeLspAdapter {
14980 capabilities: lsp::ServerCapabilities {
14981 completion_provider: Some(lsp::CompletionOptions {
14982 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14983 ..lsp::CompletionOptions::default()
14984 }),
14985 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14986 ..lsp::ServerCapabilities::default()
14987 },
14988 // Emulate vtsls label generation
14989 label_for_completion: Some(Box::new(|item, _| {
14990 let text = if let Some(description) = item
14991 .label_details
14992 .as_ref()
14993 .and_then(|label_details| label_details.description.as_ref())
14994 {
14995 format!("{} {}", item.label, description)
14996 } else if let Some(detail) = &item.detail {
14997 format!("{} {}", item.label, detail)
14998 } else {
14999 item.label.clone()
15000 };
15001 Some(language::CodeLabel::plain(text, None))
15002 })),
15003 ..FakeLspAdapter::default()
15004 },
15005 );
15006 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15007 let cx = &mut VisualTestContext::from_window(*workspace, cx);
15008 let worktree_id = workspace
15009 .update(cx, |workspace, _window, cx| {
15010 workspace.project().update(cx, |project, cx| {
15011 project.worktrees(cx).next().unwrap().read(cx).id()
15012 })
15013 })
15014 .unwrap();
15015 let _buffer = project
15016 .update(cx, |project, cx| {
15017 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
15018 })
15019 .await
15020 .unwrap();
15021 let editor = workspace
15022 .update(cx, |workspace, window, cx| {
15023 workspace.open_path((worktree_id, rel_path("main.ts")), None, true, window, cx)
15024 })
15025 .unwrap()
15026 .await
15027 .unwrap()
15028 .downcast::<Editor>()
15029 .unwrap();
15030 let fake_server = fake_servers.next().await.unwrap();
15031
15032 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
15033 let multiline_label_2 = "a\nb\nc\n";
15034 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
15035 let multiline_description = "d\ne\nf\n";
15036 let multiline_detail_2 = "g\nh\ni\n";
15037
15038 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
15039 move |params, _| async move {
15040 Ok(Some(lsp::CompletionResponse::Array(vec![
15041 lsp::CompletionItem {
15042 label: multiline_label.to_string(),
15043 text_edit: gen_text_edit(¶ms, "new_text_1"),
15044 ..lsp::CompletionItem::default()
15045 },
15046 lsp::CompletionItem {
15047 label: "single line label 1".to_string(),
15048 detail: Some(multiline_detail.to_string()),
15049 text_edit: gen_text_edit(¶ms, "new_text_2"),
15050 ..lsp::CompletionItem::default()
15051 },
15052 lsp::CompletionItem {
15053 label: "single line label 2".to_string(),
15054 label_details: Some(lsp::CompletionItemLabelDetails {
15055 description: Some(multiline_description.to_string()),
15056 detail: None,
15057 }),
15058 text_edit: gen_text_edit(¶ms, "new_text_2"),
15059 ..lsp::CompletionItem::default()
15060 },
15061 lsp::CompletionItem {
15062 label: multiline_label_2.to_string(),
15063 detail: Some(multiline_detail_2.to_string()),
15064 text_edit: gen_text_edit(¶ms, "new_text_3"),
15065 ..lsp::CompletionItem::default()
15066 },
15067 lsp::CompletionItem {
15068 label: "Label with many spaces and \t but without newlines".to_string(),
15069 detail: Some(
15070 "Details with many spaces and \t but without newlines".to_string(),
15071 ),
15072 text_edit: gen_text_edit(¶ms, "new_text_4"),
15073 ..lsp::CompletionItem::default()
15074 },
15075 ])))
15076 },
15077 );
15078
15079 editor.update_in(cx, |editor, window, cx| {
15080 cx.focus_self(window);
15081 editor.move_to_end(&MoveToEnd, window, cx);
15082 editor.handle_input(".", window, cx);
15083 });
15084 cx.run_until_parked();
15085 completion_handle.next().await.unwrap();
15086
15087 editor.update(cx, |editor, _| {
15088 assert!(editor.context_menu_visible());
15089 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15090 {
15091 let completion_labels = menu
15092 .completions
15093 .borrow()
15094 .iter()
15095 .map(|c| c.label.text.clone())
15096 .collect::<Vec<_>>();
15097 assert_eq!(
15098 completion_labels,
15099 &[
15100 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
15101 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
15102 "single line label 2 d e f ",
15103 "a b c g h i ",
15104 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
15105 ],
15106 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
15107 );
15108
15109 for completion in menu
15110 .completions
15111 .borrow()
15112 .iter() {
15113 assert_eq!(
15114 completion.label.filter_range,
15115 0..completion.label.text.len(),
15116 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
15117 );
15118 }
15119 } else {
15120 panic!("expected completion menu to be open");
15121 }
15122 });
15123}
15124
15125#[gpui::test]
15126async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
15127 init_test(cx, |_| {});
15128 let mut cx = EditorLspTestContext::new_rust(
15129 lsp::ServerCapabilities {
15130 completion_provider: Some(lsp::CompletionOptions {
15131 trigger_characters: Some(vec![".".to_string()]),
15132 ..Default::default()
15133 }),
15134 ..Default::default()
15135 },
15136 cx,
15137 )
15138 .await;
15139 cx.lsp
15140 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15141 Ok(Some(lsp::CompletionResponse::Array(vec![
15142 lsp::CompletionItem {
15143 label: "first".into(),
15144 ..Default::default()
15145 },
15146 lsp::CompletionItem {
15147 label: "last".into(),
15148 ..Default::default()
15149 },
15150 ])))
15151 });
15152 cx.set_state("variableˇ");
15153 cx.simulate_keystroke(".");
15154 cx.executor().run_until_parked();
15155
15156 cx.update_editor(|editor, _, _| {
15157 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15158 {
15159 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
15160 } else {
15161 panic!("expected completion menu to be open");
15162 }
15163 });
15164
15165 cx.update_editor(|editor, window, cx| {
15166 editor.move_page_down(&MovePageDown::default(), window, cx);
15167 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15168 {
15169 assert!(
15170 menu.selected_item == 1,
15171 "expected PageDown to select the last item from the context menu"
15172 );
15173 } else {
15174 panic!("expected completion menu to stay open after PageDown");
15175 }
15176 });
15177
15178 cx.update_editor(|editor, window, cx| {
15179 editor.move_page_up(&MovePageUp::default(), window, cx);
15180 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15181 {
15182 assert!(
15183 menu.selected_item == 0,
15184 "expected PageUp to select the first item from the context menu"
15185 );
15186 } else {
15187 panic!("expected completion menu to stay open after PageUp");
15188 }
15189 });
15190}
15191
15192#[gpui::test]
15193async fn test_as_is_completions(cx: &mut TestAppContext) {
15194 init_test(cx, |_| {});
15195 let mut cx = EditorLspTestContext::new_rust(
15196 lsp::ServerCapabilities {
15197 completion_provider: Some(lsp::CompletionOptions {
15198 ..Default::default()
15199 }),
15200 ..Default::default()
15201 },
15202 cx,
15203 )
15204 .await;
15205 cx.lsp
15206 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15207 Ok(Some(lsp::CompletionResponse::Array(vec![
15208 lsp::CompletionItem {
15209 label: "unsafe".into(),
15210 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15211 range: lsp::Range {
15212 start: lsp::Position {
15213 line: 1,
15214 character: 2,
15215 },
15216 end: lsp::Position {
15217 line: 1,
15218 character: 3,
15219 },
15220 },
15221 new_text: "unsafe".to_string(),
15222 })),
15223 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
15224 ..Default::default()
15225 },
15226 ])))
15227 });
15228 cx.set_state("fn a() {}\n nˇ");
15229 cx.executor().run_until_parked();
15230 cx.update_editor(|editor, window, cx| {
15231 editor.show_completions(
15232 &ShowCompletions {
15233 trigger: Some("\n".into()),
15234 },
15235 window,
15236 cx,
15237 );
15238 });
15239 cx.executor().run_until_parked();
15240
15241 cx.update_editor(|editor, window, cx| {
15242 editor.confirm_completion(&Default::default(), window, cx)
15243 });
15244 cx.executor().run_until_parked();
15245 cx.assert_editor_state("fn a() {}\n unsafeˇ");
15246}
15247
15248#[gpui::test]
15249async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
15250 init_test(cx, |_| {});
15251 let language =
15252 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
15253 let mut cx = EditorLspTestContext::new(
15254 language,
15255 lsp::ServerCapabilities {
15256 completion_provider: Some(lsp::CompletionOptions {
15257 ..lsp::CompletionOptions::default()
15258 }),
15259 ..lsp::ServerCapabilities::default()
15260 },
15261 cx,
15262 )
15263 .await;
15264
15265 cx.set_state(
15266 "#ifndef BAR_H
15267#define BAR_H
15268
15269#include <stdbool.h>
15270
15271int fn_branch(bool do_branch1, bool do_branch2);
15272
15273#endif // BAR_H
15274ˇ",
15275 );
15276 cx.executor().run_until_parked();
15277 cx.update_editor(|editor, window, cx| {
15278 editor.handle_input("#", window, cx);
15279 });
15280 cx.executor().run_until_parked();
15281 cx.update_editor(|editor, window, cx| {
15282 editor.handle_input("i", window, cx);
15283 });
15284 cx.executor().run_until_parked();
15285 cx.update_editor(|editor, window, cx| {
15286 editor.handle_input("n", window, cx);
15287 });
15288 cx.executor().run_until_parked();
15289 cx.assert_editor_state(
15290 "#ifndef BAR_H
15291#define BAR_H
15292
15293#include <stdbool.h>
15294
15295int fn_branch(bool do_branch1, bool do_branch2);
15296
15297#endif // BAR_H
15298#inˇ",
15299 );
15300
15301 cx.lsp
15302 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15303 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15304 is_incomplete: false,
15305 item_defaults: None,
15306 items: vec![lsp::CompletionItem {
15307 kind: Some(lsp::CompletionItemKind::SNIPPET),
15308 label_details: Some(lsp::CompletionItemLabelDetails {
15309 detail: Some("header".to_string()),
15310 description: None,
15311 }),
15312 label: " include".to_string(),
15313 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15314 range: lsp::Range {
15315 start: lsp::Position {
15316 line: 8,
15317 character: 1,
15318 },
15319 end: lsp::Position {
15320 line: 8,
15321 character: 1,
15322 },
15323 },
15324 new_text: "include \"$0\"".to_string(),
15325 })),
15326 sort_text: Some("40b67681include".to_string()),
15327 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15328 filter_text: Some("include".to_string()),
15329 insert_text: Some("include \"$0\"".to_string()),
15330 ..lsp::CompletionItem::default()
15331 }],
15332 })))
15333 });
15334 cx.update_editor(|editor, window, cx| {
15335 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15336 });
15337 cx.executor().run_until_parked();
15338 cx.update_editor(|editor, window, cx| {
15339 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15340 });
15341 cx.executor().run_until_parked();
15342 cx.assert_editor_state(
15343 "#ifndef BAR_H
15344#define BAR_H
15345
15346#include <stdbool.h>
15347
15348int fn_branch(bool do_branch1, bool do_branch2);
15349
15350#endif // BAR_H
15351#include \"ˇ\"",
15352 );
15353
15354 cx.lsp
15355 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15356 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15357 is_incomplete: true,
15358 item_defaults: None,
15359 items: vec![lsp::CompletionItem {
15360 kind: Some(lsp::CompletionItemKind::FILE),
15361 label: "AGL/".to_string(),
15362 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15363 range: lsp::Range {
15364 start: lsp::Position {
15365 line: 8,
15366 character: 10,
15367 },
15368 end: lsp::Position {
15369 line: 8,
15370 character: 11,
15371 },
15372 },
15373 new_text: "AGL/".to_string(),
15374 })),
15375 sort_text: Some("40b67681AGL/".to_string()),
15376 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15377 filter_text: Some("AGL/".to_string()),
15378 insert_text: Some("AGL/".to_string()),
15379 ..lsp::CompletionItem::default()
15380 }],
15381 })))
15382 });
15383 cx.update_editor(|editor, window, cx| {
15384 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15385 });
15386 cx.executor().run_until_parked();
15387 cx.update_editor(|editor, window, cx| {
15388 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15389 });
15390 cx.executor().run_until_parked();
15391 cx.assert_editor_state(
15392 r##"#ifndef BAR_H
15393#define BAR_H
15394
15395#include <stdbool.h>
15396
15397int fn_branch(bool do_branch1, bool do_branch2);
15398
15399#endif // BAR_H
15400#include "AGL/ˇ"##,
15401 );
15402
15403 cx.update_editor(|editor, window, cx| {
15404 editor.handle_input("\"", window, cx);
15405 });
15406 cx.executor().run_until_parked();
15407 cx.assert_editor_state(
15408 r##"#ifndef BAR_H
15409#define BAR_H
15410
15411#include <stdbool.h>
15412
15413int fn_branch(bool do_branch1, bool do_branch2);
15414
15415#endif // BAR_H
15416#include "AGL/"ˇ"##,
15417 );
15418}
15419
15420#[gpui::test]
15421async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
15422 init_test(cx, |_| {});
15423
15424 let mut cx = EditorLspTestContext::new_rust(
15425 lsp::ServerCapabilities {
15426 completion_provider: Some(lsp::CompletionOptions {
15427 trigger_characters: Some(vec![".".to_string()]),
15428 resolve_provider: Some(true),
15429 ..Default::default()
15430 }),
15431 ..Default::default()
15432 },
15433 cx,
15434 )
15435 .await;
15436
15437 cx.set_state("fn main() { let a = 2ˇ; }");
15438 cx.simulate_keystroke(".");
15439 let completion_item = lsp::CompletionItem {
15440 label: "Some".into(),
15441 kind: Some(lsp::CompletionItemKind::SNIPPET),
15442 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15443 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15444 kind: lsp::MarkupKind::Markdown,
15445 value: "```rust\nSome(2)\n```".to_string(),
15446 })),
15447 deprecated: Some(false),
15448 sort_text: Some("Some".to_string()),
15449 filter_text: Some("Some".to_string()),
15450 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15451 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15452 range: lsp::Range {
15453 start: lsp::Position {
15454 line: 0,
15455 character: 22,
15456 },
15457 end: lsp::Position {
15458 line: 0,
15459 character: 22,
15460 },
15461 },
15462 new_text: "Some(2)".to_string(),
15463 })),
15464 additional_text_edits: Some(vec![lsp::TextEdit {
15465 range: lsp::Range {
15466 start: lsp::Position {
15467 line: 0,
15468 character: 20,
15469 },
15470 end: lsp::Position {
15471 line: 0,
15472 character: 22,
15473 },
15474 },
15475 new_text: "".to_string(),
15476 }]),
15477 ..Default::default()
15478 };
15479
15480 let closure_completion_item = completion_item.clone();
15481 let counter = Arc::new(AtomicUsize::new(0));
15482 let counter_clone = counter.clone();
15483 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15484 let task_completion_item = closure_completion_item.clone();
15485 counter_clone.fetch_add(1, atomic::Ordering::Release);
15486 async move {
15487 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15488 is_incomplete: true,
15489 item_defaults: None,
15490 items: vec![task_completion_item],
15491 })))
15492 }
15493 });
15494
15495 cx.condition(|editor, _| editor.context_menu_visible())
15496 .await;
15497 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
15498 assert!(request.next().await.is_some());
15499 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15500
15501 cx.simulate_keystrokes("S o m");
15502 cx.condition(|editor, _| editor.context_menu_visible())
15503 .await;
15504 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
15505 assert!(request.next().await.is_some());
15506 assert!(request.next().await.is_some());
15507 assert!(request.next().await.is_some());
15508 request.close();
15509 assert!(request.next().await.is_none());
15510 assert_eq!(
15511 counter.load(atomic::Ordering::Acquire),
15512 4,
15513 "With the completions menu open, only one LSP request should happen per input"
15514 );
15515}
15516
15517#[gpui::test]
15518async fn test_toggle_comment(cx: &mut TestAppContext) {
15519 init_test(cx, |_| {});
15520 let mut cx = EditorTestContext::new(cx).await;
15521 let language = Arc::new(Language::new(
15522 LanguageConfig {
15523 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15524 ..Default::default()
15525 },
15526 Some(tree_sitter_rust::LANGUAGE.into()),
15527 ));
15528 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15529
15530 // If multiple selections intersect a line, the line is only toggled once.
15531 cx.set_state(indoc! {"
15532 fn a() {
15533 «//b();
15534 ˇ»// «c();
15535 //ˇ» d();
15536 }
15537 "});
15538
15539 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15540
15541 cx.assert_editor_state(indoc! {"
15542 fn a() {
15543 «b();
15544 c();
15545 ˇ» d();
15546 }
15547 "});
15548
15549 // The comment prefix is inserted at the same column for every line in a
15550 // selection.
15551 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15552
15553 cx.assert_editor_state(indoc! {"
15554 fn a() {
15555 // «b();
15556 // c();
15557 ˇ»// d();
15558 }
15559 "});
15560
15561 // If a selection ends at the beginning of a line, that line is not toggled.
15562 cx.set_selections_state(indoc! {"
15563 fn a() {
15564 // b();
15565 «// c();
15566 ˇ» // d();
15567 }
15568 "});
15569
15570 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15571
15572 cx.assert_editor_state(indoc! {"
15573 fn a() {
15574 // b();
15575 «c();
15576 ˇ» // d();
15577 }
15578 "});
15579
15580 // If a selection span a single line and is empty, the line is toggled.
15581 cx.set_state(indoc! {"
15582 fn a() {
15583 a();
15584 b();
15585 ˇ
15586 }
15587 "});
15588
15589 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15590
15591 cx.assert_editor_state(indoc! {"
15592 fn a() {
15593 a();
15594 b();
15595 //•ˇ
15596 }
15597 "});
15598
15599 // If a selection span multiple lines, empty lines are not toggled.
15600 cx.set_state(indoc! {"
15601 fn a() {
15602 «a();
15603
15604 c();ˇ»
15605 }
15606 "});
15607
15608 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15609
15610 cx.assert_editor_state(indoc! {"
15611 fn a() {
15612 // «a();
15613
15614 // c();ˇ»
15615 }
15616 "});
15617
15618 // If a selection includes multiple comment prefixes, all lines are uncommented.
15619 cx.set_state(indoc! {"
15620 fn a() {
15621 «// a();
15622 /// b();
15623 //! c();ˇ»
15624 }
15625 "});
15626
15627 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15628
15629 cx.assert_editor_state(indoc! {"
15630 fn a() {
15631 «a();
15632 b();
15633 c();ˇ»
15634 }
15635 "});
15636}
15637
15638#[gpui::test]
15639async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
15640 init_test(cx, |_| {});
15641 let mut cx = EditorTestContext::new(cx).await;
15642 let language = Arc::new(Language::new(
15643 LanguageConfig {
15644 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15645 ..Default::default()
15646 },
15647 Some(tree_sitter_rust::LANGUAGE.into()),
15648 ));
15649 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15650
15651 let toggle_comments = &ToggleComments {
15652 advance_downwards: false,
15653 ignore_indent: true,
15654 };
15655
15656 // If multiple selections intersect a line, the line is only toggled once.
15657 cx.set_state(indoc! {"
15658 fn a() {
15659 // «b();
15660 // c();
15661 // ˇ» d();
15662 }
15663 "});
15664
15665 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15666
15667 cx.assert_editor_state(indoc! {"
15668 fn a() {
15669 «b();
15670 c();
15671 ˇ» d();
15672 }
15673 "});
15674
15675 // The comment prefix is inserted at the beginning of each line
15676 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15677
15678 cx.assert_editor_state(indoc! {"
15679 fn a() {
15680 // «b();
15681 // c();
15682 // ˇ» d();
15683 }
15684 "});
15685
15686 // If a selection ends at the beginning of a line, that line is not toggled.
15687 cx.set_selections_state(indoc! {"
15688 fn a() {
15689 // b();
15690 // «c();
15691 ˇ»// d();
15692 }
15693 "});
15694
15695 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15696
15697 cx.assert_editor_state(indoc! {"
15698 fn a() {
15699 // b();
15700 «c();
15701 ˇ»// d();
15702 }
15703 "});
15704
15705 // If a selection span a single line and is empty, the line is toggled.
15706 cx.set_state(indoc! {"
15707 fn a() {
15708 a();
15709 b();
15710 ˇ
15711 }
15712 "});
15713
15714 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15715
15716 cx.assert_editor_state(indoc! {"
15717 fn a() {
15718 a();
15719 b();
15720 //ˇ
15721 }
15722 "});
15723
15724 // If a selection span multiple lines, empty lines are not toggled.
15725 cx.set_state(indoc! {"
15726 fn a() {
15727 «a();
15728
15729 c();ˇ»
15730 }
15731 "});
15732
15733 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15734
15735 cx.assert_editor_state(indoc! {"
15736 fn a() {
15737 // «a();
15738
15739 // c();ˇ»
15740 }
15741 "});
15742
15743 // If a selection includes multiple comment prefixes, all lines are uncommented.
15744 cx.set_state(indoc! {"
15745 fn a() {
15746 // «a();
15747 /// b();
15748 //! c();ˇ»
15749 }
15750 "});
15751
15752 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15753
15754 cx.assert_editor_state(indoc! {"
15755 fn a() {
15756 «a();
15757 b();
15758 c();ˇ»
15759 }
15760 "});
15761}
15762
15763#[gpui::test]
15764async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
15765 init_test(cx, |_| {});
15766
15767 let language = Arc::new(Language::new(
15768 LanguageConfig {
15769 line_comments: vec!["// ".into()],
15770 ..Default::default()
15771 },
15772 Some(tree_sitter_rust::LANGUAGE.into()),
15773 ));
15774
15775 let mut cx = EditorTestContext::new(cx).await;
15776
15777 cx.language_registry().add(language.clone());
15778 cx.update_buffer(|buffer, cx| {
15779 buffer.set_language(Some(language), cx);
15780 });
15781
15782 let toggle_comments = &ToggleComments {
15783 advance_downwards: true,
15784 ignore_indent: false,
15785 };
15786
15787 // Single cursor on one line -> advance
15788 // Cursor moves horizontally 3 characters as well on non-blank line
15789 cx.set_state(indoc!(
15790 "fn a() {
15791 ˇdog();
15792 cat();
15793 }"
15794 ));
15795 cx.update_editor(|editor, window, cx| {
15796 editor.toggle_comments(toggle_comments, window, cx);
15797 });
15798 cx.assert_editor_state(indoc!(
15799 "fn a() {
15800 // dog();
15801 catˇ();
15802 }"
15803 ));
15804
15805 // Single selection on one line -> don't advance
15806 cx.set_state(indoc!(
15807 "fn a() {
15808 «dog()ˇ»;
15809 cat();
15810 }"
15811 ));
15812 cx.update_editor(|editor, window, cx| {
15813 editor.toggle_comments(toggle_comments, window, cx);
15814 });
15815 cx.assert_editor_state(indoc!(
15816 "fn a() {
15817 // «dog()ˇ»;
15818 cat();
15819 }"
15820 ));
15821
15822 // Multiple cursors on one line -> advance
15823 cx.set_state(indoc!(
15824 "fn a() {
15825 ˇdˇog();
15826 cat();
15827 }"
15828 ));
15829 cx.update_editor(|editor, window, cx| {
15830 editor.toggle_comments(toggle_comments, window, cx);
15831 });
15832 cx.assert_editor_state(indoc!(
15833 "fn a() {
15834 // dog();
15835 catˇ(ˇ);
15836 }"
15837 ));
15838
15839 // Multiple cursors on one line, with selection -> don't advance
15840 cx.set_state(indoc!(
15841 "fn a() {
15842 ˇdˇog«()ˇ»;
15843 cat();
15844 }"
15845 ));
15846 cx.update_editor(|editor, window, cx| {
15847 editor.toggle_comments(toggle_comments, window, cx);
15848 });
15849 cx.assert_editor_state(indoc!(
15850 "fn a() {
15851 // ˇdˇog«()ˇ»;
15852 cat();
15853 }"
15854 ));
15855
15856 // Single cursor on one line -> advance
15857 // Cursor moves to column 0 on blank line
15858 cx.set_state(indoc!(
15859 "fn a() {
15860 ˇdog();
15861
15862 cat();
15863 }"
15864 ));
15865 cx.update_editor(|editor, window, cx| {
15866 editor.toggle_comments(toggle_comments, window, cx);
15867 });
15868 cx.assert_editor_state(indoc!(
15869 "fn a() {
15870 // dog();
15871 ˇ
15872 cat();
15873 }"
15874 ));
15875
15876 // Single cursor on one line -> advance
15877 // Cursor starts and ends at column 0
15878 cx.set_state(indoc!(
15879 "fn a() {
15880 ˇ dog();
15881 cat();
15882 }"
15883 ));
15884 cx.update_editor(|editor, window, cx| {
15885 editor.toggle_comments(toggle_comments, window, cx);
15886 });
15887 cx.assert_editor_state(indoc!(
15888 "fn a() {
15889 // dog();
15890 ˇ cat();
15891 }"
15892 ));
15893}
15894
15895#[gpui::test]
15896async fn test_toggle_block_comment(cx: &mut TestAppContext) {
15897 init_test(cx, |_| {});
15898
15899 let mut cx = EditorTestContext::new(cx).await;
15900
15901 let html_language = Arc::new(
15902 Language::new(
15903 LanguageConfig {
15904 name: "HTML".into(),
15905 block_comment: Some(BlockCommentConfig {
15906 start: "<!-- ".into(),
15907 prefix: "".into(),
15908 end: " -->".into(),
15909 tab_size: 0,
15910 }),
15911 ..Default::default()
15912 },
15913 Some(tree_sitter_html::LANGUAGE.into()),
15914 )
15915 .with_injection_query(
15916 r#"
15917 (script_element
15918 (raw_text) @injection.content
15919 (#set! injection.language "javascript"))
15920 "#,
15921 )
15922 .unwrap(),
15923 );
15924
15925 let javascript_language = Arc::new(Language::new(
15926 LanguageConfig {
15927 name: "JavaScript".into(),
15928 line_comments: vec!["// ".into()],
15929 ..Default::default()
15930 },
15931 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
15932 ));
15933
15934 cx.language_registry().add(html_language.clone());
15935 cx.language_registry().add(javascript_language);
15936 cx.update_buffer(|buffer, cx| {
15937 buffer.set_language(Some(html_language), cx);
15938 });
15939
15940 // Toggle comments for empty selections
15941 cx.set_state(
15942 &r#"
15943 <p>A</p>ˇ
15944 <p>B</p>ˇ
15945 <p>C</p>ˇ
15946 "#
15947 .unindent(),
15948 );
15949 cx.update_editor(|editor, window, cx| {
15950 editor.toggle_comments(&ToggleComments::default(), window, cx)
15951 });
15952 cx.assert_editor_state(
15953 &r#"
15954 <!-- <p>A</p>ˇ -->
15955 <!-- <p>B</p>ˇ -->
15956 <!-- <p>C</p>ˇ -->
15957 "#
15958 .unindent(),
15959 );
15960 cx.update_editor(|editor, window, cx| {
15961 editor.toggle_comments(&ToggleComments::default(), window, cx)
15962 });
15963 cx.assert_editor_state(
15964 &r#"
15965 <p>A</p>ˇ
15966 <p>B</p>ˇ
15967 <p>C</p>ˇ
15968 "#
15969 .unindent(),
15970 );
15971
15972 // Toggle comments for mixture of empty and non-empty selections, where
15973 // multiple selections occupy a given line.
15974 cx.set_state(
15975 &r#"
15976 <p>A«</p>
15977 <p>ˇ»B</p>ˇ
15978 <p>C«</p>
15979 <p>ˇ»D</p>ˇ
15980 "#
15981 .unindent(),
15982 );
15983
15984 cx.update_editor(|editor, window, cx| {
15985 editor.toggle_comments(&ToggleComments::default(), window, cx)
15986 });
15987 cx.assert_editor_state(
15988 &r#"
15989 <!-- <p>A«</p>
15990 <p>ˇ»B</p>ˇ -->
15991 <!-- <p>C«</p>
15992 <p>ˇ»D</p>ˇ -->
15993 "#
15994 .unindent(),
15995 );
15996 cx.update_editor(|editor, window, cx| {
15997 editor.toggle_comments(&ToggleComments::default(), window, cx)
15998 });
15999 cx.assert_editor_state(
16000 &r#"
16001 <p>A«</p>
16002 <p>ˇ»B</p>ˇ
16003 <p>C«</p>
16004 <p>ˇ»D</p>ˇ
16005 "#
16006 .unindent(),
16007 );
16008
16009 // Toggle comments when different languages are active for different
16010 // selections.
16011 cx.set_state(
16012 &r#"
16013 ˇ<script>
16014 ˇvar x = new Y();
16015 ˇ</script>
16016 "#
16017 .unindent(),
16018 );
16019 cx.executor().run_until_parked();
16020 cx.update_editor(|editor, window, cx| {
16021 editor.toggle_comments(&ToggleComments::default(), window, cx)
16022 });
16023 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
16024 // Uncommenting and commenting from this position brings in even more wrong artifacts.
16025 cx.assert_editor_state(
16026 &r#"
16027 <!-- ˇ<script> -->
16028 // ˇvar x = new Y();
16029 <!-- ˇ</script> -->
16030 "#
16031 .unindent(),
16032 );
16033}
16034
16035#[gpui::test]
16036fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
16037 init_test(cx, |_| {});
16038
16039 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16040 let multibuffer = cx.new(|cx| {
16041 let mut multibuffer = MultiBuffer::new(ReadWrite);
16042 multibuffer.push_excerpts(
16043 buffer.clone(),
16044 [
16045 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
16046 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
16047 ],
16048 cx,
16049 );
16050 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
16051 multibuffer
16052 });
16053
16054 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
16055 editor.update_in(cx, |editor, window, cx| {
16056 assert_eq!(editor.text(cx), "aaaa\nbbbb");
16057 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16058 s.select_ranges([
16059 Point::new(0, 0)..Point::new(0, 0),
16060 Point::new(1, 0)..Point::new(1, 0),
16061 ])
16062 });
16063
16064 editor.handle_input("X", window, cx);
16065 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
16066 assert_eq!(
16067 editor.selections.ranges(&editor.display_snapshot(cx)),
16068 [
16069 Point::new(0, 1)..Point::new(0, 1),
16070 Point::new(1, 1)..Point::new(1, 1),
16071 ]
16072 );
16073
16074 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
16075 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16076 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
16077 });
16078 editor.backspace(&Default::default(), window, cx);
16079 assert_eq!(editor.text(cx), "Xa\nbbb");
16080 assert_eq!(
16081 editor.selections.ranges(&editor.display_snapshot(cx)),
16082 [Point::new(1, 0)..Point::new(1, 0)]
16083 );
16084
16085 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16086 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
16087 });
16088 editor.backspace(&Default::default(), window, cx);
16089 assert_eq!(editor.text(cx), "X\nbb");
16090 assert_eq!(
16091 editor.selections.ranges(&editor.display_snapshot(cx)),
16092 [Point::new(0, 1)..Point::new(0, 1)]
16093 );
16094 });
16095}
16096
16097#[gpui::test]
16098fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
16099 init_test(cx, |_| {});
16100
16101 let markers = vec![('[', ']').into(), ('(', ')').into()];
16102 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
16103 indoc! {"
16104 [aaaa
16105 (bbbb]
16106 cccc)",
16107 },
16108 markers.clone(),
16109 );
16110 let excerpt_ranges = markers.into_iter().map(|marker| {
16111 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
16112 ExcerptRange::new(context)
16113 });
16114 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
16115 let multibuffer = cx.new(|cx| {
16116 let mut multibuffer = MultiBuffer::new(ReadWrite);
16117 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
16118 multibuffer
16119 });
16120
16121 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
16122 editor.update_in(cx, |editor, window, cx| {
16123 let (expected_text, selection_ranges) = marked_text_ranges(
16124 indoc! {"
16125 aaaa
16126 bˇbbb
16127 bˇbbˇb
16128 cccc"
16129 },
16130 true,
16131 );
16132 assert_eq!(editor.text(cx), expected_text);
16133 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16134 s.select_ranges(selection_ranges)
16135 });
16136
16137 editor.handle_input("X", window, cx);
16138
16139 let (expected_text, expected_selections) = marked_text_ranges(
16140 indoc! {"
16141 aaaa
16142 bXˇbbXb
16143 bXˇbbXˇb
16144 cccc"
16145 },
16146 false,
16147 );
16148 assert_eq!(editor.text(cx), expected_text);
16149 assert_eq!(
16150 editor.selections.ranges(&editor.display_snapshot(cx)),
16151 expected_selections
16152 );
16153
16154 editor.newline(&Newline, window, cx);
16155 let (expected_text, expected_selections) = marked_text_ranges(
16156 indoc! {"
16157 aaaa
16158 bX
16159 ˇbbX
16160 b
16161 bX
16162 ˇbbX
16163 ˇb
16164 cccc"
16165 },
16166 false,
16167 );
16168 assert_eq!(editor.text(cx), expected_text);
16169 assert_eq!(
16170 editor.selections.ranges(&editor.display_snapshot(cx)),
16171 expected_selections
16172 );
16173 });
16174}
16175
16176#[gpui::test]
16177fn test_refresh_selections(cx: &mut TestAppContext) {
16178 init_test(cx, |_| {});
16179
16180 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16181 let mut excerpt1_id = None;
16182 let multibuffer = cx.new(|cx| {
16183 let mut multibuffer = MultiBuffer::new(ReadWrite);
16184 excerpt1_id = multibuffer
16185 .push_excerpts(
16186 buffer.clone(),
16187 [
16188 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
16189 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
16190 ],
16191 cx,
16192 )
16193 .into_iter()
16194 .next();
16195 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16196 multibuffer
16197 });
16198
16199 let editor = cx.add_window(|window, cx| {
16200 let mut editor = build_editor(multibuffer.clone(), window, cx);
16201 let snapshot = editor.snapshot(window, cx);
16202 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16203 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
16204 });
16205 editor.begin_selection(
16206 Point::new(2, 1).to_display_point(&snapshot),
16207 true,
16208 1,
16209 window,
16210 cx,
16211 );
16212 assert_eq!(
16213 editor.selections.ranges(&editor.display_snapshot(cx)),
16214 [
16215 Point::new(1, 3)..Point::new(1, 3),
16216 Point::new(2, 1)..Point::new(2, 1),
16217 ]
16218 );
16219 editor
16220 });
16221
16222 // Refreshing selections is a no-op when excerpts haven't changed.
16223 _ = editor.update(cx, |editor, window, cx| {
16224 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16225 assert_eq!(
16226 editor.selections.ranges(&editor.display_snapshot(cx)),
16227 [
16228 Point::new(1, 3)..Point::new(1, 3),
16229 Point::new(2, 1)..Point::new(2, 1),
16230 ]
16231 );
16232 });
16233
16234 multibuffer.update(cx, |multibuffer, cx| {
16235 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16236 });
16237 _ = editor.update(cx, |editor, window, cx| {
16238 // Removing an excerpt causes the first selection to become degenerate.
16239 assert_eq!(
16240 editor.selections.ranges(&editor.display_snapshot(cx)),
16241 [
16242 Point::new(0, 0)..Point::new(0, 0),
16243 Point::new(0, 1)..Point::new(0, 1)
16244 ]
16245 );
16246
16247 // Refreshing selections will relocate the first selection to the original buffer
16248 // location.
16249 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16250 assert_eq!(
16251 editor.selections.ranges(&editor.display_snapshot(cx)),
16252 [
16253 Point::new(0, 1)..Point::new(0, 1),
16254 Point::new(0, 3)..Point::new(0, 3)
16255 ]
16256 );
16257 assert!(editor.selections.pending_anchor().is_some());
16258 });
16259}
16260
16261#[gpui::test]
16262fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
16263 init_test(cx, |_| {});
16264
16265 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16266 let mut excerpt1_id = None;
16267 let multibuffer = cx.new(|cx| {
16268 let mut multibuffer = MultiBuffer::new(ReadWrite);
16269 excerpt1_id = multibuffer
16270 .push_excerpts(
16271 buffer.clone(),
16272 [
16273 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
16274 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
16275 ],
16276 cx,
16277 )
16278 .into_iter()
16279 .next();
16280 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16281 multibuffer
16282 });
16283
16284 let editor = cx.add_window(|window, cx| {
16285 let mut editor = build_editor(multibuffer.clone(), window, cx);
16286 let snapshot = editor.snapshot(window, cx);
16287 editor.begin_selection(
16288 Point::new(1, 3).to_display_point(&snapshot),
16289 false,
16290 1,
16291 window,
16292 cx,
16293 );
16294 assert_eq!(
16295 editor.selections.ranges(&editor.display_snapshot(cx)),
16296 [Point::new(1, 3)..Point::new(1, 3)]
16297 );
16298 editor
16299 });
16300
16301 multibuffer.update(cx, |multibuffer, cx| {
16302 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16303 });
16304 _ = editor.update(cx, |editor, window, cx| {
16305 assert_eq!(
16306 editor.selections.ranges(&editor.display_snapshot(cx)),
16307 [Point::new(0, 0)..Point::new(0, 0)]
16308 );
16309
16310 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
16311 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16312 assert_eq!(
16313 editor.selections.ranges(&editor.display_snapshot(cx)),
16314 [Point::new(0, 3)..Point::new(0, 3)]
16315 );
16316 assert!(editor.selections.pending_anchor().is_some());
16317 });
16318}
16319
16320#[gpui::test]
16321async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
16322 init_test(cx, |_| {});
16323
16324 let language = Arc::new(
16325 Language::new(
16326 LanguageConfig {
16327 brackets: BracketPairConfig {
16328 pairs: vec![
16329 BracketPair {
16330 start: "{".to_string(),
16331 end: "}".to_string(),
16332 close: true,
16333 surround: true,
16334 newline: true,
16335 },
16336 BracketPair {
16337 start: "/* ".to_string(),
16338 end: " */".to_string(),
16339 close: true,
16340 surround: true,
16341 newline: true,
16342 },
16343 ],
16344 ..Default::default()
16345 },
16346 ..Default::default()
16347 },
16348 Some(tree_sitter_rust::LANGUAGE.into()),
16349 )
16350 .with_indents_query("")
16351 .unwrap(),
16352 );
16353
16354 let text = concat!(
16355 "{ }\n", //
16356 " x\n", //
16357 " /* */\n", //
16358 "x\n", //
16359 "{{} }\n", //
16360 );
16361
16362 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
16363 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16364 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16365 editor
16366 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
16367 .await;
16368
16369 editor.update_in(cx, |editor, window, cx| {
16370 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16371 s.select_display_ranges([
16372 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
16373 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
16374 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
16375 ])
16376 });
16377 editor.newline(&Newline, window, cx);
16378
16379 assert_eq!(
16380 editor.buffer().read(cx).read(cx).text(),
16381 concat!(
16382 "{ \n", // Suppress rustfmt
16383 "\n", //
16384 "}\n", //
16385 " x\n", //
16386 " /* \n", //
16387 " \n", //
16388 " */\n", //
16389 "x\n", //
16390 "{{} \n", //
16391 "}\n", //
16392 )
16393 );
16394 });
16395}
16396
16397#[gpui::test]
16398fn test_highlighted_ranges(cx: &mut TestAppContext) {
16399 init_test(cx, |_| {});
16400
16401 let editor = cx.add_window(|window, cx| {
16402 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
16403 build_editor(buffer, window, cx)
16404 });
16405
16406 _ = editor.update(cx, |editor, window, cx| {
16407 struct Type1;
16408 struct Type2;
16409
16410 let buffer = editor.buffer.read(cx).snapshot(cx);
16411
16412 let anchor_range =
16413 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
16414
16415 editor.highlight_background::<Type1>(
16416 &[
16417 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
16418 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
16419 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
16420 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
16421 ],
16422 |_| Hsla::red(),
16423 cx,
16424 );
16425 editor.highlight_background::<Type2>(
16426 &[
16427 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
16428 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
16429 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
16430 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
16431 ],
16432 |_| Hsla::green(),
16433 cx,
16434 );
16435
16436 let snapshot = editor.snapshot(window, cx);
16437 let highlighted_ranges = editor.sorted_background_highlights_in_range(
16438 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
16439 &snapshot,
16440 cx.theme(),
16441 );
16442 assert_eq!(
16443 highlighted_ranges,
16444 &[
16445 (
16446 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
16447 Hsla::green(),
16448 ),
16449 (
16450 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
16451 Hsla::red(),
16452 ),
16453 (
16454 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
16455 Hsla::green(),
16456 ),
16457 (
16458 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16459 Hsla::red(),
16460 ),
16461 ]
16462 );
16463 assert_eq!(
16464 editor.sorted_background_highlights_in_range(
16465 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
16466 &snapshot,
16467 cx.theme(),
16468 ),
16469 &[(
16470 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16471 Hsla::red(),
16472 )]
16473 );
16474 });
16475}
16476
16477#[gpui::test]
16478async fn test_following(cx: &mut TestAppContext) {
16479 init_test(cx, |_| {});
16480
16481 let fs = FakeFs::new(cx.executor());
16482 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16483
16484 let buffer = project.update(cx, |project, cx| {
16485 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, false, cx);
16486 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
16487 });
16488 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
16489 let follower = cx.update(|cx| {
16490 cx.open_window(
16491 WindowOptions {
16492 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
16493 gpui::Point::new(px(0.), px(0.)),
16494 gpui::Point::new(px(10.), px(80.)),
16495 ))),
16496 ..Default::default()
16497 },
16498 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
16499 )
16500 .unwrap()
16501 });
16502
16503 let is_still_following = Rc::new(RefCell::new(true));
16504 let follower_edit_event_count = Rc::new(RefCell::new(0));
16505 let pending_update = Rc::new(RefCell::new(None));
16506 let leader_entity = leader.root(cx).unwrap();
16507 let follower_entity = follower.root(cx).unwrap();
16508 _ = follower.update(cx, {
16509 let update = pending_update.clone();
16510 let is_still_following = is_still_following.clone();
16511 let follower_edit_event_count = follower_edit_event_count.clone();
16512 |_, window, cx| {
16513 cx.subscribe_in(
16514 &leader_entity,
16515 window,
16516 move |_, leader, event, window, cx| {
16517 leader.read(cx).add_event_to_update_proto(
16518 event,
16519 &mut update.borrow_mut(),
16520 window,
16521 cx,
16522 );
16523 },
16524 )
16525 .detach();
16526
16527 cx.subscribe_in(
16528 &follower_entity,
16529 window,
16530 move |_, _, event: &EditorEvent, _window, _cx| {
16531 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
16532 *is_still_following.borrow_mut() = false;
16533 }
16534
16535 if let EditorEvent::BufferEdited = event {
16536 *follower_edit_event_count.borrow_mut() += 1;
16537 }
16538 },
16539 )
16540 .detach();
16541 }
16542 });
16543
16544 // Update the selections only
16545 _ = leader.update(cx, |leader, window, cx| {
16546 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16547 s.select_ranges([1..1])
16548 });
16549 });
16550 follower
16551 .update(cx, |follower, window, cx| {
16552 follower.apply_update_proto(
16553 &project,
16554 pending_update.borrow_mut().take().unwrap(),
16555 window,
16556 cx,
16557 )
16558 })
16559 .unwrap()
16560 .await
16561 .unwrap();
16562 _ = follower.update(cx, |follower, _, cx| {
16563 assert_eq!(
16564 follower.selections.ranges(&follower.display_snapshot(cx)),
16565 vec![1..1]
16566 );
16567 });
16568 assert!(*is_still_following.borrow());
16569 assert_eq!(*follower_edit_event_count.borrow(), 0);
16570
16571 // Update the scroll position only
16572 _ = leader.update(cx, |leader, window, cx| {
16573 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16574 });
16575 follower
16576 .update(cx, |follower, window, cx| {
16577 follower.apply_update_proto(
16578 &project,
16579 pending_update.borrow_mut().take().unwrap(),
16580 window,
16581 cx,
16582 )
16583 })
16584 .unwrap()
16585 .await
16586 .unwrap();
16587 assert_eq!(
16588 follower
16589 .update(cx, |follower, _, cx| follower.scroll_position(cx))
16590 .unwrap(),
16591 gpui::Point::new(1.5, 3.5)
16592 );
16593 assert!(*is_still_following.borrow());
16594 assert_eq!(*follower_edit_event_count.borrow(), 0);
16595
16596 // Update the selections and scroll position. The follower's scroll position is updated
16597 // via autoscroll, not via the leader's exact scroll position.
16598 _ = leader.update(cx, |leader, window, cx| {
16599 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16600 s.select_ranges([0..0])
16601 });
16602 leader.request_autoscroll(Autoscroll::newest(), cx);
16603 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16604 });
16605 follower
16606 .update(cx, |follower, window, cx| {
16607 follower.apply_update_proto(
16608 &project,
16609 pending_update.borrow_mut().take().unwrap(),
16610 window,
16611 cx,
16612 )
16613 })
16614 .unwrap()
16615 .await
16616 .unwrap();
16617 _ = follower.update(cx, |follower, _, cx| {
16618 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
16619 assert_eq!(
16620 follower.selections.ranges(&follower.display_snapshot(cx)),
16621 vec![0..0]
16622 );
16623 });
16624 assert!(*is_still_following.borrow());
16625
16626 // Creating a pending selection that precedes another selection
16627 _ = leader.update(cx, |leader, window, cx| {
16628 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16629 s.select_ranges([1..1])
16630 });
16631 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
16632 });
16633 follower
16634 .update(cx, |follower, window, cx| {
16635 follower.apply_update_proto(
16636 &project,
16637 pending_update.borrow_mut().take().unwrap(),
16638 window,
16639 cx,
16640 )
16641 })
16642 .unwrap()
16643 .await
16644 .unwrap();
16645 _ = follower.update(cx, |follower, _, cx| {
16646 assert_eq!(
16647 follower.selections.ranges(&follower.display_snapshot(cx)),
16648 vec![0..0, 1..1]
16649 );
16650 });
16651 assert!(*is_still_following.borrow());
16652
16653 // Extend the pending selection so that it surrounds another selection
16654 _ = leader.update(cx, |leader, window, cx| {
16655 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
16656 });
16657 follower
16658 .update(cx, |follower, window, cx| {
16659 follower.apply_update_proto(
16660 &project,
16661 pending_update.borrow_mut().take().unwrap(),
16662 window,
16663 cx,
16664 )
16665 })
16666 .unwrap()
16667 .await
16668 .unwrap();
16669 _ = follower.update(cx, |follower, _, cx| {
16670 assert_eq!(
16671 follower.selections.ranges(&follower.display_snapshot(cx)),
16672 vec![0..2]
16673 );
16674 });
16675
16676 // Scrolling locally breaks the follow
16677 _ = follower.update(cx, |follower, window, cx| {
16678 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
16679 follower.set_scroll_anchor(
16680 ScrollAnchor {
16681 anchor: top_anchor,
16682 offset: gpui::Point::new(0.0, 0.5),
16683 },
16684 window,
16685 cx,
16686 );
16687 });
16688 assert!(!(*is_still_following.borrow()));
16689}
16690
16691#[gpui::test]
16692async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
16693 init_test(cx, |_| {});
16694
16695 let fs = FakeFs::new(cx.executor());
16696 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16697 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16698 let pane = workspace
16699 .update(cx, |workspace, _, _| workspace.active_pane().clone())
16700 .unwrap();
16701
16702 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16703
16704 let leader = pane.update_in(cx, |_, window, cx| {
16705 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
16706 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
16707 });
16708
16709 // Start following the editor when it has no excerpts.
16710 let mut state_message =
16711 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16712 let workspace_entity = workspace.root(cx).unwrap();
16713 let follower_1 = cx
16714 .update_window(*workspace.deref(), |_, window, cx| {
16715 Editor::from_state_proto(
16716 workspace_entity,
16717 ViewId {
16718 creator: CollaboratorId::PeerId(PeerId::default()),
16719 id: 0,
16720 },
16721 &mut state_message,
16722 window,
16723 cx,
16724 )
16725 })
16726 .unwrap()
16727 .unwrap()
16728 .await
16729 .unwrap();
16730
16731 let update_message = Rc::new(RefCell::new(None));
16732 follower_1.update_in(cx, {
16733 let update = update_message.clone();
16734 |_, window, cx| {
16735 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
16736 leader.read(cx).add_event_to_update_proto(
16737 event,
16738 &mut update.borrow_mut(),
16739 window,
16740 cx,
16741 );
16742 })
16743 .detach();
16744 }
16745 });
16746
16747 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
16748 (
16749 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, false, cx),
16750 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, false, cx),
16751 )
16752 });
16753
16754 // Insert some excerpts.
16755 leader.update(cx, |leader, cx| {
16756 leader.buffer.update(cx, |multibuffer, cx| {
16757 multibuffer.set_excerpts_for_path(
16758 PathKey::with_sort_prefix(1, rel_path("b.txt").into_arc()),
16759 buffer_1.clone(),
16760 vec![
16761 Point::row_range(0..3),
16762 Point::row_range(1..6),
16763 Point::row_range(12..15),
16764 ],
16765 0,
16766 cx,
16767 );
16768 multibuffer.set_excerpts_for_path(
16769 PathKey::with_sort_prefix(1, rel_path("a.txt").into_arc()),
16770 buffer_2.clone(),
16771 vec![Point::row_range(0..6), Point::row_range(8..12)],
16772 0,
16773 cx,
16774 );
16775 });
16776 });
16777
16778 // Apply the update of adding the excerpts.
16779 follower_1
16780 .update_in(cx, |follower, window, cx| {
16781 follower.apply_update_proto(
16782 &project,
16783 update_message.borrow().clone().unwrap(),
16784 window,
16785 cx,
16786 )
16787 })
16788 .await
16789 .unwrap();
16790 assert_eq!(
16791 follower_1.update(cx, |editor, cx| editor.text(cx)),
16792 leader.update(cx, |editor, cx| editor.text(cx))
16793 );
16794 update_message.borrow_mut().take();
16795
16796 // Start following separately after it already has excerpts.
16797 let mut state_message =
16798 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16799 let workspace_entity = workspace.root(cx).unwrap();
16800 let follower_2 = cx
16801 .update_window(*workspace.deref(), |_, window, cx| {
16802 Editor::from_state_proto(
16803 workspace_entity,
16804 ViewId {
16805 creator: CollaboratorId::PeerId(PeerId::default()),
16806 id: 0,
16807 },
16808 &mut state_message,
16809 window,
16810 cx,
16811 )
16812 })
16813 .unwrap()
16814 .unwrap()
16815 .await
16816 .unwrap();
16817 assert_eq!(
16818 follower_2.update(cx, |editor, cx| editor.text(cx)),
16819 leader.update(cx, |editor, cx| editor.text(cx))
16820 );
16821
16822 // Remove some excerpts.
16823 leader.update(cx, |leader, cx| {
16824 leader.buffer.update(cx, |multibuffer, cx| {
16825 let excerpt_ids = multibuffer.excerpt_ids();
16826 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
16827 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
16828 });
16829 });
16830
16831 // Apply the update of removing the excerpts.
16832 follower_1
16833 .update_in(cx, |follower, window, cx| {
16834 follower.apply_update_proto(
16835 &project,
16836 update_message.borrow().clone().unwrap(),
16837 window,
16838 cx,
16839 )
16840 })
16841 .await
16842 .unwrap();
16843 follower_2
16844 .update_in(cx, |follower, window, cx| {
16845 follower.apply_update_proto(
16846 &project,
16847 update_message.borrow().clone().unwrap(),
16848 window,
16849 cx,
16850 )
16851 })
16852 .await
16853 .unwrap();
16854 update_message.borrow_mut().take();
16855 assert_eq!(
16856 follower_1.update(cx, |editor, cx| editor.text(cx)),
16857 leader.update(cx, |editor, cx| editor.text(cx))
16858 );
16859}
16860
16861#[gpui::test]
16862async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16863 init_test(cx, |_| {});
16864
16865 let mut cx = EditorTestContext::new(cx).await;
16866 let lsp_store =
16867 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
16868
16869 cx.set_state(indoc! {"
16870 ˇfn func(abc def: i32) -> u32 {
16871 }
16872 "});
16873
16874 cx.update(|_, cx| {
16875 lsp_store.update(cx, |lsp_store, cx| {
16876 lsp_store
16877 .update_diagnostics(
16878 LanguageServerId(0),
16879 lsp::PublishDiagnosticsParams {
16880 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
16881 version: None,
16882 diagnostics: vec![
16883 lsp::Diagnostic {
16884 range: lsp::Range::new(
16885 lsp::Position::new(0, 11),
16886 lsp::Position::new(0, 12),
16887 ),
16888 severity: Some(lsp::DiagnosticSeverity::ERROR),
16889 ..Default::default()
16890 },
16891 lsp::Diagnostic {
16892 range: lsp::Range::new(
16893 lsp::Position::new(0, 12),
16894 lsp::Position::new(0, 15),
16895 ),
16896 severity: Some(lsp::DiagnosticSeverity::ERROR),
16897 ..Default::default()
16898 },
16899 lsp::Diagnostic {
16900 range: lsp::Range::new(
16901 lsp::Position::new(0, 25),
16902 lsp::Position::new(0, 28),
16903 ),
16904 severity: Some(lsp::DiagnosticSeverity::ERROR),
16905 ..Default::default()
16906 },
16907 ],
16908 },
16909 None,
16910 DiagnosticSourceKind::Pushed,
16911 &[],
16912 cx,
16913 )
16914 .unwrap()
16915 });
16916 });
16917
16918 executor.run_until_parked();
16919
16920 cx.update_editor(|editor, window, cx| {
16921 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16922 });
16923
16924 cx.assert_editor_state(indoc! {"
16925 fn func(abc def: i32) -> ˇu32 {
16926 }
16927 "});
16928
16929 cx.update_editor(|editor, window, cx| {
16930 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16931 });
16932
16933 cx.assert_editor_state(indoc! {"
16934 fn func(abc ˇdef: i32) -> u32 {
16935 }
16936 "});
16937
16938 cx.update_editor(|editor, window, cx| {
16939 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16940 });
16941
16942 cx.assert_editor_state(indoc! {"
16943 fn func(abcˇ def: i32) -> u32 {
16944 }
16945 "});
16946
16947 cx.update_editor(|editor, window, cx| {
16948 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16949 });
16950
16951 cx.assert_editor_state(indoc! {"
16952 fn func(abc def: i32) -> ˇu32 {
16953 }
16954 "});
16955}
16956
16957#[gpui::test]
16958async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16959 init_test(cx, |_| {});
16960
16961 let mut cx = EditorTestContext::new(cx).await;
16962
16963 let diff_base = r#"
16964 use some::mod;
16965
16966 const A: u32 = 42;
16967
16968 fn main() {
16969 println!("hello");
16970
16971 println!("world");
16972 }
16973 "#
16974 .unindent();
16975
16976 // Edits are modified, removed, modified, added
16977 cx.set_state(
16978 &r#"
16979 use some::modified;
16980
16981 ˇ
16982 fn main() {
16983 println!("hello there");
16984
16985 println!("around the");
16986 println!("world");
16987 }
16988 "#
16989 .unindent(),
16990 );
16991
16992 cx.set_head_text(&diff_base);
16993 executor.run_until_parked();
16994
16995 cx.update_editor(|editor, window, cx| {
16996 //Wrap around the bottom of the buffer
16997 for _ in 0..3 {
16998 editor.go_to_next_hunk(&GoToHunk, window, cx);
16999 }
17000 });
17001
17002 cx.assert_editor_state(
17003 &r#"
17004 ˇuse some::modified;
17005
17006
17007 fn main() {
17008 println!("hello there");
17009
17010 println!("around the");
17011 println!("world");
17012 }
17013 "#
17014 .unindent(),
17015 );
17016
17017 cx.update_editor(|editor, window, cx| {
17018 //Wrap around the top of the buffer
17019 for _ in 0..2 {
17020 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17021 }
17022 });
17023
17024 cx.assert_editor_state(
17025 &r#"
17026 use some::modified;
17027
17028
17029 fn main() {
17030 ˇ println!("hello there");
17031
17032 println!("around the");
17033 println!("world");
17034 }
17035 "#
17036 .unindent(),
17037 );
17038
17039 cx.update_editor(|editor, window, cx| {
17040 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17041 });
17042
17043 cx.assert_editor_state(
17044 &r#"
17045 use some::modified;
17046
17047 ˇ
17048 fn main() {
17049 println!("hello there");
17050
17051 println!("around the");
17052 println!("world");
17053 }
17054 "#
17055 .unindent(),
17056 );
17057
17058 cx.update_editor(|editor, window, cx| {
17059 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17060 });
17061
17062 cx.assert_editor_state(
17063 &r#"
17064 ˇuse some::modified;
17065
17066
17067 fn main() {
17068 println!("hello there");
17069
17070 println!("around the");
17071 println!("world");
17072 }
17073 "#
17074 .unindent(),
17075 );
17076
17077 cx.update_editor(|editor, window, cx| {
17078 for _ in 0..2 {
17079 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17080 }
17081 });
17082
17083 cx.assert_editor_state(
17084 &r#"
17085 use some::modified;
17086
17087
17088 fn main() {
17089 ˇ println!("hello there");
17090
17091 println!("around the");
17092 println!("world");
17093 }
17094 "#
17095 .unindent(),
17096 );
17097
17098 cx.update_editor(|editor, window, cx| {
17099 editor.fold(&Fold, window, cx);
17100 });
17101
17102 cx.update_editor(|editor, window, cx| {
17103 editor.go_to_next_hunk(&GoToHunk, window, cx);
17104 });
17105
17106 cx.assert_editor_state(
17107 &r#"
17108 ˇuse some::modified;
17109
17110
17111 fn main() {
17112 println!("hello there");
17113
17114 println!("around the");
17115 println!("world");
17116 }
17117 "#
17118 .unindent(),
17119 );
17120}
17121
17122#[test]
17123fn test_split_words() {
17124 fn split(text: &str) -> Vec<&str> {
17125 split_words(text).collect()
17126 }
17127
17128 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
17129 assert_eq!(split("hello_world"), &["hello_", "world"]);
17130 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
17131 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
17132 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
17133 assert_eq!(split("helloworld"), &["helloworld"]);
17134
17135 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
17136}
17137
17138#[gpui::test]
17139async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
17140 init_test(cx, |_| {});
17141
17142 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
17143 let mut assert = |before, after| {
17144 let _state_context = cx.set_state(before);
17145 cx.run_until_parked();
17146 cx.update_editor(|editor, window, cx| {
17147 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
17148 });
17149 cx.run_until_parked();
17150 cx.assert_editor_state(after);
17151 };
17152
17153 // Outside bracket jumps to outside of matching bracket
17154 assert("console.logˇ(var);", "console.log(var)ˇ;");
17155 assert("console.log(var)ˇ;", "console.logˇ(var);");
17156
17157 // Inside bracket jumps to inside of matching bracket
17158 assert("console.log(ˇvar);", "console.log(varˇ);");
17159 assert("console.log(varˇ);", "console.log(ˇvar);");
17160
17161 // When outside a bracket and inside, favor jumping to the inside bracket
17162 assert(
17163 "console.log('foo', [1, 2, 3]ˇ);",
17164 "console.log(ˇ'foo', [1, 2, 3]);",
17165 );
17166 assert(
17167 "console.log(ˇ'foo', [1, 2, 3]);",
17168 "console.log('foo', [1, 2, 3]ˇ);",
17169 );
17170
17171 // Bias forward if two options are equally likely
17172 assert(
17173 "let result = curried_fun()ˇ();",
17174 "let result = curried_fun()()ˇ;",
17175 );
17176
17177 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
17178 assert(
17179 indoc! {"
17180 function test() {
17181 console.log('test')ˇ
17182 }"},
17183 indoc! {"
17184 function test() {
17185 console.logˇ('test')
17186 }"},
17187 );
17188}
17189
17190#[gpui::test]
17191async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
17192 init_test(cx, |_| {});
17193
17194 let fs = FakeFs::new(cx.executor());
17195 fs.insert_tree(
17196 path!("/a"),
17197 json!({
17198 "main.rs": "fn main() { let a = 5; }",
17199 "other.rs": "// Test file",
17200 }),
17201 )
17202 .await;
17203 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17204
17205 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17206 language_registry.add(Arc::new(Language::new(
17207 LanguageConfig {
17208 name: "Rust".into(),
17209 matcher: LanguageMatcher {
17210 path_suffixes: vec!["rs".to_string()],
17211 ..Default::default()
17212 },
17213 brackets: BracketPairConfig {
17214 pairs: vec![BracketPair {
17215 start: "{".to_string(),
17216 end: "}".to_string(),
17217 close: true,
17218 surround: true,
17219 newline: true,
17220 }],
17221 disabled_scopes_by_bracket_ix: Vec::new(),
17222 },
17223 ..Default::default()
17224 },
17225 Some(tree_sitter_rust::LANGUAGE.into()),
17226 )));
17227 let mut fake_servers = language_registry.register_fake_lsp(
17228 "Rust",
17229 FakeLspAdapter {
17230 capabilities: lsp::ServerCapabilities {
17231 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
17232 first_trigger_character: "{".to_string(),
17233 more_trigger_character: None,
17234 }),
17235 ..Default::default()
17236 },
17237 ..Default::default()
17238 },
17239 );
17240
17241 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17242
17243 let cx = &mut VisualTestContext::from_window(*workspace, cx);
17244
17245 let worktree_id = workspace
17246 .update(cx, |workspace, _, cx| {
17247 workspace.project().update(cx, |project, cx| {
17248 project.worktrees(cx).next().unwrap().read(cx).id()
17249 })
17250 })
17251 .unwrap();
17252
17253 let buffer = project
17254 .update(cx, |project, cx| {
17255 project.open_local_buffer(path!("/a/main.rs"), cx)
17256 })
17257 .await
17258 .unwrap();
17259 let editor_handle = workspace
17260 .update(cx, |workspace, window, cx| {
17261 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
17262 })
17263 .unwrap()
17264 .await
17265 .unwrap()
17266 .downcast::<Editor>()
17267 .unwrap();
17268
17269 cx.executor().start_waiting();
17270 let fake_server = fake_servers.next().await.unwrap();
17271
17272 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
17273 |params, _| async move {
17274 assert_eq!(
17275 params.text_document_position.text_document.uri,
17276 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
17277 );
17278 assert_eq!(
17279 params.text_document_position.position,
17280 lsp::Position::new(0, 21),
17281 );
17282
17283 Ok(Some(vec![lsp::TextEdit {
17284 new_text: "]".to_string(),
17285 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17286 }]))
17287 },
17288 );
17289
17290 editor_handle.update_in(cx, |editor, window, cx| {
17291 window.focus(&editor.focus_handle(cx));
17292 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17293 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
17294 });
17295 editor.handle_input("{", window, cx);
17296 });
17297
17298 cx.executor().run_until_parked();
17299
17300 buffer.update(cx, |buffer, _| {
17301 assert_eq!(
17302 buffer.text(),
17303 "fn main() { let a = {5}; }",
17304 "No extra braces from on type formatting should appear in the buffer"
17305 )
17306 });
17307}
17308
17309#[gpui::test(iterations = 20, seeds(31))]
17310async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
17311 init_test(cx, |_| {});
17312
17313 let mut cx = EditorLspTestContext::new_rust(
17314 lsp::ServerCapabilities {
17315 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
17316 first_trigger_character: ".".to_string(),
17317 more_trigger_character: None,
17318 }),
17319 ..Default::default()
17320 },
17321 cx,
17322 )
17323 .await;
17324
17325 cx.update_buffer(|buffer, _| {
17326 // This causes autoindent to be async.
17327 buffer.set_sync_parse_timeout(Duration::ZERO)
17328 });
17329
17330 cx.set_state("fn c() {\n d()ˇ\n}\n");
17331 cx.simulate_keystroke("\n");
17332 cx.run_until_parked();
17333
17334 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
17335 let mut request =
17336 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
17337 let buffer_cloned = buffer_cloned.clone();
17338 async move {
17339 buffer_cloned.update(&mut cx, |buffer, _| {
17340 assert_eq!(
17341 buffer.text(),
17342 "fn c() {\n d()\n .\n}\n",
17343 "OnTypeFormatting should triggered after autoindent applied"
17344 )
17345 })?;
17346
17347 Ok(Some(vec![]))
17348 }
17349 });
17350
17351 cx.simulate_keystroke(".");
17352 cx.run_until_parked();
17353
17354 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
17355 assert!(request.next().await.is_some());
17356 request.close();
17357 assert!(request.next().await.is_none());
17358}
17359
17360#[gpui::test]
17361async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
17362 init_test(cx, |_| {});
17363
17364 let fs = FakeFs::new(cx.executor());
17365 fs.insert_tree(
17366 path!("/a"),
17367 json!({
17368 "main.rs": "fn main() { let a = 5; }",
17369 "other.rs": "// Test file",
17370 }),
17371 )
17372 .await;
17373
17374 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17375
17376 let server_restarts = Arc::new(AtomicUsize::new(0));
17377 let closure_restarts = Arc::clone(&server_restarts);
17378 let language_server_name = "test language server";
17379 let language_name: LanguageName = "Rust".into();
17380
17381 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17382 language_registry.add(Arc::new(Language::new(
17383 LanguageConfig {
17384 name: language_name.clone(),
17385 matcher: LanguageMatcher {
17386 path_suffixes: vec!["rs".to_string()],
17387 ..Default::default()
17388 },
17389 ..Default::default()
17390 },
17391 Some(tree_sitter_rust::LANGUAGE.into()),
17392 )));
17393 let mut fake_servers = language_registry.register_fake_lsp(
17394 "Rust",
17395 FakeLspAdapter {
17396 name: language_server_name,
17397 initialization_options: Some(json!({
17398 "testOptionValue": true
17399 })),
17400 initializer: Some(Box::new(move |fake_server| {
17401 let task_restarts = Arc::clone(&closure_restarts);
17402 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
17403 task_restarts.fetch_add(1, atomic::Ordering::Release);
17404 futures::future::ready(Ok(()))
17405 });
17406 })),
17407 ..Default::default()
17408 },
17409 );
17410
17411 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17412 let _buffer = project
17413 .update(cx, |project, cx| {
17414 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
17415 })
17416 .await
17417 .unwrap();
17418 let _fake_server = fake_servers.next().await.unwrap();
17419 update_test_language_settings(cx, |language_settings| {
17420 language_settings.languages.0.insert(
17421 language_name.clone().0,
17422 LanguageSettingsContent {
17423 tab_size: NonZeroU32::new(8),
17424 ..Default::default()
17425 },
17426 );
17427 });
17428 cx.executor().run_until_parked();
17429 assert_eq!(
17430 server_restarts.load(atomic::Ordering::Acquire),
17431 0,
17432 "Should not restart LSP server on an unrelated change"
17433 );
17434
17435 update_test_project_settings(cx, |project_settings| {
17436 project_settings.lsp.insert(
17437 "Some other server name".into(),
17438 LspSettings {
17439 binary: None,
17440 settings: None,
17441 initialization_options: Some(json!({
17442 "some other init value": false
17443 })),
17444 enable_lsp_tasks: false,
17445 fetch: None,
17446 },
17447 );
17448 });
17449 cx.executor().run_until_parked();
17450 assert_eq!(
17451 server_restarts.load(atomic::Ordering::Acquire),
17452 0,
17453 "Should not restart LSP server on an unrelated LSP settings change"
17454 );
17455
17456 update_test_project_settings(cx, |project_settings| {
17457 project_settings.lsp.insert(
17458 language_server_name.into(),
17459 LspSettings {
17460 binary: None,
17461 settings: None,
17462 initialization_options: Some(json!({
17463 "anotherInitValue": false
17464 })),
17465 enable_lsp_tasks: false,
17466 fetch: None,
17467 },
17468 );
17469 });
17470 cx.executor().run_until_parked();
17471 assert_eq!(
17472 server_restarts.load(atomic::Ordering::Acquire),
17473 1,
17474 "Should restart LSP server on a related LSP settings change"
17475 );
17476
17477 update_test_project_settings(cx, |project_settings| {
17478 project_settings.lsp.insert(
17479 language_server_name.into(),
17480 LspSettings {
17481 binary: None,
17482 settings: None,
17483 initialization_options: Some(json!({
17484 "anotherInitValue": false
17485 })),
17486 enable_lsp_tasks: false,
17487 fetch: None,
17488 },
17489 );
17490 });
17491 cx.executor().run_until_parked();
17492 assert_eq!(
17493 server_restarts.load(atomic::Ordering::Acquire),
17494 1,
17495 "Should not restart LSP server on a related LSP settings change that is the same"
17496 );
17497
17498 update_test_project_settings(cx, |project_settings| {
17499 project_settings.lsp.insert(
17500 language_server_name.into(),
17501 LspSettings {
17502 binary: None,
17503 settings: None,
17504 initialization_options: None,
17505 enable_lsp_tasks: false,
17506 fetch: None,
17507 },
17508 );
17509 });
17510 cx.executor().run_until_parked();
17511 assert_eq!(
17512 server_restarts.load(atomic::Ordering::Acquire),
17513 2,
17514 "Should restart LSP server on another related LSP settings change"
17515 );
17516}
17517
17518#[gpui::test]
17519async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
17520 init_test(cx, |_| {});
17521
17522 let mut cx = EditorLspTestContext::new_rust(
17523 lsp::ServerCapabilities {
17524 completion_provider: Some(lsp::CompletionOptions {
17525 trigger_characters: Some(vec![".".to_string()]),
17526 resolve_provider: Some(true),
17527 ..Default::default()
17528 }),
17529 ..Default::default()
17530 },
17531 cx,
17532 )
17533 .await;
17534
17535 cx.set_state("fn main() { let a = 2ˇ; }");
17536 cx.simulate_keystroke(".");
17537 let completion_item = lsp::CompletionItem {
17538 label: "some".into(),
17539 kind: Some(lsp::CompletionItemKind::SNIPPET),
17540 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
17541 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
17542 kind: lsp::MarkupKind::Markdown,
17543 value: "```rust\nSome(2)\n```".to_string(),
17544 })),
17545 deprecated: Some(false),
17546 sort_text: Some("fffffff2".to_string()),
17547 filter_text: Some("some".to_string()),
17548 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
17549 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17550 range: lsp::Range {
17551 start: lsp::Position {
17552 line: 0,
17553 character: 22,
17554 },
17555 end: lsp::Position {
17556 line: 0,
17557 character: 22,
17558 },
17559 },
17560 new_text: "Some(2)".to_string(),
17561 })),
17562 additional_text_edits: Some(vec![lsp::TextEdit {
17563 range: lsp::Range {
17564 start: lsp::Position {
17565 line: 0,
17566 character: 20,
17567 },
17568 end: lsp::Position {
17569 line: 0,
17570 character: 22,
17571 },
17572 },
17573 new_text: "".to_string(),
17574 }]),
17575 ..Default::default()
17576 };
17577
17578 let closure_completion_item = completion_item.clone();
17579 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17580 let task_completion_item = closure_completion_item.clone();
17581 async move {
17582 Ok(Some(lsp::CompletionResponse::Array(vec![
17583 task_completion_item,
17584 ])))
17585 }
17586 });
17587
17588 request.next().await;
17589
17590 cx.condition(|editor, _| editor.context_menu_visible())
17591 .await;
17592 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
17593 editor
17594 .confirm_completion(&ConfirmCompletion::default(), window, cx)
17595 .unwrap()
17596 });
17597 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
17598
17599 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
17600 let task_completion_item = completion_item.clone();
17601 async move { Ok(task_completion_item) }
17602 })
17603 .next()
17604 .await
17605 .unwrap();
17606 apply_additional_edits.await.unwrap();
17607 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
17608}
17609
17610#[gpui::test]
17611async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
17612 init_test(cx, |_| {});
17613
17614 let mut cx = EditorLspTestContext::new_rust(
17615 lsp::ServerCapabilities {
17616 completion_provider: Some(lsp::CompletionOptions {
17617 trigger_characters: Some(vec![".".to_string()]),
17618 resolve_provider: Some(true),
17619 ..Default::default()
17620 }),
17621 ..Default::default()
17622 },
17623 cx,
17624 )
17625 .await;
17626
17627 cx.set_state("fn main() { let a = 2ˇ; }");
17628 cx.simulate_keystroke(".");
17629
17630 let item1 = lsp::CompletionItem {
17631 label: "method id()".to_string(),
17632 filter_text: Some("id".to_string()),
17633 detail: None,
17634 documentation: None,
17635 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17636 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17637 new_text: ".id".to_string(),
17638 })),
17639 ..lsp::CompletionItem::default()
17640 };
17641
17642 let item2 = lsp::CompletionItem {
17643 label: "other".to_string(),
17644 filter_text: Some("other".to_string()),
17645 detail: None,
17646 documentation: None,
17647 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17648 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17649 new_text: ".other".to_string(),
17650 })),
17651 ..lsp::CompletionItem::default()
17652 };
17653
17654 let item1 = item1.clone();
17655 cx.set_request_handler::<lsp::request::Completion, _, _>({
17656 let item1 = item1.clone();
17657 move |_, _, _| {
17658 let item1 = item1.clone();
17659 let item2 = item2.clone();
17660 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
17661 }
17662 })
17663 .next()
17664 .await;
17665
17666 cx.condition(|editor, _| editor.context_menu_visible())
17667 .await;
17668 cx.update_editor(|editor, _, _| {
17669 let context_menu = editor.context_menu.borrow_mut();
17670 let context_menu = context_menu
17671 .as_ref()
17672 .expect("Should have the context menu deployed");
17673 match context_menu {
17674 CodeContextMenu::Completions(completions_menu) => {
17675 let completions = completions_menu.completions.borrow_mut();
17676 assert_eq!(
17677 completions
17678 .iter()
17679 .map(|completion| &completion.label.text)
17680 .collect::<Vec<_>>(),
17681 vec!["method id()", "other"]
17682 )
17683 }
17684 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17685 }
17686 });
17687
17688 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
17689 let item1 = item1.clone();
17690 move |_, item_to_resolve, _| {
17691 let item1 = item1.clone();
17692 async move {
17693 if item1 == item_to_resolve {
17694 Ok(lsp::CompletionItem {
17695 label: "method id()".to_string(),
17696 filter_text: Some("id".to_string()),
17697 detail: Some("Now resolved!".to_string()),
17698 documentation: Some(lsp::Documentation::String("Docs".to_string())),
17699 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17700 range: lsp::Range::new(
17701 lsp::Position::new(0, 22),
17702 lsp::Position::new(0, 22),
17703 ),
17704 new_text: ".id".to_string(),
17705 })),
17706 ..lsp::CompletionItem::default()
17707 })
17708 } else {
17709 Ok(item_to_resolve)
17710 }
17711 }
17712 }
17713 })
17714 .next()
17715 .await
17716 .unwrap();
17717 cx.run_until_parked();
17718
17719 cx.update_editor(|editor, window, cx| {
17720 editor.context_menu_next(&Default::default(), window, cx);
17721 });
17722
17723 cx.update_editor(|editor, _, _| {
17724 let context_menu = editor.context_menu.borrow_mut();
17725 let context_menu = context_menu
17726 .as_ref()
17727 .expect("Should have the context menu deployed");
17728 match context_menu {
17729 CodeContextMenu::Completions(completions_menu) => {
17730 let completions = completions_menu.completions.borrow_mut();
17731 assert_eq!(
17732 completions
17733 .iter()
17734 .map(|completion| &completion.label.text)
17735 .collect::<Vec<_>>(),
17736 vec!["method id() Now resolved!", "other"],
17737 "Should update first completion label, but not second as the filter text did not match."
17738 );
17739 }
17740 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17741 }
17742 });
17743}
17744
17745#[gpui::test]
17746async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
17747 init_test(cx, |_| {});
17748 let mut cx = EditorLspTestContext::new_rust(
17749 lsp::ServerCapabilities {
17750 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
17751 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
17752 completion_provider: Some(lsp::CompletionOptions {
17753 resolve_provider: Some(true),
17754 ..Default::default()
17755 }),
17756 ..Default::default()
17757 },
17758 cx,
17759 )
17760 .await;
17761 cx.set_state(indoc! {"
17762 struct TestStruct {
17763 field: i32
17764 }
17765
17766 fn mainˇ() {
17767 let unused_var = 42;
17768 let test_struct = TestStruct { field: 42 };
17769 }
17770 "});
17771 let symbol_range = cx.lsp_range(indoc! {"
17772 struct TestStruct {
17773 field: i32
17774 }
17775
17776 «fn main»() {
17777 let unused_var = 42;
17778 let test_struct = TestStruct { field: 42 };
17779 }
17780 "});
17781 let mut hover_requests =
17782 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
17783 Ok(Some(lsp::Hover {
17784 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
17785 kind: lsp::MarkupKind::Markdown,
17786 value: "Function documentation".to_string(),
17787 }),
17788 range: Some(symbol_range),
17789 }))
17790 });
17791
17792 // Case 1: Test that code action menu hide hover popover
17793 cx.dispatch_action(Hover);
17794 hover_requests.next().await;
17795 cx.condition(|editor, _| editor.hover_state.visible()).await;
17796 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
17797 move |_, _, _| async move {
17798 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
17799 lsp::CodeAction {
17800 title: "Remove unused variable".to_string(),
17801 kind: Some(CodeActionKind::QUICKFIX),
17802 edit: Some(lsp::WorkspaceEdit {
17803 changes: Some(
17804 [(
17805 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
17806 vec![lsp::TextEdit {
17807 range: lsp::Range::new(
17808 lsp::Position::new(5, 4),
17809 lsp::Position::new(5, 27),
17810 ),
17811 new_text: "".to_string(),
17812 }],
17813 )]
17814 .into_iter()
17815 .collect(),
17816 ),
17817 ..Default::default()
17818 }),
17819 ..Default::default()
17820 },
17821 )]))
17822 },
17823 );
17824 cx.update_editor(|editor, window, cx| {
17825 editor.toggle_code_actions(
17826 &ToggleCodeActions {
17827 deployed_from: None,
17828 quick_launch: false,
17829 },
17830 window,
17831 cx,
17832 );
17833 });
17834 code_action_requests.next().await;
17835 cx.run_until_parked();
17836 cx.condition(|editor, _| editor.context_menu_visible())
17837 .await;
17838 cx.update_editor(|editor, _, _| {
17839 assert!(
17840 !editor.hover_state.visible(),
17841 "Hover popover should be hidden when code action menu is shown"
17842 );
17843 // Hide code actions
17844 editor.context_menu.take();
17845 });
17846
17847 // Case 2: Test that code completions hide hover popover
17848 cx.dispatch_action(Hover);
17849 hover_requests.next().await;
17850 cx.condition(|editor, _| editor.hover_state.visible()).await;
17851 let counter = Arc::new(AtomicUsize::new(0));
17852 let mut completion_requests =
17853 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17854 let counter = counter.clone();
17855 async move {
17856 counter.fetch_add(1, atomic::Ordering::Release);
17857 Ok(Some(lsp::CompletionResponse::Array(vec![
17858 lsp::CompletionItem {
17859 label: "main".into(),
17860 kind: Some(lsp::CompletionItemKind::FUNCTION),
17861 detail: Some("() -> ()".to_string()),
17862 ..Default::default()
17863 },
17864 lsp::CompletionItem {
17865 label: "TestStruct".into(),
17866 kind: Some(lsp::CompletionItemKind::STRUCT),
17867 detail: Some("struct TestStruct".to_string()),
17868 ..Default::default()
17869 },
17870 ])))
17871 }
17872 });
17873 cx.update_editor(|editor, window, cx| {
17874 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
17875 });
17876 completion_requests.next().await;
17877 cx.condition(|editor, _| editor.context_menu_visible())
17878 .await;
17879 cx.update_editor(|editor, _, _| {
17880 assert!(
17881 !editor.hover_state.visible(),
17882 "Hover popover should be hidden when completion menu is shown"
17883 );
17884 });
17885}
17886
17887#[gpui::test]
17888async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
17889 init_test(cx, |_| {});
17890
17891 let mut cx = EditorLspTestContext::new_rust(
17892 lsp::ServerCapabilities {
17893 completion_provider: Some(lsp::CompletionOptions {
17894 trigger_characters: Some(vec![".".to_string()]),
17895 resolve_provider: Some(true),
17896 ..Default::default()
17897 }),
17898 ..Default::default()
17899 },
17900 cx,
17901 )
17902 .await;
17903
17904 cx.set_state("fn main() { let a = 2ˇ; }");
17905 cx.simulate_keystroke(".");
17906
17907 let unresolved_item_1 = lsp::CompletionItem {
17908 label: "id".to_string(),
17909 filter_text: Some("id".to_string()),
17910 detail: None,
17911 documentation: None,
17912 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17913 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17914 new_text: ".id".to_string(),
17915 })),
17916 ..lsp::CompletionItem::default()
17917 };
17918 let resolved_item_1 = lsp::CompletionItem {
17919 additional_text_edits: Some(vec![lsp::TextEdit {
17920 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17921 new_text: "!!".to_string(),
17922 }]),
17923 ..unresolved_item_1.clone()
17924 };
17925 let unresolved_item_2 = lsp::CompletionItem {
17926 label: "other".to_string(),
17927 filter_text: Some("other".to_string()),
17928 detail: None,
17929 documentation: None,
17930 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17931 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17932 new_text: ".other".to_string(),
17933 })),
17934 ..lsp::CompletionItem::default()
17935 };
17936 let resolved_item_2 = lsp::CompletionItem {
17937 additional_text_edits: Some(vec![lsp::TextEdit {
17938 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17939 new_text: "??".to_string(),
17940 }]),
17941 ..unresolved_item_2.clone()
17942 };
17943
17944 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
17945 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
17946 cx.lsp
17947 .server
17948 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
17949 let unresolved_item_1 = unresolved_item_1.clone();
17950 let resolved_item_1 = resolved_item_1.clone();
17951 let unresolved_item_2 = unresolved_item_2.clone();
17952 let resolved_item_2 = resolved_item_2.clone();
17953 let resolve_requests_1 = resolve_requests_1.clone();
17954 let resolve_requests_2 = resolve_requests_2.clone();
17955 move |unresolved_request, _| {
17956 let unresolved_item_1 = unresolved_item_1.clone();
17957 let resolved_item_1 = resolved_item_1.clone();
17958 let unresolved_item_2 = unresolved_item_2.clone();
17959 let resolved_item_2 = resolved_item_2.clone();
17960 let resolve_requests_1 = resolve_requests_1.clone();
17961 let resolve_requests_2 = resolve_requests_2.clone();
17962 async move {
17963 if unresolved_request == unresolved_item_1 {
17964 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
17965 Ok(resolved_item_1.clone())
17966 } else if unresolved_request == unresolved_item_2 {
17967 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
17968 Ok(resolved_item_2.clone())
17969 } else {
17970 panic!("Unexpected completion item {unresolved_request:?}")
17971 }
17972 }
17973 }
17974 })
17975 .detach();
17976
17977 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17978 let unresolved_item_1 = unresolved_item_1.clone();
17979 let unresolved_item_2 = unresolved_item_2.clone();
17980 async move {
17981 Ok(Some(lsp::CompletionResponse::Array(vec![
17982 unresolved_item_1,
17983 unresolved_item_2,
17984 ])))
17985 }
17986 })
17987 .next()
17988 .await;
17989
17990 cx.condition(|editor, _| editor.context_menu_visible())
17991 .await;
17992 cx.update_editor(|editor, _, _| {
17993 let context_menu = editor.context_menu.borrow_mut();
17994 let context_menu = context_menu
17995 .as_ref()
17996 .expect("Should have the context menu deployed");
17997 match context_menu {
17998 CodeContextMenu::Completions(completions_menu) => {
17999 let completions = completions_menu.completions.borrow_mut();
18000 assert_eq!(
18001 completions
18002 .iter()
18003 .map(|completion| &completion.label.text)
18004 .collect::<Vec<_>>(),
18005 vec!["id", "other"]
18006 )
18007 }
18008 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
18009 }
18010 });
18011 cx.run_until_parked();
18012
18013 cx.update_editor(|editor, window, cx| {
18014 editor.context_menu_next(&ContextMenuNext, window, cx);
18015 });
18016 cx.run_until_parked();
18017 cx.update_editor(|editor, window, cx| {
18018 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
18019 });
18020 cx.run_until_parked();
18021 cx.update_editor(|editor, window, cx| {
18022 editor.context_menu_next(&ContextMenuNext, window, cx);
18023 });
18024 cx.run_until_parked();
18025 cx.update_editor(|editor, window, cx| {
18026 editor
18027 .compose_completion(&ComposeCompletion::default(), window, cx)
18028 .expect("No task returned")
18029 })
18030 .await
18031 .expect("Completion failed");
18032 cx.run_until_parked();
18033
18034 cx.update_editor(|editor, _, cx| {
18035 assert_eq!(
18036 resolve_requests_1.load(atomic::Ordering::Acquire),
18037 1,
18038 "Should always resolve once despite multiple selections"
18039 );
18040 assert_eq!(
18041 resolve_requests_2.load(atomic::Ordering::Acquire),
18042 1,
18043 "Should always resolve once after multiple selections and applying the completion"
18044 );
18045 assert_eq!(
18046 editor.text(cx),
18047 "fn main() { let a = ??.other; }",
18048 "Should use resolved data when applying the completion"
18049 );
18050 });
18051}
18052
18053#[gpui::test]
18054async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
18055 init_test(cx, |_| {});
18056
18057 let item_0 = lsp::CompletionItem {
18058 label: "abs".into(),
18059 insert_text: Some("abs".into()),
18060 data: Some(json!({ "very": "special"})),
18061 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
18062 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
18063 lsp::InsertReplaceEdit {
18064 new_text: "abs".to_string(),
18065 insert: lsp::Range::default(),
18066 replace: lsp::Range::default(),
18067 },
18068 )),
18069 ..lsp::CompletionItem::default()
18070 };
18071 let items = iter::once(item_0.clone())
18072 .chain((11..51).map(|i| lsp::CompletionItem {
18073 label: format!("item_{}", i),
18074 insert_text: Some(format!("item_{}", i)),
18075 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
18076 ..lsp::CompletionItem::default()
18077 }))
18078 .collect::<Vec<_>>();
18079
18080 let default_commit_characters = vec!["?".to_string()];
18081 let default_data = json!({ "default": "data"});
18082 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
18083 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
18084 let default_edit_range = lsp::Range {
18085 start: lsp::Position {
18086 line: 0,
18087 character: 5,
18088 },
18089 end: lsp::Position {
18090 line: 0,
18091 character: 5,
18092 },
18093 };
18094
18095 let mut cx = EditorLspTestContext::new_rust(
18096 lsp::ServerCapabilities {
18097 completion_provider: Some(lsp::CompletionOptions {
18098 trigger_characters: Some(vec![".".to_string()]),
18099 resolve_provider: Some(true),
18100 ..Default::default()
18101 }),
18102 ..Default::default()
18103 },
18104 cx,
18105 )
18106 .await;
18107
18108 cx.set_state("fn main() { let a = 2ˇ; }");
18109 cx.simulate_keystroke(".");
18110
18111 let completion_data = default_data.clone();
18112 let completion_characters = default_commit_characters.clone();
18113 let completion_items = items.clone();
18114 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18115 let default_data = completion_data.clone();
18116 let default_commit_characters = completion_characters.clone();
18117 let items = completion_items.clone();
18118 async move {
18119 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
18120 items,
18121 item_defaults: Some(lsp::CompletionListItemDefaults {
18122 data: Some(default_data.clone()),
18123 commit_characters: Some(default_commit_characters.clone()),
18124 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
18125 default_edit_range,
18126 )),
18127 insert_text_format: Some(default_insert_text_format),
18128 insert_text_mode: Some(default_insert_text_mode),
18129 }),
18130 ..lsp::CompletionList::default()
18131 })))
18132 }
18133 })
18134 .next()
18135 .await;
18136
18137 let resolved_items = Arc::new(Mutex::new(Vec::new()));
18138 cx.lsp
18139 .server
18140 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
18141 let closure_resolved_items = resolved_items.clone();
18142 move |item_to_resolve, _| {
18143 let closure_resolved_items = closure_resolved_items.clone();
18144 async move {
18145 closure_resolved_items.lock().push(item_to_resolve.clone());
18146 Ok(item_to_resolve)
18147 }
18148 }
18149 })
18150 .detach();
18151
18152 cx.condition(|editor, _| editor.context_menu_visible())
18153 .await;
18154 cx.run_until_parked();
18155 cx.update_editor(|editor, _, _| {
18156 let menu = editor.context_menu.borrow_mut();
18157 match menu.as_ref().expect("should have the completions menu") {
18158 CodeContextMenu::Completions(completions_menu) => {
18159 assert_eq!(
18160 completions_menu
18161 .entries
18162 .borrow()
18163 .iter()
18164 .map(|mat| mat.string.clone())
18165 .collect::<Vec<String>>(),
18166 items
18167 .iter()
18168 .map(|completion| completion.label.clone())
18169 .collect::<Vec<String>>()
18170 );
18171 }
18172 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
18173 }
18174 });
18175 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
18176 // with 4 from the end.
18177 assert_eq!(
18178 *resolved_items.lock(),
18179 [&items[0..16], &items[items.len() - 4..items.len()]]
18180 .concat()
18181 .iter()
18182 .cloned()
18183 .map(|mut item| {
18184 if item.data.is_none() {
18185 item.data = Some(default_data.clone());
18186 }
18187 item
18188 })
18189 .collect::<Vec<lsp::CompletionItem>>(),
18190 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
18191 );
18192 resolved_items.lock().clear();
18193
18194 cx.update_editor(|editor, window, cx| {
18195 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
18196 });
18197 cx.run_until_parked();
18198 // Completions that have already been resolved are skipped.
18199 assert_eq!(
18200 *resolved_items.lock(),
18201 items[items.len() - 17..items.len() - 4]
18202 .iter()
18203 .cloned()
18204 .map(|mut item| {
18205 if item.data.is_none() {
18206 item.data = Some(default_data.clone());
18207 }
18208 item
18209 })
18210 .collect::<Vec<lsp::CompletionItem>>()
18211 );
18212 resolved_items.lock().clear();
18213}
18214
18215#[gpui::test]
18216async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
18217 init_test(cx, |_| {});
18218
18219 let mut cx = EditorLspTestContext::new(
18220 Language::new(
18221 LanguageConfig {
18222 matcher: LanguageMatcher {
18223 path_suffixes: vec!["jsx".into()],
18224 ..Default::default()
18225 },
18226 overrides: [(
18227 "element".into(),
18228 LanguageConfigOverride {
18229 completion_query_characters: Override::Set(['-'].into_iter().collect()),
18230 ..Default::default()
18231 },
18232 )]
18233 .into_iter()
18234 .collect(),
18235 ..Default::default()
18236 },
18237 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
18238 )
18239 .with_override_query("(jsx_self_closing_element) @element")
18240 .unwrap(),
18241 lsp::ServerCapabilities {
18242 completion_provider: Some(lsp::CompletionOptions {
18243 trigger_characters: Some(vec![":".to_string()]),
18244 ..Default::default()
18245 }),
18246 ..Default::default()
18247 },
18248 cx,
18249 )
18250 .await;
18251
18252 cx.lsp
18253 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
18254 Ok(Some(lsp::CompletionResponse::Array(vec![
18255 lsp::CompletionItem {
18256 label: "bg-blue".into(),
18257 ..Default::default()
18258 },
18259 lsp::CompletionItem {
18260 label: "bg-red".into(),
18261 ..Default::default()
18262 },
18263 lsp::CompletionItem {
18264 label: "bg-yellow".into(),
18265 ..Default::default()
18266 },
18267 ])))
18268 });
18269
18270 cx.set_state(r#"<p class="bgˇ" />"#);
18271
18272 // Trigger completion when typing a dash, because the dash is an extra
18273 // word character in the 'element' scope, which contains the cursor.
18274 cx.simulate_keystroke("-");
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!(
18280 completion_menu_entries(menu),
18281 &["bg-blue", "bg-red", "bg-yellow"]
18282 );
18283 } else {
18284 panic!("expected completion menu to be open");
18285 }
18286 });
18287
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-blue", "bg-yellow"]);
18294 } else {
18295 panic!("expected completion menu to be open");
18296 }
18297 });
18298
18299 // When filtering completions, consider the character after the '-' to
18300 // be the start of a subword.
18301 cx.set_state(r#"<p class="yelˇ" />"#);
18302 cx.simulate_keystroke("l");
18303 cx.executor().run_until_parked();
18304 cx.update_editor(|editor, _, _| {
18305 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18306 {
18307 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
18308 } else {
18309 panic!("expected completion menu to be open");
18310 }
18311 });
18312}
18313
18314fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
18315 let entries = menu.entries.borrow();
18316 entries.iter().map(|mat| mat.string.clone()).collect()
18317}
18318
18319#[gpui::test]
18320async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
18321 init_test(cx, |settings| {
18322 settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
18323 });
18324
18325 let fs = FakeFs::new(cx.executor());
18326 fs.insert_file(path!("/file.ts"), Default::default()).await;
18327
18328 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
18329 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18330
18331 language_registry.add(Arc::new(Language::new(
18332 LanguageConfig {
18333 name: "TypeScript".into(),
18334 matcher: LanguageMatcher {
18335 path_suffixes: vec!["ts".to_string()],
18336 ..Default::default()
18337 },
18338 ..Default::default()
18339 },
18340 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
18341 )));
18342 update_test_language_settings(cx, |settings| {
18343 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
18344 });
18345
18346 let test_plugin = "test_plugin";
18347 let _ = language_registry.register_fake_lsp(
18348 "TypeScript",
18349 FakeLspAdapter {
18350 prettier_plugins: vec![test_plugin],
18351 ..Default::default()
18352 },
18353 );
18354
18355 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
18356 let buffer = project
18357 .update(cx, |project, cx| {
18358 project.open_local_buffer(path!("/file.ts"), cx)
18359 })
18360 .await
18361 .unwrap();
18362
18363 let buffer_text = "one\ntwo\nthree\n";
18364 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
18365 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
18366 editor.update_in(cx, |editor, window, cx| {
18367 editor.set_text(buffer_text, window, cx)
18368 });
18369
18370 editor
18371 .update_in(cx, |editor, window, cx| {
18372 editor.perform_format(
18373 project.clone(),
18374 FormatTrigger::Manual,
18375 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18376 window,
18377 cx,
18378 )
18379 })
18380 .unwrap()
18381 .await;
18382 assert_eq!(
18383 editor.update(cx, |editor, cx| editor.text(cx)),
18384 buffer_text.to_string() + prettier_format_suffix,
18385 "Test prettier formatting was not applied to the original buffer text",
18386 );
18387
18388 update_test_language_settings(cx, |settings| {
18389 settings.defaults.formatter = Some(FormatterList::default())
18390 });
18391 let format = editor.update_in(cx, |editor, window, cx| {
18392 editor.perform_format(
18393 project.clone(),
18394 FormatTrigger::Manual,
18395 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18396 window,
18397 cx,
18398 )
18399 });
18400 format.await.unwrap();
18401 assert_eq!(
18402 editor.update(cx, |editor, cx| editor.text(cx)),
18403 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
18404 "Autoformatting (via test prettier) was not applied to the original buffer text",
18405 );
18406}
18407
18408#[gpui::test]
18409async fn test_addition_reverts(cx: &mut TestAppContext) {
18410 init_test(cx, |_| {});
18411 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18412 let base_text = indoc! {r#"
18413 struct Row;
18414 struct Row1;
18415 struct Row2;
18416
18417 struct Row4;
18418 struct Row5;
18419 struct Row6;
18420
18421 struct Row8;
18422 struct Row9;
18423 struct Row10;"#};
18424
18425 // When addition hunks are not adjacent to carets, no hunk revert is performed
18426 assert_hunk_revert(
18427 indoc! {r#"struct Row;
18428 struct Row1;
18429 struct Row1.1;
18430 struct Row1.2;
18431 struct Row2;ˇ
18432
18433 struct Row4;
18434 struct Row5;
18435 struct Row6;
18436
18437 struct Row8;
18438 ˇstruct Row9;
18439 struct Row9.1;
18440 struct Row9.2;
18441 struct Row9.3;
18442 struct Row10;"#},
18443 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18444 indoc! {r#"struct Row;
18445 struct Row1;
18446 struct Row1.1;
18447 struct Row1.2;
18448 struct Row2;ˇ
18449
18450 struct Row4;
18451 struct Row5;
18452 struct Row6;
18453
18454 struct Row8;
18455 ˇstruct Row9;
18456 struct Row9.1;
18457 struct Row9.2;
18458 struct Row9.3;
18459 struct Row10;"#},
18460 base_text,
18461 &mut cx,
18462 );
18463 // Same for selections
18464 assert_hunk_revert(
18465 indoc! {r#"struct Row;
18466 struct Row1;
18467 struct Row2;
18468 struct Row2.1;
18469 struct Row2.2;
18470 «ˇ
18471 struct Row4;
18472 struct» Row5;
18473 «struct Row6;
18474 ˇ»
18475 struct Row9.1;
18476 struct Row9.2;
18477 struct Row9.3;
18478 struct Row8;
18479 struct Row9;
18480 struct Row10;"#},
18481 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18482 indoc! {r#"struct Row;
18483 struct Row1;
18484 struct Row2;
18485 struct Row2.1;
18486 struct Row2.2;
18487 «ˇ
18488 struct Row4;
18489 struct» Row5;
18490 «struct Row6;
18491 ˇ»
18492 struct Row9.1;
18493 struct Row9.2;
18494 struct Row9.3;
18495 struct Row8;
18496 struct Row9;
18497 struct Row10;"#},
18498 base_text,
18499 &mut cx,
18500 );
18501
18502 // When carets and selections intersect the addition hunks, those are reverted.
18503 // Adjacent carets got merged.
18504 assert_hunk_revert(
18505 indoc! {r#"struct Row;
18506 ˇ// something on the top
18507 struct Row1;
18508 struct Row2;
18509 struct Roˇw3.1;
18510 struct Row2.2;
18511 struct Row2.3;ˇ
18512
18513 struct Row4;
18514 struct ˇRow5.1;
18515 struct Row5.2;
18516 struct «Rowˇ»5.3;
18517 struct Row5;
18518 struct Row6;
18519 ˇ
18520 struct Row9.1;
18521 struct «Rowˇ»9.2;
18522 struct «ˇRow»9.3;
18523 struct Row8;
18524 struct Row9;
18525 «ˇ// something on bottom»
18526 struct Row10;"#},
18527 vec![
18528 DiffHunkStatusKind::Added,
18529 DiffHunkStatusKind::Added,
18530 DiffHunkStatusKind::Added,
18531 DiffHunkStatusKind::Added,
18532 DiffHunkStatusKind::Added,
18533 ],
18534 indoc! {r#"struct Row;
18535 ˇstruct Row1;
18536 struct Row2;
18537 ˇ
18538 struct Row4;
18539 ˇstruct Row5;
18540 struct Row6;
18541 ˇ
18542 ˇstruct Row8;
18543 struct Row9;
18544 ˇstruct Row10;"#},
18545 base_text,
18546 &mut cx,
18547 );
18548}
18549
18550#[gpui::test]
18551async fn test_modification_reverts(cx: &mut TestAppContext) {
18552 init_test(cx, |_| {});
18553 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18554 let base_text = indoc! {r#"
18555 struct Row;
18556 struct Row1;
18557 struct Row2;
18558
18559 struct Row4;
18560 struct Row5;
18561 struct Row6;
18562
18563 struct Row8;
18564 struct Row9;
18565 struct Row10;"#};
18566
18567 // Modification hunks behave the same as the addition ones.
18568 assert_hunk_revert(
18569 indoc! {r#"struct Row;
18570 struct Row1;
18571 struct Row33;
18572 ˇ
18573 struct Row4;
18574 struct Row5;
18575 struct Row6;
18576 ˇ
18577 struct Row99;
18578 struct Row9;
18579 struct Row10;"#},
18580 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18581 indoc! {r#"struct Row;
18582 struct Row1;
18583 struct Row33;
18584 ˇ
18585 struct Row4;
18586 struct Row5;
18587 struct Row6;
18588 ˇ
18589 struct Row99;
18590 struct Row9;
18591 struct Row10;"#},
18592 base_text,
18593 &mut cx,
18594 );
18595 assert_hunk_revert(
18596 indoc! {r#"struct Row;
18597 struct Row1;
18598 struct Row33;
18599 «ˇ
18600 struct Row4;
18601 struct» Row5;
18602 «struct Row6;
18603 ˇ»
18604 struct Row99;
18605 struct Row9;
18606 struct Row10;"#},
18607 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18608 indoc! {r#"struct Row;
18609 struct Row1;
18610 struct Row33;
18611 «ˇ
18612 struct Row4;
18613 struct» Row5;
18614 «struct Row6;
18615 ˇ»
18616 struct Row99;
18617 struct Row9;
18618 struct Row10;"#},
18619 base_text,
18620 &mut cx,
18621 );
18622
18623 assert_hunk_revert(
18624 indoc! {r#"ˇstruct Row1.1;
18625 struct Row1;
18626 «ˇstr»uct Row22;
18627
18628 struct ˇRow44;
18629 struct Row5;
18630 struct «Rˇ»ow66;ˇ
18631
18632 «struˇ»ct Row88;
18633 struct Row9;
18634 struct Row1011;ˇ"#},
18635 vec![
18636 DiffHunkStatusKind::Modified,
18637 DiffHunkStatusKind::Modified,
18638 DiffHunkStatusKind::Modified,
18639 DiffHunkStatusKind::Modified,
18640 DiffHunkStatusKind::Modified,
18641 DiffHunkStatusKind::Modified,
18642 ],
18643 indoc! {r#"struct Row;
18644 ˇstruct Row1;
18645 struct Row2;
18646 ˇ
18647 struct Row4;
18648 ˇstruct Row5;
18649 struct Row6;
18650 ˇ
18651 struct Row8;
18652 ˇstruct Row9;
18653 struct Row10;ˇ"#},
18654 base_text,
18655 &mut cx,
18656 );
18657}
18658
18659#[gpui::test]
18660async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
18661 init_test(cx, |_| {});
18662 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18663 let base_text = indoc! {r#"
18664 one
18665
18666 two
18667 three
18668 "#};
18669
18670 cx.set_head_text(base_text);
18671 cx.set_state("\nˇ\n");
18672 cx.executor().run_until_parked();
18673 cx.update_editor(|editor, _window, cx| {
18674 editor.expand_selected_diff_hunks(cx);
18675 });
18676 cx.executor().run_until_parked();
18677 cx.update_editor(|editor, window, cx| {
18678 editor.backspace(&Default::default(), window, cx);
18679 });
18680 cx.run_until_parked();
18681 cx.assert_state_with_diff(
18682 indoc! {r#"
18683
18684 - two
18685 - threeˇ
18686 +
18687 "#}
18688 .to_string(),
18689 );
18690}
18691
18692#[gpui::test]
18693async fn test_deletion_reverts(cx: &mut TestAppContext) {
18694 init_test(cx, |_| {});
18695 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18696 let base_text = indoc! {r#"struct Row;
18697struct Row1;
18698struct Row2;
18699
18700struct Row4;
18701struct Row5;
18702struct Row6;
18703
18704struct Row8;
18705struct Row9;
18706struct Row10;"#};
18707
18708 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
18709 assert_hunk_revert(
18710 indoc! {r#"struct Row;
18711 struct Row2;
18712
18713 ˇstruct Row4;
18714 struct Row5;
18715 struct Row6;
18716 ˇ
18717 struct Row8;
18718 struct Row10;"#},
18719 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18720 indoc! {r#"struct Row;
18721 struct Row2;
18722
18723 ˇstruct Row4;
18724 struct Row5;
18725 struct Row6;
18726 ˇ
18727 struct Row8;
18728 struct Row10;"#},
18729 base_text,
18730 &mut cx,
18731 );
18732 assert_hunk_revert(
18733 indoc! {r#"struct Row;
18734 struct Row2;
18735
18736 «ˇstruct Row4;
18737 struct» Row5;
18738 «struct Row6;
18739 ˇ»
18740 struct Row8;
18741 struct Row10;"#},
18742 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18743 indoc! {r#"struct Row;
18744 struct Row2;
18745
18746 «ˇstruct Row4;
18747 struct» Row5;
18748 «struct Row6;
18749 ˇ»
18750 struct Row8;
18751 struct Row10;"#},
18752 base_text,
18753 &mut cx,
18754 );
18755
18756 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
18757 assert_hunk_revert(
18758 indoc! {r#"struct Row;
18759 ˇstruct Row2;
18760
18761 struct Row4;
18762 struct Row5;
18763 struct Row6;
18764
18765 struct Row8;ˇ
18766 struct Row10;"#},
18767 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18768 indoc! {r#"struct Row;
18769 struct Row1;
18770 ˇstruct Row2;
18771
18772 struct Row4;
18773 struct Row5;
18774 struct Row6;
18775
18776 struct Row8;ˇ
18777 struct Row9;
18778 struct Row10;"#},
18779 base_text,
18780 &mut cx,
18781 );
18782 assert_hunk_revert(
18783 indoc! {r#"struct Row;
18784 struct Row2«ˇ;
18785 struct Row4;
18786 struct» Row5;
18787 «struct Row6;
18788
18789 struct Row8;ˇ»
18790 struct Row10;"#},
18791 vec![
18792 DiffHunkStatusKind::Deleted,
18793 DiffHunkStatusKind::Deleted,
18794 DiffHunkStatusKind::Deleted,
18795 ],
18796 indoc! {r#"struct Row;
18797 struct Row1;
18798 struct Row2«ˇ;
18799
18800 struct Row4;
18801 struct» Row5;
18802 «struct Row6;
18803
18804 struct Row8;ˇ»
18805 struct Row9;
18806 struct Row10;"#},
18807 base_text,
18808 &mut cx,
18809 );
18810}
18811
18812#[gpui::test]
18813async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
18814 init_test(cx, |_| {});
18815
18816 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
18817 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
18818 let base_text_3 =
18819 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
18820
18821 let text_1 = edit_first_char_of_every_line(base_text_1);
18822 let text_2 = edit_first_char_of_every_line(base_text_2);
18823 let text_3 = edit_first_char_of_every_line(base_text_3);
18824
18825 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
18826 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
18827 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
18828
18829 let multibuffer = cx.new(|cx| {
18830 let mut multibuffer = MultiBuffer::new(ReadWrite);
18831 multibuffer.push_excerpts(
18832 buffer_1.clone(),
18833 [
18834 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18835 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18836 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18837 ],
18838 cx,
18839 );
18840 multibuffer.push_excerpts(
18841 buffer_2.clone(),
18842 [
18843 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18844 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18845 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18846 ],
18847 cx,
18848 );
18849 multibuffer.push_excerpts(
18850 buffer_3.clone(),
18851 [
18852 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18853 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18854 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18855 ],
18856 cx,
18857 );
18858 multibuffer
18859 });
18860
18861 let fs = FakeFs::new(cx.executor());
18862 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
18863 let (editor, cx) = cx
18864 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
18865 editor.update_in(cx, |editor, _window, cx| {
18866 for (buffer, diff_base) in [
18867 (buffer_1.clone(), base_text_1),
18868 (buffer_2.clone(), base_text_2),
18869 (buffer_3.clone(), base_text_3),
18870 ] {
18871 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
18872 editor
18873 .buffer
18874 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
18875 }
18876 });
18877 cx.executor().run_until_parked();
18878
18879 editor.update_in(cx, |editor, window, cx| {
18880 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}");
18881 editor.select_all(&SelectAll, window, cx);
18882 editor.git_restore(&Default::default(), window, cx);
18883 });
18884 cx.executor().run_until_parked();
18885
18886 // When all ranges are selected, all buffer hunks are reverted.
18887 editor.update(cx, |editor, cx| {
18888 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");
18889 });
18890 buffer_1.update(cx, |buffer, _| {
18891 assert_eq!(buffer.text(), base_text_1);
18892 });
18893 buffer_2.update(cx, |buffer, _| {
18894 assert_eq!(buffer.text(), base_text_2);
18895 });
18896 buffer_3.update(cx, |buffer, _| {
18897 assert_eq!(buffer.text(), base_text_3);
18898 });
18899
18900 editor.update_in(cx, |editor, window, cx| {
18901 editor.undo(&Default::default(), window, cx);
18902 });
18903
18904 editor.update_in(cx, |editor, window, cx| {
18905 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18906 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
18907 });
18908 editor.git_restore(&Default::default(), window, cx);
18909 });
18910
18911 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
18912 // but not affect buffer_2 and its related excerpts.
18913 editor.update(cx, |editor, cx| {
18914 assert_eq!(
18915 editor.text(cx),
18916 "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}"
18917 );
18918 });
18919 buffer_1.update(cx, |buffer, _| {
18920 assert_eq!(buffer.text(), base_text_1);
18921 });
18922 buffer_2.update(cx, |buffer, _| {
18923 assert_eq!(
18924 buffer.text(),
18925 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
18926 );
18927 });
18928 buffer_3.update(cx, |buffer, _| {
18929 assert_eq!(
18930 buffer.text(),
18931 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
18932 );
18933 });
18934
18935 fn edit_first_char_of_every_line(text: &str) -> String {
18936 text.split('\n')
18937 .map(|line| format!("X{}", &line[1..]))
18938 .collect::<Vec<_>>()
18939 .join("\n")
18940 }
18941}
18942
18943#[gpui::test]
18944async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
18945 init_test(cx, |_| {});
18946
18947 let cols = 4;
18948 let rows = 10;
18949 let sample_text_1 = sample_text(rows, cols, 'a');
18950 assert_eq!(
18951 sample_text_1,
18952 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
18953 );
18954 let sample_text_2 = sample_text(rows, cols, 'l');
18955 assert_eq!(
18956 sample_text_2,
18957 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
18958 );
18959 let sample_text_3 = sample_text(rows, cols, 'v');
18960 assert_eq!(
18961 sample_text_3,
18962 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
18963 );
18964
18965 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
18966 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
18967 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
18968
18969 let multi_buffer = cx.new(|cx| {
18970 let mut multibuffer = MultiBuffer::new(ReadWrite);
18971 multibuffer.push_excerpts(
18972 buffer_1.clone(),
18973 [
18974 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18975 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18976 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18977 ],
18978 cx,
18979 );
18980 multibuffer.push_excerpts(
18981 buffer_2.clone(),
18982 [
18983 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18984 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18985 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18986 ],
18987 cx,
18988 );
18989 multibuffer.push_excerpts(
18990 buffer_3.clone(),
18991 [
18992 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18993 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18994 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18995 ],
18996 cx,
18997 );
18998 multibuffer
18999 });
19000
19001 let fs = FakeFs::new(cx.executor());
19002 fs.insert_tree(
19003 "/a",
19004 json!({
19005 "main.rs": sample_text_1,
19006 "other.rs": sample_text_2,
19007 "lib.rs": sample_text_3,
19008 }),
19009 )
19010 .await;
19011 let project = Project::test(fs, ["/a".as_ref()], cx).await;
19012 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19013 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19014 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19015 Editor::new(
19016 EditorMode::full(),
19017 multi_buffer,
19018 Some(project.clone()),
19019 window,
19020 cx,
19021 )
19022 });
19023 let multibuffer_item_id = workspace
19024 .update(cx, |workspace, window, cx| {
19025 assert!(
19026 workspace.active_item(cx).is_none(),
19027 "active item should be None before the first item is added"
19028 );
19029 workspace.add_item_to_active_pane(
19030 Box::new(multi_buffer_editor.clone()),
19031 None,
19032 true,
19033 window,
19034 cx,
19035 );
19036 let active_item = workspace
19037 .active_item(cx)
19038 .expect("should have an active item after adding the multi buffer");
19039 assert_eq!(
19040 active_item.buffer_kind(cx),
19041 ItemBufferKind::Multibuffer,
19042 "A multi buffer was expected to active after adding"
19043 );
19044 active_item.item_id()
19045 })
19046 .unwrap();
19047 cx.executor().run_until_parked();
19048
19049 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19050 editor.change_selections(
19051 SelectionEffects::scroll(Autoscroll::Next),
19052 window,
19053 cx,
19054 |s| s.select_ranges(Some(1..2)),
19055 );
19056 editor.open_excerpts(&OpenExcerpts, window, cx);
19057 });
19058 cx.executor().run_until_parked();
19059 let first_item_id = workspace
19060 .update(cx, |workspace, window, cx| {
19061 let active_item = workspace
19062 .active_item(cx)
19063 .expect("should have an active item after navigating into the 1st buffer");
19064 let first_item_id = active_item.item_id();
19065 assert_ne!(
19066 first_item_id, multibuffer_item_id,
19067 "Should navigate into the 1st buffer and activate it"
19068 );
19069 assert_eq!(
19070 active_item.buffer_kind(cx),
19071 ItemBufferKind::Singleton,
19072 "New active item should be a singleton buffer"
19073 );
19074 assert_eq!(
19075 active_item
19076 .act_as::<Editor>(cx)
19077 .expect("should have navigated into an editor for the 1st buffer")
19078 .read(cx)
19079 .text(cx),
19080 sample_text_1
19081 );
19082
19083 workspace
19084 .go_back(workspace.active_pane().downgrade(), window, cx)
19085 .detach_and_log_err(cx);
19086
19087 first_item_id
19088 })
19089 .unwrap();
19090 cx.executor().run_until_parked();
19091 workspace
19092 .update(cx, |workspace, _, cx| {
19093 let active_item = workspace
19094 .active_item(cx)
19095 .expect("should have an active item after navigating back");
19096 assert_eq!(
19097 active_item.item_id(),
19098 multibuffer_item_id,
19099 "Should navigate back to the multi buffer"
19100 );
19101 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19102 })
19103 .unwrap();
19104
19105 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19106 editor.change_selections(
19107 SelectionEffects::scroll(Autoscroll::Next),
19108 window,
19109 cx,
19110 |s| s.select_ranges(Some(39..40)),
19111 );
19112 editor.open_excerpts(&OpenExcerpts, window, cx);
19113 });
19114 cx.executor().run_until_parked();
19115 let second_item_id = workspace
19116 .update(cx, |workspace, window, cx| {
19117 let active_item = workspace
19118 .active_item(cx)
19119 .expect("should have an active item after navigating into the 2nd buffer");
19120 let second_item_id = active_item.item_id();
19121 assert_ne!(
19122 second_item_id, multibuffer_item_id,
19123 "Should navigate away from the multibuffer"
19124 );
19125 assert_ne!(
19126 second_item_id, first_item_id,
19127 "Should navigate into the 2nd buffer and activate it"
19128 );
19129 assert_eq!(
19130 active_item.buffer_kind(cx),
19131 ItemBufferKind::Singleton,
19132 "New active item should be a singleton buffer"
19133 );
19134 assert_eq!(
19135 active_item
19136 .act_as::<Editor>(cx)
19137 .expect("should have navigated into an editor")
19138 .read(cx)
19139 .text(cx),
19140 sample_text_2
19141 );
19142
19143 workspace
19144 .go_back(workspace.active_pane().downgrade(), window, cx)
19145 .detach_and_log_err(cx);
19146
19147 second_item_id
19148 })
19149 .unwrap();
19150 cx.executor().run_until_parked();
19151 workspace
19152 .update(cx, |workspace, _, cx| {
19153 let active_item = workspace
19154 .active_item(cx)
19155 .expect("should have an active item after navigating back from the 2nd buffer");
19156 assert_eq!(
19157 active_item.item_id(),
19158 multibuffer_item_id,
19159 "Should navigate back from the 2nd buffer to the multi buffer"
19160 );
19161 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19162 })
19163 .unwrap();
19164
19165 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19166 editor.change_selections(
19167 SelectionEffects::scroll(Autoscroll::Next),
19168 window,
19169 cx,
19170 |s| s.select_ranges(Some(70..70)),
19171 );
19172 editor.open_excerpts(&OpenExcerpts, window, cx);
19173 });
19174 cx.executor().run_until_parked();
19175 workspace
19176 .update(cx, |workspace, window, cx| {
19177 let active_item = workspace
19178 .active_item(cx)
19179 .expect("should have an active item after navigating into the 3rd buffer");
19180 let third_item_id = active_item.item_id();
19181 assert_ne!(
19182 third_item_id, multibuffer_item_id,
19183 "Should navigate into the 3rd buffer and activate it"
19184 );
19185 assert_ne!(third_item_id, first_item_id);
19186 assert_ne!(third_item_id, second_item_id);
19187 assert_eq!(
19188 active_item.buffer_kind(cx),
19189 ItemBufferKind::Singleton,
19190 "New active item should be a singleton buffer"
19191 );
19192 assert_eq!(
19193 active_item
19194 .act_as::<Editor>(cx)
19195 .expect("should have navigated into an editor")
19196 .read(cx)
19197 .text(cx),
19198 sample_text_3
19199 );
19200
19201 workspace
19202 .go_back(workspace.active_pane().downgrade(), window, cx)
19203 .detach_and_log_err(cx);
19204 })
19205 .unwrap();
19206 cx.executor().run_until_parked();
19207 workspace
19208 .update(cx, |workspace, _, cx| {
19209 let active_item = workspace
19210 .active_item(cx)
19211 .expect("should have an active item after navigating back from the 3rd buffer");
19212 assert_eq!(
19213 active_item.item_id(),
19214 multibuffer_item_id,
19215 "Should navigate back from the 3rd buffer to the multi buffer"
19216 );
19217 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19218 })
19219 .unwrap();
19220}
19221
19222#[gpui::test]
19223async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19224 init_test(cx, |_| {});
19225
19226 let mut cx = EditorTestContext::new(cx).await;
19227
19228 let diff_base = r#"
19229 use some::mod;
19230
19231 const A: u32 = 42;
19232
19233 fn main() {
19234 println!("hello");
19235
19236 println!("world");
19237 }
19238 "#
19239 .unindent();
19240
19241 cx.set_state(
19242 &r#"
19243 use some::modified;
19244
19245 ˇ
19246 fn main() {
19247 println!("hello there");
19248
19249 println!("around the");
19250 println!("world");
19251 }
19252 "#
19253 .unindent(),
19254 );
19255
19256 cx.set_head_text(&diff_base);
19257 executor.run_until_parked();
19258
19259 cx.update_editor(|editor, window, cx| {
19260 editor.go_to_next_hunk(&GoToHunk, window, cx);
19261 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19262 });
19263 executor.run_until_parked();
19264 cx.assert_state_with_diff(
19265 r#"
19266 use some::modified;
19267
19268
19269 fn main() {
19270 - println!("hello");
19271 + ˇ println!("hello there");
19272
19273 println!("around the");
19274 println!("world");
19275 }
19276 "#
19277 .unindent(),
19278 );
19279
19280 cx.update_editor(|editor, window, cx| {
19281 for _ in 0..2 {
19282 editor.go_to_next_hunk(&GoToHunk, window, cx);
19283 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19284 }
19285 });
19286 executor.run_until_parked();
19287 cx.assert_state_with_diff(
19288 r#"
19289 - use some::mod;
19290 + ˇuse some::modified;
19291
19292
19293 fn main() {
19294 - println!("hello");
19295 + println!("hello there");
19296
19297 + println!("around the");
19298 println!("world");
19299 }
19300 "#
19301 .unindent(),
19302 );
19303
19304 cx.update_editor(|editor, window, cx| {
19305 editor.go_to_next_hunk(&GoToHunk, window, cx);
19306 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19307 });
19308 executor.run_until_parked();
19309 cx.assert_state_with_diff(
19310 r#"
19311 - use some::mod;
19312 + use some::modified;
19313
19314 - const A: u32 = 42;
19315 ˇ
19316 fn main() {
19317 - println!("hello");
19318 + println!("hello there");
19319
19320 + println!("around the");
19321 println!("world");
19322 }
19323 "#
19324 .unindent(),
19325 );
19326
19327 cx.update_editor(|editor, window, cx| {
19328 editor.cancel(&Cancel, window, cx);
19329 });
19330
19331 cx.assert_state_with_diff(
19332 r#"
19333 use some::modified;
19334
19335 ˇ
19336 fn main() {
19337 println!("hello there");
19338
19339 println!("around the");
19340 println!("world");
19341 }
19342 "#
19343 .unindent(),
19344 );
19345}
19346
19347#[gpui::test]
19348async fn test_diff_base_change_with_expanded_diff_hunks(
19349 executor: BackgroundExecutor,
19350 cx: &mut TestAppContext,
19351) {
19352 init_test(cx, |_| {});
19353
19354 let mut cx = EditorTestContext::new(cx).await;
19355
19356 let diff_base = r#"
19357 use some::mod1;
19358 use some::mod2;
19359
19360 const A: u32 = 42;
19361 const B: u32 = 42;
19362 const C: u32 = 42;
19363
19364 fn main() {
19365 println!("hello");
19366
19367 println!("world");
19368 }
19369 "#
19370 .unindent();
19371
19372 cx.set_state(
19373 &r#"
19374 use some::mod2;
19375
19376 const A: u32 = 42;
19377 const C: u32 = 42;
19378
19379 fn main(ˇ) {
19380 //println!("hello");
19381
19382 println!("world");
19383 //
19384 //
19385 }
19386 "#
19387 .unindent(),
19388 );
19389
19390 cx.set_head_text(&diff_base);
19391 executor.run_until_parked();
19392
19393 cx.update_editor(|editor, window, cx| {
19394 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19395 });
19396 executor.run_until_parked();
19397 cx.assert_state_with_diff(
19398 r#"
19399 - use some::mod1;
19400 use some::mod2;
19401
19402 const A: u32 = 42;
19403 - const B: u32 = 42;
19404 const C: u32 = 42;
19405
19406 fn main(ˇ) {
19407 - println!("hello");
19408 + //println!("hello");
19409
19410 println!("world");
19411 + //
19412 + //
19413 }
19414 "#
19415 .unindent(),
19416 );
19417
19418 cx.set_head_text("new diff base!");
19419 executor.run_until_parked();
19420 cx.assert_state_with_diff(
19421 r#"
19422 - new diff base!
19423 + use some::mod2;
19424 +
19425 + const A: u32 = 42;
19426 + const C: u32 = 42;
19427 +
19428 + fn main(ˇ) {
19429 + //println!("hello");
19430 +
19431 + println!("world");
19432 + //
19433 + //
19434 + }
19435 "#
19436 .unindent(),
19437 );
19438}
19439
19440#[gpui::test]
19441async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
19442 init_test(cx, |_| {});
19443
19444 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19445 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19446 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19447 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19448 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
19449 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
19450
19451 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
19452 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
19453 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
19454
19455 let multi_buffer = cx.new(|cx| {
19456 let mut multibuffer = MultiBuffer::new(ReadWrite);
19457 multibuffer.push_excerpts(
19458 buffer_1.clone(),
19459 [
19460 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19461 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19462 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19463 ],
19464 cx,
19465 );
19466 multibuffer.push_excerpts(
19467 buffer_2.clone(),
19468 [
19469 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19470 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19471 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19472 ],
19473 cx,
19474 );
19475 multibuffer.push_excerpts(
19476 buffer_3.clone(),
19477 [
19478 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19479 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19480 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19481 ],
19482 cx,
19483 );
19484 multibuffer
19485 });
19486
19487 let editor =
19488 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19489 editor
19490 .update(cx, |editor, _window, cx| {
19491 for (buffer, diff_base) in [
19492 (buffer_1.clone(), file_1_old),
19493 (buffer_2.clone(), file_2_old),
19494 (buffer_3.clone(), file_3_old),
19495 ] {
19496 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
19497 editor
19498 .buffer
19499 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
19500 }
19501 })
19502 .unwrap();
19503
19504 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19505 cx.run_until_parked();
19506
19507 cx.assert_editor_state(
19508 &"
19509 ˇaaa
19510 ccc
19511 ddd
19512
19513 ggg
19514 hhh
19515
19516
19517 lll
19518 mmm
19519 NNN
19520
19521 qqq
19522 rrr
19523
19524 uuu
19525 111
19526 222
19527 333
19528
19529 666
19530 777
19531
19532 000
19533 !!!"
19534 .unindent(),
19535 );
19536
19537 cx.update_editor(|editor, window, cx| {
19538 editor.select_all(&SelectAll, window, cx);
19539 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19540 });
19541 cx.executor().run_until_parked();
19542
19543 cx.assert_state_with_diff(
19544 "
19545 «aaa
19546 - bbb
19547 ccc
19548 ddd
19549
19550 ggg
19551 hhh
19552
19553
19554 lll
19555 mmm
19556 - nnn
19557 + NNN
19558
19559 qqq
19560 rrr
19561
19562 uuu
19563 111
19564 222
19565 333
19566
19567 + 666
19568 777
19569
19570 000
19571 !!!ˇ»"
19572 .unindent(),
19573 );
19574}
19575
19576#[gpui::test]
19577async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
19578 init_test(cx, |_| {});
19579
19580 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
19581 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
19582
19583 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
19584 let multi_buffer = cx.new(|cx| {
19585 let mut multibuffer = MultiBuffer::new(ReadWrite);
19586 multibuffer.push_excerpts(
19587 buffer.clone(),
19588 [
19589 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
19590 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
19591 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
19592 ],
19593 cx,
19594 );
19595 multibuffer
19596 });
19597
19598 let editor =
19599 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19600 editor
19601 .update(cx, |editor, _window, cx| {
19602 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
19603 editor
19604 .buffer
19605 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
19606 })
19607 .unwrap();
19608
19609 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19610 cx.run_until_parked();
19611
19612 cx.update_editor(|editor, window, cx| {
19613 editor.expand_all_diff_hunks(&Default::default(), window, cx)
19614 });
19615 cx.executor().run_until_parked();
19616
19617 // When the start of a hunk coincides with the start of its excerpt,
19618 // the hunk is expanded. When the start of a hunk is earlier than
19619 // the start of its excerpt, the hunk is not expanded.
19620 cx.assert_state_with_diff(
19621 "
19622 ˇaaa
19623 - bbb
19624 + BBB
19625
19626 - ddd
19627 - eee
19628 + DDD
19629 + EEE
19630 fff
19631
19632 iii
19633 "
19634 .unindent(),
19635 );
19636}
19637
19638#[gpui::test]
19639async fn test_edits_around_expanded_insertion_hunks(
19640 executor: BackgroundExecutor,
19641 cx: &mut TestAppContext,
19642) {
19643 init_test(cx, |_| {});
19644
19645 let mut cx = EditorTestContext::new(cx).await;
19646
19647 let diff_base = r#"
19648 use some::mod1;
19649 use some::mod2;
19650
19651 const A: u32 = 42;
19652
19653 fn main() {
19654 println!("hello");
19655
19656 println!("world");
19657 }
19658 "#
19659 .unindent();
19660 executor.run_until_parked();
19661 cx.set_state(
19662 &r#"
19663 use some::mod1;
19664 use some::mod2;
19665
19666 const A: u32 = 42;
19667 const B: u32 = 42;
19668 const C: u32 = 42;
19669 ˇ
19670
19671 fn main() {
19672 println!("hello");
19673
19674 println!("world");
19675 }
19676 "#
19677 .unindent(),
19678 );
19679
19680 cx.set_head_text(&diff_base);
19681 executor.run_until_parked();
19682
19683 cx.update_editor(|editor, window, cx| {
19684 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19685 });
19686 executor.run_until_parked();
19687
19688 cx.assert_state_with_diff(
19689 r#"
19690 use some::mod1;
19691 use some::mod2;
19692
19693 const A: u32 = 42;
19694 + const B: u32 = 42;
19695 + const C: u32 = 42;
19696 + ˇ
19697
19698 fn main() {
19699 println!("hello");
19700
19701 println!("world");
19702 }
19703 "#
19704 .unindent(),
19705 );
19706
19707 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
19708 executor.run_until_parked();
19709
19710 cx.assert_state_with_diff(
19711 r#"
19712 use some::mod1;
19713 use some::mod2;
19714
19715 const A: u32 = 42;
19716 + const B: u32 = 42;
19717 + const C: u32 = 42;
19718 + const D: u32 = 42;
19719 + ˇ
19720
19721 fn main() {
19722 println!("hello");
19723
19724 println!("world");
19725 }
19726 "#
19727 .unindent(),
19728 );
19729
19730 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
19731 executor.run_until_parked();
19732
19733 cx.assert_state_with_diff(
19734 r#"
19735 use some::mod1;
19736 use some::mod2;
19737
19738 const A: u32 = 42;
19739 + const B: u32 = 42;
19740 + const C: u32 = 42;
19741 + const D: u32 = 42;
19742 + const E: u32 = 42;
19743 + ˇ
19744
19745 fn main() {
19746 println!("hello");
19747
19748 println!("world");
19749 }
19750 "#
19751 .unindent(),
19752 );
19753
19754 cx.update_editor(|editor, window, cx| {
19755 editor.delete_line(&DeleteLine, window, cx);
19756 });
19757 executor.run_until_parked();
19758
19759 cx.assert_state_with_diff(
19760 r#"
19761 use some::mod1;
19762 use some::mod2;
19763
19764 const A: u32 = 42;
19765 + const B: u32 = 42;
19766 + const C: u32 = 42;
19767 + const D: u32 = 42;
19768 + const E: u32 = 42;
19769 ˇ
19770 fn main() {
19771 println!("hello");
19772
19773 println!("world");
19774 }
19775 "#
19776 .unindent(),
19777 );
19778
19779 cx.update_editor(|editor, window, cx| {
19780 editor.move_up(&MoveUp, window, cx);
19781 editor.delete_line(&DeleteLine, window, cx);
19782 editor.move_up(&MoveUp, window, cx);
19783 editor.delete_line(&DeleteLine, window, cx);
19784 editor.move_up(&MoveUp, window, cx);
19785 editor.delete_line(&DeleteLine, window, cx);
19786 });
19787 executor.run_until_parked();
19788 cx.assert_state_with_diff(
19789 r#"
19790 use some::mod1;
19791 use some::mod2;
19792
19793 const A: u32 = 42;
19794 + const B: u32 = 42;
19795 ˇ
19796 fn main() {
19797 println!("hello");
19798
19799 println!("world");
19800 }
19801 "#
19802 .unindent(),
19803 );
19804
19805 cx.update_editor(|editor, window, cx| {
19806 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
19807 editor.delete_line(&DeleteLine, window, cx);
19808 });
19809 executor.run_until_parked();
19810 cx.assert_state_with_diff(
19811 r#"
19812 ˇ
19813 fn main() {
19814 println!("hello");
19815
19816 println!("world");
19817 }
19818 "#
19819 .unindent(),
19820 );
19821}
19822
19823#[gpui::test]
19824async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
19825 init_test(cx, |_| {});
19826
19827 let mut cx = EditorTestContext::new(cx).await;
19828 cx.set_head_text(indoc! { "
19829 one
19830 two
19831 three
19832 four
19833 five
19834 "
19835 });
19836 cx.set_state(indoc! { "
19837 one
19838 ˇthree
19839 five
19840 "});
19841 cx.run_until_parked();
19842 cx.update_editor(|editor, window, cx| {
19843 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19844 });
19845 cx.assert_state_with_diff(
19846 indoc! { "
19847 one
19848 - two
19849 ˇthree
19850 - four
19851 five
19852 "}
19853 .to_string(),
19854 );
19855 cx.update_editor(|editor, window, cx| {
19856 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19857 });
19858
19859 cx.assert_state_with_diff(
19860 indoc! { "
19861 one
19862 ˇthree
19863 five
19864 "}
19865 .to_string(),
19866 );
19867
19868 cx.set_state(indoc! { "
19869 one
19870 ˇTWO
19871 three
19872 four
19873 five
19874 "});
19875 cx.run_until_parked();
19876 cx.update_editor(|editor, window, cx| {
19877 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19878 });
19879
19880 cx.assert_state_with_diff(
19881 indoc! { "
19882 one
19883 - two
19884 + ˇTWO
19885 three
19886 four
19887 five
19888 "}
19889 .to_string(),
19890 );
19891 cx.update_editor(|editor, window, cx| {
19892 editor.move_up(&Default::default(), window, cx);
19893 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19894 });
19895 cx.assert_state_with_diff(
19896 indoc! { "
19897 one
19898 ˇTWO
19899 three
19900 four
19901 five
19902 "}
19903 .to_string(),
19904 );
19905}
19906
19907#[gpui::test]
19908async fn test_edits_around_expanded_deletion_hunks(
19909 executor: BackgroundExecutor,
19910 cx: &mut TestAppContext,
19911) {
19912 init_test(cx, |_| {});
19913
19914 let mut cx = EditorTestContext::new(cx).await;
19915
19916 let diff_base = r#"
19917 use some::mod1;
19918 use some::mod2;
19919
19920 const A: u32 = 42;
19921 const B: u32 = 42;
19922 const C: u32 = 42;
19923
19924
19925 fn main() {
19926 println!("hello");
19927
19928 println!("world");
19929 }
19930 "#
19931 .unindent();
19932 executor.run_until_parked();
19933 cx.set_state(
19934 &r#"
19935 use some::mod1;
19936 use some::mod2;
19937
19938 ˇconst B: u32 = 42;
19939 const C: u32 = 42;
19940
19941
19942 fn main() {
19943 println!("hello");
19944
19945 println!("world");
19946 }
19947 "#
19948 .unindent(),
19949 );
19950
19951 cx.set_head_text(&diff_base);
19952 executor.run_until_parked();
19953
19954 cx.update_editor(|editor, window, cx| {
19955 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19956 });
19957 executor.run_until_parked();
19958
19959 cx.assert_state_with_diff(
19960 r#"
19961 use some::mod1;
19962 use some::mod2;
19963
19964 - const A: u32 = 42;
19965 ˇconst B: u32 = 42;
19966 const C: u32 = 42;
19967
19968
19969 fn main() {
19970 println!("hello");
19971
19972 println!("world");
19973 }
19974 "#
19975 .unindent(),
19976 );
19977
19978 cx.update_editor(|editor, window, cx| {
19979 editor.delete_line(&DeleteLine, window, cx);
19980 });
19981 executor.run_until_parked();
19982 cx.assert_state_with_diff(
19983 r#"
19984 use some::mod1;
19985 use some::mod2;
19986
19987 - const A: u32 = 42;
19988 - const B: u32 = 42;
19989 ˇconst C: u32 = 42;
19990
19991
19992 fn main() {
19993 println!("hello");
19994
19995 println!("world");
19996 }
19997 "#
19998 .unindent(),
19999 );
20000
20001 cx.update_editor(|editor, window, cx| {
20002 editor.delete_line(&DeleteLine, window, cx);
20003 });
20004 executor.run_until_parked();
20005 cx.assert_state_with_diff(
20006 r#"
20007 use some::mod1;
20008 use some::mod2;
20009
20010 - const A: u32 = 42;
20011 - const B: u32 = 42;
20012 - const C: u32 = 42;
20013 ˇ
20014
20015 fn main() {
20016 println!("hello");
20017
20018 println!("world");
20019 }
20020 "#
20021 .unindent(),
20022 );
20023
20024 cx.update_editor(|editor, window, cx| {
20025 editor.handle_input("replacement", window, cx);
20026 });
20027 executor.run_until_parked();
20028 cx.assert_state_with_diff(
20029 r#"
20030 use some::mod1;
20031 use some::mod2;
20032
20033 - const A: u32 = 42;
20034 - const B: u32 = 42;
20035 - const C: u32 = 42;
20036 -
20037 + replacementˇ
20038
20039 fn main() {
20040 println!("hello");
20041
20042 println!("world");
20043 }
20044 "#
20045 .unindent(),
20046 );
20047}
20048
20049#[gpui::test]
20050async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20051 init_test(cx, |_| {});
20052
20053 let mut cx = EditorTestContext::new(cx).await;
20054
20055 let base_text = r#"
20056 one
20057 two
20058 three
20059 four
20060 five
20061 "#
20062 .unindent();
20063 executor.run_until_parked();
20064 cx.set_state(
20065 &r#"
20066 one
20067 two
20068 fˇour
20069 five
20070 "#
20071 .unindent(),
20072 );
20073
20074 cx.set_head_text(&base_text);
20075 executor.run_until_parked();
20076
20077 cx.update_editor(|editor, window, cx| {
20078 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20079 });
20080 executor.run_until_parked();
20081
20082 cx.assert_state_with_diff(
20083 r#"
20084 one
20085 two
20086 - three
20087 fˇour
20088 five
20089 "#
20090 .unindent(),
20091 );
20092
20093 cx.update_editor(|editor, window, cx| {
20094 editor.backspace(&Backspace, window, cx);
20095 editor.backspace(&Backspace, window, cx);
20096 });
20097 executor.run_until_parked();
20098 cx.assert_state_with_diff(
20099 r#"
20100 one
20101 two
20102 - threeˇ
20103 - four
20104 + our
20105 five
20106 "#
20107 .unindent(),
20108 );
20109}
20110
20111#[gpui::test]
20112async fn test_edit_after_expanded_modification_hunk(
20113 executor: BackgroundExecutor,
20114 cx: &mut TestAppContext,
20115) {
20116 init_test(cx, |_| {});
20117
20118 let mut cx = EditorTestContext::new(cx).await;
20119
20120 let diff_base = r#"
20121 use some::mod1;
20122 use some::mod2;
20123
20124 const A: u32 = 42;
20125 const B: u32 = 42;
20126 const C: u32 = 42;
20127 const D: u32 = 42;
20128
20129
20130 fn main() {
20131 println!("hello");
20132
20133 println!("world");
20134 }"#
20135 .unindent();
20136
20137 cx.set_state(
20138 &r#"
20139 use some::mod1;
20140 use some::mod2;
20141
20142 const A: u32 = 42;
20143 const B: u32 = 42;
20144 const C: u32 = 43ˇ
20145 const D: u32 = 42;
20146
20147
20148 fn main() {
20149 println!("hello");
20150
20151 println!("world");
20152 }"#
20153 .unindent(),
20154 );
20155
20156 cx.set_head_text(&diff_base);
20157 executor.run_until_parked();
20158 cx.update_editor(|editor, window, cx| {
20159 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20160 });
20161 executor.run_until_parked();
20162
20163 cx.assert_state_with_diff(
20164 r#"
20165 use some::mod1;
20166 use some::mod2;
20167
20168 const A: u32 = 42;
20169 const B: u32 = 42;
20170 - const C: u32 = 42;
20171 + const C: u32 = 43ˇ
20172 const D: u32 = 42;
20173
20174
20175 fn main() {
20176 println!("hello");
20177
20178 println!("world");
20179 }"#
20180 .unindent(),
20181 );
20182
20183 cx.update_editor(|editor, window, cx| {
20184 editor.handle_input("\nnew_line\n", window, cx);
20185 });
20186 executor.run_until_parked();
20187
20188 cx.assert_state_with_diff(
20189 r#"
20190 use some::mod1;
20191 use some::mod2;
20192
20193 const A: u32 = 42;
20194 const B: u32 = 42;
20195 - const C: u32 = 42;
20196 + const C: u32 = 43
20197 + new_line
20198 + ˇ
20199 const D: u32 = 42;
20200
20201
20202 fn main() {
20203 println!("hello");
20204
20205 println!("world");
20206 }"#
20207 .unindent(),
20208 );
20209}
20210
20211#[gpui::test]
20212async fn test_stage_and_unstage_added_file_hunk(
20213 executor: BackgroundExecutor,
20214 cx: &mut TestAppContext,
20215) {
20216 init_test(cx, |_| {});
20217
20218 let mut cx = EditorTestContext::new(cx).await;
20219 cx.update_editor(|editor, _, cx| {
20220 editor.set_expand_all_diff_hunks(cx);
20221 });
20222
20223 let working_copy = r#"
20224 ˇfn main() {
20225 println!("hello, world!");
20226 }
20227 "#
20228 .unindent();
20229
20230 cx.set_state(&working_copy);
20231 executor.run_until_parked();
20232
20233 cx.assert_state_with_diff(
20234 r#"
20235 + ˇfn main() {
20236 + println!("hello, world!");
20237 + }
20238 "#
20239 .unindent(),
20240 );
20241 cx.assert_index_text(None);
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(Some(&working_copy.replace("ˇ", "")));
20248 cx.assert_state_with_diff(
20249 r#"
20250 + ˇfn main() {
20251 + println!("hello, world!");
20252 + }
20253 "#
20254 .unindent(),
20255 );
20256
20257 cx.update_editor(|editor, window, cx| {
20258 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20259 });
20260 executor.run_until_parked();
20261 cx.assert_index_text(None);
20262}
20263
20264async fn setup_indent_guides_editor(
20265 text: &str,
20266 cx: &mut TestAppContext,
20267) -> (BufferId, EditorTestContext) {
20268 init_test(cx, |_| {});
20269
20270 let mut cx = EditorTestContext::new(cx).await;
20271
20272 let buffer_id = cx.update_editor(|editor, window, cx| {
20273 editor.set_text(text, window, cx);
20274 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
20275
20276 buffer_ids[0]
20277 });
20278
20279 (buffer_id, cx)
20280}
20281
20282fn assert_indent_guides(
20283 range: Range<u32>,
20284 expected: Vec<IndentGuide>,
20285 active_indices: Option<Vec<usize>>,
20286 cx: &mut EditorTestContext,
20287) {
20288 let indent_guides = cx.update_editor(|editor, window, cx| {
20289 let snapshot = editor.snapshot(window, cx).display_snapshot;
20290 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
20291 editor,
20292 MultiBufferRow(range.start)..MultiBufferRow(range.end),
20293 true,
20294 &snapshot,
20295 cx,
20296 );
20297
20298 indent_guides.sort_by(|a, b| {
20299 a.depth.cmp(&b.depth).then(
20300 a.start_row
20301 .cmp(&b.start_row)
20302 .then(a.end_row.cmp(&b.end_row)),
20303 )
20304 });
20305 indent_guides
20306 });
20307
20308 if let Some(expected) = active_indices {
20309 let active_indices = cx.update_editor(|editor, window, cx| {
20310 let snapshot = editor.snapshot(window, cx).display_snapshot;
20311 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
20312 });
20313
20314 assert_eq!(
20315 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
20316 expected,
20317 "Active indent guide indices do not match"
20318 );
20319 }
20320
20321 assert_eq!(indent_guides, expected, "Indent guides do not match");
20322}
20323
20324fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
20325 IndentGuide {
20326 buffer_id,
20327 start_row: MultiBufferRow(start_row),
20328 end_row: MultiBufferRow(end_row),
20329 depth,
20330 tab_size: 4,
20331 settings: IndentGuideSettings {
20332 enabled: true,
20333 line_width: 1,
20334 active_line_width: 1,
20335 coloring: IndentGuideColoring::default(),
20336 background_coloring: IndentGuideBackgroundColoring::default(),
20337 },
20338 }
20339}
20340
20341#[gpui::test]
20342async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
20343 let (buffer_id, mut cx) = setup_indent_guides_editor(
20344 &"
20345 fn main() {
20346 let a = 1;
20347 }"
20348 .unindent(),
20349 cx,
20350 )
20351 .await;
20352
20353 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20354}
20355
20356#[gpui::test]
20357async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
20358 let (buffer_id, mut cx) = setup_indent_guides_editor(
20359 &"
20360 fn main() {
20361 let a = 1;
20362 let b = 2;
20363 }"
20364 .unindent(),
20365 cx,
20366 )
20367 .await;
20368
20369 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
20370}
20371
20372#[gpui::test]
20373async fn test_indent_guide_nested(cx: &mut TestAppContext) {
20374 let (buffer_id, mut cx) = setup_indent_guides_editor(
20375 &"
20376 fn main() {
20377 let a = 1;
20378 if a == 3 {
20379 let b = 2;
20380 } else {
20381 let c = 3;
20382 }
20383 }"
20384 .unindent(),
20385 cx,
20386 )
20387 .await;
20388
20389 assert_indent_guides(
20390 0..8,
20391 vec![
20392 indent_guide(buffer_id, 1, 6, 0),
20393 indent_guide(buffer_id, 3, 3, 1),
20394 indent_guide(buffer_id, 5, 5, 1),
20395 ],
20396 None,
20397 &mut cx,
20398 );
20399}
20400
20401#[gpui::test]
20402async fn test_indent_guide_tab(cx: &mut TestAppContext) {
20403 let (buffer_id, mut cx) = setup_indent_guides_editor(
20404 &"
20405 fn main() {
20406 let a = 1;
20407 let b = 2;
20408 let c = 3;
20409 }"
20410 .unindent(),
20411 cx,
20412 )
20413 .await;
20414
20415 assert_indent_guides(
20416 0..5,
20417 vec![
20418 indent_guide(buffer_id, 1, 3, 0),
20419 indent_guide(buffer_id, 2, 2, 1),
20420 ],
20421 None,
20422 &mut cx,
20423 );
20424}
20425
20426#[gpui::test]
20427async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
20428 let (buffer_id, mut cx) = setup_indent_guides_editor(
20429 &"
20430 fn main() {
20431 let a = 1;
20432
20433 let c = 3;
20434 }"
20435 .unindent(),
20436 cx,
20437 )
20438 .await;
20439
20440 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
20441}
20442
20443#[gpui::test]
20444async fn test_indent_guide_complex(cx: &mut TestAppContext) {
20445 let (buffer_id, mut cx) = setup_indent_guides_editor(
20446 &"
20447 fn main() {
20448 let a = 1;
20449
20450 let c = 3;
20451
20452 if a == 3 {
20453 let b = 2;
20454 } else {
20455 let c = 3;
20456 }
20457 }"
20458 .unindent(),
20459 cx,
20460 )
20461 .await;
20462
20463 assert_indent_guides(
20464 0..11,
20465 vec![
20466 indent_guide(buffer_id, 1, 9, 0),
20467 indent_guide(buffer_id, 6, 6, 1),
20468 indent_guide(buffer_id, 8, 8, 1),
20469 ],
20470 None,
20471 &mut cx,
20472 );
20473}
20474
20475#[gpui::test]
20476async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
20477 let (buffer_id, mut cx) = setup_indent_guides_editor(
20478 &"
20479 fn main() {
20480 let a = 1;
20481
20482 let c = 3;
20483
20484 if a == 3 {
20485 let b = 2;
20486 } else {
20487 let c = 3;
20488 }
20489 }"
20490 .unindent(),
20491 cx,
20492 )
20493 .await;
20494
20495 assert_indent_guides(
20496 1..11,
20497 vec![
20498 indent_guide(buffer_id, 1, 9, 0),
20499 indent_guide(buffer_id, 6, 6, 1),
20500 indent_guide(buffer_id, 8, 8, 1),
20501 ],
20502 None,
20503 &mut cx,
20504 );
20505}
20506
20507#[gpui::test]
20508async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
20509 let (buffer_id, mut cx) = setup_indent_guides_editor(
20510 &"
20511 fn main() {
20512 let a = 1;
20513
20514 let c = 3;
20515
20516 if a == 3 {
20517 let b = 2;
20518 } else {
20519 let c = 3;
20520 }
20521 }"
20522 .unindent(),
20523 cx,
20524 )
20525 .await;
20526
20527 assert_indent_guides(
20528 1..10,
20529 vec![
20530 indent_guide(buffer_id, 1, 9, 0),
20531 indent_guide(buffer_id, 6, 6, 1),
20532 indent_guide(buffer_id, 8, 8, 1),
20533 ],
20534 None,
20535 &mut cx,
20536 );
20537}
20538
20539#[gpui::test]
20540async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
20541 let (buffer_id, mut cx) = setup_indent_guides_editor(
20542 &"
20543 fn main() {
20544 if a {
20545 b(
20546 c,
20547 d,
20548 )
20549 } else {
20550 e(
20551 f
20552 )
20553 }
20554 }"
20555 .unindent(),
20556 cx,
20557 )
20558 .await;
20559
20560 assert_indent_guides(
20561 0..11,
20562 vec![
20563 indent_guide(buffer_id, 1, 10, 0),
20564 indent_guide(buffer_id, 2, 5, 1),
20565 indent_guide(buffer_id, 7, 9, 1),
20566 indent_guide(buffer_id, 3, 4, 2),
20567 indent_guide(buffer_id, 8, 8, 2),
20568 ],
20569 None,
20570 &mut cx,
20571 );
20572
20573 cx.update_editor(|editor, window, cx| {
20574 editor.fold_at(MultiBufferRow(2), window, cx);
20575 assert_eq!(
20576 editor.display_text(cx),
20577 "
20578 fn main() {
20579 if a {
20580 b(⋯
20581 )
20582 } else {
20583 e(
20584 f
20585 )
20586 }
20587 }"
20588 .unindent()
20589 );
20590 });
20591
20592 assert_indent_guides(
20593 0..11,
20594 vec![
20595 indent_guide(buffer_id, 1, 10, 0),
20596 indent_guide(buffer_id, 2, 5, 1),
20597 indent_guide(buffer_id, 7, 9, 1),
20598 indent_guide(buffer_id, 8, 8, 2),
20599 ],
20600 None,
20601 &mut cx,
20602 );
20603}
20604
20605#[gpui::test]
20606async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
20607 let (buffer_id, mut cx) = setup_indent_guides_editor(
20608 &"
20609 block1
20610 block2
20611 block3
20612 block4
20613 block2
20614 block1
20615 block1"
20616 .unindent(),
20617 cx,
20618 )
20619 .await;
20620
20621 assert_indent_guides(
20622 1..10,
20623 vec![
20624 indent_guide(buffer_id, 1, 4, 0),
20625 indent_guide(buffer_id, 2, 3, 1),
20626 indent_guide(buffer_id, 3, 3, 2),
20627 ],
20628 None,
20629 &mut cx,
20630 );
20631}
20632
20633#[gpui::test]
20634async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
20635 let (buffer_id, mut cx) = setup_indent_guides_editor(
20636 &"
20637 block1
20638 block2
20639 block3
20640
20641 block1
20642 block1"
20643 .unindent(),
20644 cx,
20645 )
20646 .await;
20647
20648 assert_indent_guides(
20649 0..6,
20650 vec![
20651 indent_guide(buffer_id, 1, 2, 0),
20652 indent_guide(buffer_id, 2, 2, 1),
20653 ],
20654 None,
20655 &mut cx,
20656 );
20657}
20658
20659#[gpui::test]
20660async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
20661 let (buffer_id, mut cx) = setup_indent_guides_editor(
20662 &"
20663 function component() {
20664 \treturn (
20665 \t\t\t
20666 \t\t<div>
20667 \t\t\t<abc></abc>
20668 \t\t</div>
20669 \t)
20670 }"
20671 .unindent(),
20672 cx,
20673 )
20674 .await;
20675
20676 assert_indent_guides(
20677 0..8,
20678 vec![
20679 indent_guide(buffer_id, 1, 6, 0),
20680 indent_guide(buffer_id, 2, 5, 1),
20681 indent_guide(buffer_id, 4, 4, 2),
20682 ],
20683 None,
20684 &mut cx,
20685 );
20686}
20687
20688#[gpui::test]
20689async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
20690 let (buffer_id, mut cx) = setup_indent_guides_editor(
20691 &"
20692 function component() {
20693 \treturn (
20694 \t
20695 \t\t<div>
20696 \t\t\t<abc></abc>
20697 \t\t</div>
20698 \t)
20699 }"
20700 .unindent(),
20701 cx,
20702 )
20703 .await;
20704
20705 assert_indent_guides(
20706 0..8,
20707 vec![
20708 indent_guide(buffer_id, 1, 6, 0),
20709 indent_guide(buffer_id, 2, 5, 1),
20710 indent_guide(buffer_id, 4, 4, 2),
20711 ],
20712 None,
20713 &mut cx,
20714 );
20715}
20716
20717#[gpui::test]
20718async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
20719 let (buffer_id, mut cx) = setup_indent_guides_editor(
20720 &"
20721 block1
20722
20723
20724
20725 block2
20726 "
20727 .unindent(),
20728 cx,
20729 )
20730 .await;
20731
20732 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20733}
20734
20735#[gpui::test]
20736async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
20737 let (buffer_id, mut cx) = setup_indent_guides_editor(
20738 &"
20739 def a:
20740 \tb = 3
20741 \tif True:
20742 \t\tc = 4
20743 \t\td = 5
20744 \tprint(b)
20745 "
20746 .unindent(),
20747 cx,
20748 )
20749 .await;
20750
20751 assert_indent_guides(
20752 0..6,
20753 vec![
20754 indent_guide(buffer_id, 1, 5, 0),
20755 indent_guide(buffer_id, 3, 4, 1),
20756 ],
20757 None,
20758 &mut cx,
20759 );
20760}
20761
20762#[gpui::test]
20763async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
20764 let (buffer_id, mut cx) = setup_indent_guides_editor(
20765 &"
20766 fn main() {
20767 let a = 1;
20768 }"
20769 .unindent(),
20770 cx,
20771 )
20772 .await;
20773
20774 cx.update_editor(|editor, window, cx| {
20775 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20776 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20777 });
20778 });
20779
20780 assert_indent_guides(
20781 0..3,
20782 vec![indent_guide(buffer_id, 1, 1, 0)],
20783 Some(vec![0]),
20784 &mut cx,
20785 );
20786}
20787
20788#[gpui::test]
20789async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
20790 let (buffer_id, mut cx) = setup_indent_guides_editor(
20791 &"
20792 fn main() {
20793 if 1 == 2 {
20794 let a = 1;
20795 }
20796 }"
20797 .unindent(),
20798 cx,
20799 )
20800 .await;
20801
20802 cx.update_editor(|editor, window, cx| {
20803 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20804 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20805 });
20806 });
20807
20808 assert_indent_guides(
20809 0..4,
20810 vec![
20811 indent_guide(buffer_id, 1, 3, 0),
20812 indent_guide(buffer_id, 2, 2, 1),
20813 ],
20814 Some(vec![1]),
20815 &mut cx,
20816 );
20817
20818 cx.update_editor(|editor, window, cx| {
20819 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20820 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20821 });
20822 });
20823
20824 assert_indent_guides(
20825 0..4,
20826 vec![
20827 indent_guide(buffer_id, 1, 3, 0),
20828 indent_guide(buffer_id, 2, 2, 1),
20829 ],
20830 Some(vec![1]),
20831 &mut cx,
20832 );
20833
20834 cx.update_editor(|editor, window, cx| {
20835 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20836 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
20837 });
20838 });
20839
20840 assert_indent_guides(
20841 0..4,
20842 vec![
20843 indent_guide(buffer_id, 1, 3, 0),
20844 indent_guide(buffer_id, 2, 2, 1),
20845 ],
20846 Some(vec![0]),
20847 &mut cx,
20848 );
20849}
20850
20851#[gpui::test]
20852async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
20853 let (buffer_id, mut cx) = setup_indent_guides_editor(
20854 &"
20855 fn main() {
20856 let a = 1;
20857
20858 let b = 2;
20859 }"
20860 .unindent(),
20861 cx,
20862 )
20863 .await;
20864
20865 cx.update_editor(|editor, window, cx| {
20866 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20867 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20868 });
20869 });
20870
20871 assert_indent_guides(
20872 0..5,
20873 vec![indent_guide(buffer_id, 1, 3, 0)],
20874 Some(vec![0]),
20875 &mut cx,
20876 );
20877}
20878
20879#[gpui::test]
20880async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
20881 let (buffer_id, mut cx) = setup_indent_guides_editor(
20882 &"
20883 def m:
20884 a = 1
20885 pass"
20886 .unindent(),
20887 cx,
20888 )
20889 .await;
20890
20891 cx.update_editor(|editor, window, cx| {
20892 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20893 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20894 });
20895 });
20896
20897 assert_indent_guides(
20898 0..3,
20899 vec![indent_guide(buffer_id, 1, 2, 0)],
20900 Some(vec![0]),
20901 &mut cx,
20902 );
20903}
20904
20905#[gpui::test]
20906async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
20907 init_test(cx, |_| {});
20908 let mut cx = EditorTestContext::new(cx).await;
20909 let text = indoc! {
20910 "
20911 impl A {
20912 fn b() {
20913 0;
20914 3;
20915 5;
20916 6;
20917 7;
20918 }
20919 }
20920 "
20921 };
20922 let base_text = indoc! {
20923 "
20924 impl A {
20925 fn b() {
20926 0;
20927 1;
20928 2;
20929 3;
20930 4;
20931 }
20932 fn c() {
20933 5;
20934 6;
20935 7;
20936 }
20937 }
20938 "
20939 };
20940
20941 cx.update_editor(|editor, window, cx| {
20942 editor.set_text(text, window, cx);
20943
20944 editor.buffer().update(cx, |multibuffer, cx| {
20945 let buffer = multibuffer.as_singleton().unwrap();
20946 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
20947
20948 multibuffer.set_all_diff_hunks_expanded(cx);
20949 multibuffer.add_diff(diff, cx);
20950
20951 buffer.read(cx).remote_id()
20952 })
20953 });
20954 cx.run_until_parked();
20955
20956 cx.assert_state_with_diff(
20957 indoc! { "
20958 impl A {
20959 fn b() {
20960 0;
20961 - 1;
20962 - 2;
20963 3;
20964 - 4;
20965 - }
20966 - fn c() {
20967 5;
20968 6;
20969 7;
20970 }
20971 }
20972 ˇ"
20973 }
20974 .to_string(),
20975 );
20976
20977 let mut actual_guides = cx.update_editor(|editor, window, cx| {
20978 editor
20979 .snapshot(window, cx)
20980 .buffer_snapshot()
20981 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
20982 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
20983 .collect::<Vec<_>>()
20984 });
20985 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
20986 assert_eq!(
20987 actual_guides,
20988 vec![
20989 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
20990 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
20991 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
20992 ]
20993 );
20994}
20995
20996#[gpui::test]
20997async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20998 init_test(cx, |_| {});
20999 let mut cx = EditorTestContext::new(cx).await;
21000
21001 let diff_base = r#"
21002 a
21003 b
21004 c
21005 "#
21006 .unindent();
21007
21008 cx.set_state(
21009 &r#"
21010 ˇA
21011 b
21012 C
21013 "#
21014 .unindent(),
21015 );
21016 cx.set_head_text(&diff_base);
21017 cx.update_editor(|editor, window, cx| {
21018 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21019 });
21020 executor.run_until_parked();
21021
21022 let both_hunks_expanded = r#"
21023 - a
21024 + ˇA
21025 b
21026 - c
21027 + C
21028 "#
21029 .unindent();
21030
21031 cx.assert_state_with_diff(both_hunks_expanded.clone());
21032
21033 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21034 let snapshot = editor.snapshot(window, cx);
21035 let hunks = editor
21036 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21037 .collect::<Vec<_>>();
21038 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21039 let buffer_id = hunks[0].buffer_id;
21040 hunks
21041 .into_iter()
21042 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
21043 .collect::<Vec<_>>()
21044 });
21045 assert_eq!(hunk_ranges.len(), 2);
21046
21047 cx.update_editor(|editor, _, cx| {
21048 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21049 });
21050 executor.run_until_parked();
21051
21052 let second_hunk_expanded = r#"
21053 ˇA
21054 b
21055 - c
21056 + C
21057 "#
21058 .unindent();
21059
21060 cx.assert_state_with_diff(second_hunk_expanded);
21061
21062 cx.update_editor(|editor, _, cx| {
21063 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21064 });
21065 executor.run_until_parked();
21066
21067 cx.assert_state_with_diff(both_hunks_expanded.clone());
21068
21069 cx.update_editor(|editor, _, cx| {
21070 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21071 });
21072 executor.run_until_parked();
21073
21074 let first_hunk_expanded = r#"
21075 - a
21076 + ˇA
21077 b
21078 C
21079 "#
21080 .unindent();
21081
21082 cx.assert_state_with_diff(first_hunk_expanded);
21083
21084 cx.update_editor(|editor, _, cx| {
21085 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21086 });
21087 executor.run_until_parked();
21088
21089 cx.assert_state_with_diff(both_hunks_expanded);
21090
21091 cx.set_state(
21092 &r#"
21093 ˇA
21094 b
21095 "#
21096 .unindent(),
21097 );
21098 cx.run_until_parked();
21099
21100 // TODO this cursor position seems bad
21101 cx.assert_state_with_diff(
21102 r#"
21103 - ˇa
21104 + A
21105 b
21106 "#
21107 .unindent(),
21108 );
21109
21110 cx.update_editor(|editor, window, cx| {
21111 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21112 });
21113
21114 cx.assert_state_with_diff(
21115 r#"
21116 - ˇa
21117 + A
21118 b
21119 - c
21120 "#
21121 .unindent(),
21122 );
21123
21124 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21125 let snapshot = editor.snapshot(window, cx);
21126 let hunks = editor
21127 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21128 .collect::<Vec<_>>();
21129 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21130 let buffer_id = hunks[0].buffer_id;
21131 hunks
21132 .into_iter()
21133 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
21134 .collect::<Vec<_>>()
21135 });
21136 assert_eq!(hunk_ranges.len(), 2);
21137
21138 cx.update_editor(|editor, _, cx| {
21139 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21140 });
21141 executor.run_until_parked();
21142
21143 cx.assert_state_with_diff(
21144 r#"
21145 - ˇa
21146 + A
21147 b
21148 "#
21149 .unindent(),
21150 );
21151}
21152
21153#[gpui::test]
21154async fn test_toggle_deletion_hunk_at_start_of_file(
21155 executor: BackgroundExecutor,
21156 cx: &mut TestAppContext,
21157) {
21158 init_test(cx, |_| {});
21159 let mut cx = EditorTestContext::new(cx).await;
21160
21161 let diff_base = r#"
21162 a
21163 b
21164 c
21165 "#
21166 .unindent();
21167
21168 cx.set_state(
21169 &r#"
21170 ˇb
21171 c
21172 "#
21173 .unindent(),
21174 );
21175 cx.set_head_text(&diff_base);
21176 cx.update_editor(|editor, window, cx| {
21177 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21178 });
21179 executor.run_until_parked();
21180
21181 let hunk_expanded = r#"
21182 - a
21183 ˇb
21184 c
21185 "#
21186 .unindent();
21187
21188 cx.assert_state_with_diff(hunk_expanded.clone());
21189
21190 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21191 let snapshot = editor.snapshot(window, cx);
21192 let hunks = editor
21193 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21194 .collect::<Vec<_>>();
21195 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21196 let buffer_id = hunks[0].buffer_id;
21197 hunks
21198 .into_iter()
21199 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
21200 .collect::<Vec<_>>()
21201 });
21202 assert_eq!(hunk_ranges.len(), 1);
21203
21204 cx.update_editor(|editor, _, cx| {
21205 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21206 });
21207 executor.run_until_parked();
21208
21209 let hunk_collapsed = r#"
21210 ˇb
21211 c
21212 "#
21213 .unindent();
21214
21215 cx.assert_state_with_diff(hunk_collapsed);
21216
21217 cx.update_editor(|editor, _, cx| {
21218 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21219 });
21220 executor.run_until_parked();
21221
21222 cx.assert_state_with_diff(hunk_expanded);
21223}
21224
21225#[gpui::test]
21226async fn test_display_diff_hunks(cx: &mut TestAppContext) {
21227 init_test(cx, |_| {});
21228
21229 let fs = FakeFs::new(cx.executor());
21230 fs.insert_tree(
21231 path!("/test"),
21232 json!({
21233 ".git": {},
21234 "file-1": "ONE\n",
21235 "file-2": "TWO\n",
21236 "file-3": "THREE\n",
21237 }),
21238 )
21239 .await;
21240
21241 fs.set_head_for_repo(
21242 path!("/test/.git").as_ref(),
21243 &[
21244 ("file-1", "one\n".into()),
21245 ("file-2", "two\n".into()),
21246 ("file-3", "three\n".into()),
21247 ],
21248 "deadbeef",
21249 );
21250
21251 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
21252 let mut buffers = vec![];
21253 for i in 1..=3 {
21254 let buffer = project
21255 .update(cx, |project, cx| {
21256 let path = format!(path!("/test/file-{}"), i);
21257 project.open_local_buffer(path, cx)
21258 })
21259 .await
21260 .unwrap();
21261 buffers.push(buffer);
21262 }
21263
21264 let multibuffer = cx.new(|cx| {
21265 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
21266 multibuffer.set_all_diff_hunks_expanded(cx);
21267 for buffer in &buffers {
21268 let snapshot = buffer.read(cx).snapshot();
21269 multibuffer.set_excerpts_for_path(
21270 PathKey::with_sort_prefix(0, buffer.read(cx).file().unwrap().path().clone()),
21271 buffer.clone(),
21272 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
21273 2,
21274 cx,
21275 );
21276 }
21277 multibuffer
21278 });
21279
21280 let editor = cx.add_window(|window, cx| {
21281 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
21282 });
21283 cx.run_until_parked();
21284
21285 let snapshot = editor
21286 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21287 .unwrap();
21288 let hunks = snapshot
21289 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
21290 .map(|hunk| match hunk {
21291 DisplayDiffHunk::Unfolded {
21292 display_row_range, ..
21293 } => display_row_range,
21294 DisplayDiffHunk::Folded { .. } => unreachable!(),
21295 })
21296 .collect::<Vec<_>>();
21297 assert_eq!(
21298 hunks,
21299 [
21300 DisplayRow(2)..DisplayRow(4),
21301 DisplayRow(7)..DisplayRow(9),
21302 DisplayRow(12)..DisplayRow(14),
21303 ]
21304 );
21305}
21306
21307#[gpui::test]
21308async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
21309 init_test(cx, |_| {});
21310
21311 let mut cx = EditorTestContext::new(cx).await;
21312 cx.set_head_text(indoc! { "
21313 one
21314 two
21315 three
21316 four
21317 five
21318 "
21319 });
21320 cx.set_index_text(indoc! { "
21321 one
21322 two
21323 three
21324 four
21325 five
21326 "
21327 });
21328 cx.set_state(indoc! {"
21329 one
21330 TWO
21331 ˇTHREE
21332 FOUR
21333 five
21334 "});
21335 cx.run_until_parked();
21336 cx.update_editor(|editor, window, cx| {
21337 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21338 });
21339 cx.run_until_parked();
21340 cx.assert_index_text(Some(indoc! {"
21341 one
21342 TWO
21343 THREE
21344 FOUR
21345 five
21346 "}));
21347 cx.set_state(indoc! { "
21348 one
21349 TWO
21350 ˇTHREE-HUNDRED
21351 FOUR
21352 five
21353 "});
21354 cx.run_until_parked();
21355 cx.update_editor(|editor, window, cx| {
21356 let snapshot = editor.snapshot(window, cx);
21357 let hunks = editor
21358 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21359 .collect::<Vec<_>>();
21360 assert_eq!(hunks.len(), 1);
21361 assert_eq!(
21362 hunks[0].status(),
21363 DiffHunkStatus {
21364 kind: DiffHunkStatusKind::Modified,
21365 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
21366 }
21367 );
21368
21369 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21370 });
21371 cx.run_until_parked();
21372 cx.assert_index_text(Some(indoc! {"
21373 one
21374 TWO
21375 THREE-HUNDRED
21376 FOUR
21377 five
21378 "}));
21379}
21380
21381#[gpui::test]
21382fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
21383 init_test(cx, |_| {});
21384
21385 let editor = cx.add_window(|window, cx| {
21386 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
21387 build_editor(buffer, window, cx)
21388 });
21389
21390 let render_args = Arc::new(Mutex::new(None));
21391 let snapshot = editor
21392 .update(cx, |editor, window, cx| {
21393 let snapshot = editor.buffer().read(cx).snapshot(cx);
21394 let range =
21395 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
21396
21397 struct RenderArgs {
21398 row: MultiBufferRow,
21399 folded: bool,
21400 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
21401 }
21402
21403 let crease = Crease::inline(
21404 range,
21405 FoldPlaceholder::test(),
21406 {
21407 let toggle_callback = render_args.clone();
21408 move |row, folded, callback, _window, _cx| {
21409 *toggle_callback.lock() = Some(RenderArgs {
21410 row,
21411 folded,
21412 callback,
21413 });
21414 div()
21415 }
21416 },
21417 |_row, _folded, _window, _cx| div(),
21418 );
21419
21420 editor.insert_creases(Some(crease), cx);
21421 let snapshot = editor.snapshot(window, cx);
21422 let _div =
21423 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
21424 snapshot
21425 })
21426 .unwrap();
21427
21428 let render_args = render_args.lock().take().unwrap();
21429 assert_eq!(render_args.row, MultiBufferRow(1));
21430 assert!(!render_args.folded);
21431 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21432
21433 cx.update_window(*editor, |_, window, cx| {
21434 (render_args.callback)(true, window, cx)
21435 })
21436 .unwrap();
21437 let snapshot = editor
21438 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21439 .unwrap();
21440 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
21441
21442 cx.update_window(*editor, |_, window, cx| {
21443 (render_args.callback)(false, window, cx)
21444 })
21445 .unwrap();
21446 let snapshot = editor
21447 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21448 .unwrap();
21449 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21450}
21451
21452#[gpui::test]
21453async fn test_input_text(cx: &mut TestAppContext) {
21454 init_test(cx, |_| {});
21455 let mut cx = EditorTestContext::new(cx).await;
21456
21457 cx.set_state(
21458 &r#"ˇone
21459 two
21460
21461 three
21462 fourˇ
21463 five
21464
21465 siˇx"#
21466 .unindent(),
21467 );
21468
21469 cx.dispatch_action(HandleInput(String::new()));
21470 cx.assert_editor_state(
21471 &r#"ˇone
21472 two
21473
21474 three
21475 fourˇ
21476 five
21477
21478 siˇx"#
21479 .unindent(),
21480 );
21481
21482 cx.dispatch_action(HandleInput("AAAA".to_string()));
21483 cx.assert_editor_state(
21484 &r#"AAAAˇone
21485 two
21486
21487 three
21488 fourAAAAˇ
21489 five
21490
21491 siAAAAˇx"#
21492 .unindent(),
21493 );
21494}
21495
21496#[gpui::test]
21497async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
21498 init_test(cx, |_| {});
21499
21500 let mut cx = EditorTestContext::new(cx).await;
21501 cx.set_state(
21502 r#"let foo = 1;
21503let foo = 2;
21504let foo = 3;
21505let fooˇ = 4;
21506let foo = 5;
21507let foo = 6;
21508let foo = 7;
21509let foo = 8;
21510let foo = 9;
21511let foo = 10;
21512let foo = 11;
21513let foo = 12;
21514let foo = 13;
21515let foo = 14;
21516let foo = 15;"#,
21517 );
21518
21519 cx.update_editor(|e, window, cx| {
21520 assert_eq!(
21521 e.next_scroll_position,
21522 NextScrollCursorCenterTopBottom::Center,
21523 "Default next scroll direction is center",
21524 );
21525
21526 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21527 assert_eq!(
21528 e.next_scroll_position,
21529 NextScrollCursorCenterTopBottom::Top,
21530 "After center, next scroll direction should be top",
21531 );
21532
21533 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21534 assert_eq!(
21535 e.next_scroll_position,
21536 NextScrollCursorCenterTopBottom::Bottom,
21537 "After top, next scroll direction should be bottom",
21538 );
21539
21540 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21541 assert_eq!(
21542 e.next_scroll_position,
21543 NextScrollCursorCenterTopBottom::Center,
21544 "After bottom, scrolling should start over",
21545 );
21546
21547 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21548 assert_eq!(
21549 e.next_scroll_position,
21550 NextScrollCursorCenterTopBottom::Top,
21551 "Scrolling continues if retriggered fast enough"
21552 );
21553 });
21554
21555 cx.executor()
21556 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
21557 cx.executor().run_until_parked();
21558 cx.update_editor(|e, _, _| {
21559 assert_eq!(
21560 e.next_scroll_position,
21561 NextScrollCursorCenterTopBottom::Center,
21562 "If scrolling is not triggered fast enough, it should reset"
21563 );
21564 });
21565}
21566
21567#[gpui::test]
21568async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
21569 init_test(cx, |_| {});
21570 let mut cx = EditorLspTestContext::new_rust(
21571 lsp::ServerCapabilities {
21572 definition_provider: Some(lsp::OneOf::Left(true)),
21573 references_provider: Some(lsp::OneOf::Left(true)),
21574 ..lsp::ServerCapabilities::default()
21575 },
21576 cx,
21577 )
21578 .await;
21579
21580 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
21581 let go_to_definition = cx
21582 .lsp
21583 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21584 move |params, _| async move {
21585 if empty_go_to_definition {
21586 Ok(None)
21587 } else {
21588 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
21589 uri: params.text_document_position_params.text_document.uri,
21590 range: lsp::Range::new(
21591 lsp::Position::new(4, 3),
21592 lsp::Position::new(4, 6),
21593 ),
21594 })))
21595 }
21596 },
21597 );
21598 let references = cx
21599 .lsp
21600 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21601 Ok(Some(vec![lsp::Location {
21602 uri: params.text_document_position.text_document.uri,
21603 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
21604 }]))
21605 });
21606 (go_to_definition, references)
21607 };
21608
21609 cx.set_state(
21610 &r#"fn one() {
21611 let mut a = ˇtwo();
21612 }
21613
21614 fn two() {}"#
21615 .unindent(),
21616 );
21617 set_up_lsp_handlers(false, &mut cx);
21618 let navigated = cx
21619 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21620 .await
21621 .expect("Failed to navigate to definition");
21622 assert_eq!(
21623 navigated,
21624 Navigated::Yes,
21625 "Should have navigated to definition from the GetDefinition response"
21626 );
21627 cx.assert_editor_state(
21628 &r#"fn one() {
21629 let mut a = two();
21630 }
21631
21632 fn «twoˇ»() {}"#
21633 .unindent(),
21634 );
21635
21636 let editors = cx.update_workspace(|workspace, _, cx| {
21637 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21638 });
21639 cx.update_editor(|_, _, test_editor_cx| {
21640 assert_eq!(
21641 editors.len(),
21642 1,
21643 "Initially, only one, test, editor should be open in the workspace"
21644 );
21645 assert_eq!(
21646 test_editor_cx.entity(),
21647 editors.last().expect("Asserted len is 1").clone()
21648 );
21649 });
21650
21651 set_up_lsp_handlers(true, &mut cx);
21652 let navigated = cx
21653 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21654 .await
21655 .expect("Failed to navigate to lookup references");
21656 assert_eq!(
21657 navigated,
21658 Navigated::Yes,
21659 "Should have navigated to references as a fallback after empty GoToDefinition response"
21660 );
21661 // We should not change the selections in the existing file,
21662 // if opening another milti buffer with the references
21663 cx.assert_editor_state(
21664 &r#"fn one() {
21665 let mut a = two();
21666 }
21667
21668 fn «twoˇ»() {}"#
21669 .unindent(),
21670 );
21671 let editors = cx.update_workspace(|workspace, _, cx| {
21672 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21673 });
21674 cx.update_editor(|_, _, test_editor_cx| {
21675 assert_eq!(
21676 editors.len(),
21677 2,
21678 "After falling back to references search, we open a new editor with the results"
21679 );
21680 let references_fallback_text = editors
21681 .into_iter()
21682 .find(|new_editor| *new_editor != test_editor_cx.entity())
21683 .expect("Should have one non-test editor now")
21684 .read(test_editor_cx)
21685 .text(test_editor_cx);
21686 assert_eq!(
21687 references_fallback_text, "fn one() {\n let mut a = two();\n}",
21688 "Should use the range from the references response and not the GoToDefinition one"
21689 );
21690 });
21691}
21692
21693#[gpui::test]
21694async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
21695 init_test(cx, |_| {});
21696 cx.update(|cx| {
21697 let mut editor_settings = EditorSettings::get_global(cx).clone();
21698 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
21699 EditorSettings::override_global(editor_settings, cx);
21700 });
21701 let mut cx = EditorLspTestContext::new_rust(
21702 lsp::ServerCapabilities {
21703 definition_provider: Some(lsp::OneOf::Left(true)),
21704 references_provider: Some(lsp::OneOf::Left(true)),
21705 ..lsp::ServerCapabilities::default()
21706 },
21707 cx,
21708 )
21709 .await;
21710 let original_state = r#"fn one() {
21711 let mut a = ˇtwo();
21712 }
21713
21714 fn two() {}"#
21715 .unindent();
21716 cx.set_state(&original_state);
21717
21718 let mut go_to_definition = cx
21719 .lsp
21720 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21721 move |_, _| async move { Ok(None) },
21722 );
21723 let _references = cx
21724 .lsp
21725 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
21726 panic!("Should not call for references with no go to definition fallback")
21727 });
21728
21729 let navigated = cx
21730 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21731 .await
21732 .expect("Failed to navigate to lookup references");
21733 go_to_definition
21734 .next()
21735 .await
21736 .expect("Should have called the go_to_definition handler");
21737
21738 assert_eq!(
21739 navigated,
21740 Navigated::No,
21741 "Should have navigated to references as a fallback after empty GoToDefinition response"
21742 );
21743 cx.assert_editor_state(&original_state);
21744 let editors = cx.update_workspace(|workspace, _, cx| {
21745 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21746 });
21747 cx.update_editor(|_, _, _| {
21748 assert_eq!(
21749 editors.len(),
21750 1,
21751 "After unsuccessful fallback, no other editor should have been opened"
21752 );
21753 });
21754}
21755
21756#[gpui::test]
21757async fn test_find_all_references_editor_reuse(cx: &mut TestAppContext) {
21758 init_test(cx, |_| {});
21759 let mut cx = EditorLspTestContext::new_rust(
21760 lsp::ServerCapabilities {
21761 references_provider: Some(lsp::OneOf::Left(true)),
21762 ..lsp::ServerCapabilities::default()
21763 },
21764 cx,
21765 )
21766 .await;
21767
21768 cx.set_state(
21769 &r#"
21770 fn one() {
21771 let mut a = two();
21772 }
21773
21774 fn ˇtwo() {}"#
21775 .unindent(),
21776 );
21777 cx.lsp
21778 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21779 Ok(Some(vec![
21780 lsp::Location {
21781 uri: params.text_document_position.text_document.uri.clone(),
21782 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21783 },
21784 lsp::Location {
21785 uri: params.text_document_position.text_document.uri,
21786 range: lsp::Range::new(lsp::Position::new(4, 4), lsp::Position::new(4, 7)),
21787 },
21788 ]))
21789 });
21790 let navigated = cx
21791 .update_editor(|editor, window, cx| {
21792 editor.find_all_references(&FindAllReferences, window, cx)
21793 })
21794 .unwrap()
21795 .await
21796 .expect("Failed to navigate to references");
21797 assert_eq!(
21798 navigated,
21799 Navigated::Yes,
21800 "Should have navigated to references from the FindAllReferences response"
21801 );
21802 cx.assert_editor_state(
21803 &r#"fn one() {
21804 let mut a = two();
21805 }
21806
21807 fn ˇtwo() {}"#
21808 .unindent(),
21809 );
21810
21811 let editors = cx.update_workspace(|workspace, _, cx| {
21812 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21813 });
21814 cx.update_editor(|_, _, _| {
21815 assert_eq!(editors.len(), 2, "We should have opened a new multibuffer");
21816 });
21817
21818 cx.set_state(
21819 &r#"fn one() {
21820 let mut a = ˇtwo();
21821 }
21822
21823 fn two() {}"#
21824 .unindent(),
21825 );
21826 let navigated = cx
21827 .update_editor(|editor, window, cx| {
21828 editor.find_all_references(&FindAllReferences, window, cx)
21829 })
21830 .unwrap()
21831 .await
21832 .expect("Failed to navigate to references");
21833 assert_eq!(
21834 navigated,
21835 Navigated::Yes,
21836 "Should have navigated to references from the FindAllReferences response"
21837 );
21838 cx.assert_editor_state(
21839 &r#"fn one() {
21840 let mut a = ˇtwo();
21841 }
21842
21843 fn two() {}"#
21844 .unindent(),
21845 );
21846 let editors = cx.update_workspace(|workspace, _, cx| {
21847 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21848 });
21849 cx.update_editor(|_, _, _| {
21850 assert_eq!(
21851 editors.len(),
21852 2,
21853 "should have re-used the previous multibuffer"
21854 );
21855 });
21856
21857 cx.set_state(
21858 &r#"fn one() {
21859 let mut a = ˇtwo();
21860 }
21861 fn three() {}
21862 fn two() {}"#
21863 .unindent(),
21864 );
21865 cx.lsp
21866 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21867 Ok(Some(vec![
21868 lsp::Location {
21869 uri: params.text_document_position.text_document.uri.clone(),
21870 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21871 },
21872 lsp::Location {
21873 uri: params.text_document_position.text_document.uri,
21874 range: lsp::Range::new(lsp::Position::new(5, 4), lsp::Position::new(5, 7)),
21875 },
21876 ]))
21877 });
21878 let navigated = cx
21879 .update_editor(|editor, window, cx| {
21880 editor.find_all_references(&FindAllReferences, window, cx)
21881 })
21882 .unwrap()
21883 .await
21884 .expect("Failed to navigate to references");
21885 assert_eq!(
21886 navigated,
21887 Navigated::Yes,
21888 "Should have navigated to references from the FindAllReferences response"
21889 );
21890 cx.assert_editor_state(
21891 &r#"fn one() {
21892 let mut a = ˇtwo();
21893 }
21894 fn three() {}
21895 fn two() {}"#
21896 .unindent(),
21897 );
21898 let editors = cx.update_workspace(|workspace, _, cx| {
21899 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21900 });
21901 cx.update_editor(|_, _, _| {
21902 assert_eq!(
21903 editors.len(),
21904 3,
21905 "should have used a new multibuffer as offsets changed"
21906 );
21907 });
21908}
21909#[gpui::test]
21910async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
21911 init_test(cx, |_| {});
21912
21913 let language = Arc::new(Language::new(
21914 LanguageConfig::default(),
21915 Some(tree_sitter_rust::LANGUAGE.into()),
21916 ));
21917
21918 let text = r#"
21919 #[cfg(test)]
21920 mod tests() {
21921 #[test]
21922 fn runnable_1() {
21923 let a = 1;
21924 }
21925
21926 #[test]
21927 fn runnable_2() {
21928 let a = 1;
21929 let b = 2;
21930 }
21931 }
21932 "#
21933 .unindent();
21934
21935 let fs = FakeFs::new(cx.executor());
21936 fs.insert_file("/file.rs", Default::default()).await;
21937
21938 let project = Project::test(fs, ["/a".as_ref()], cx).await;
21939 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21940 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21941 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
21942 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
21943
21944 let editor = cx.new_window_entity(|window, cx| {
21945 Editor::new(
21946 EditorMode::full(),
21947 multi_buffer,
21948 Some(project.clone()),
21949 window,
21950 cx,
21951 )
21952 });
21953
21954 editor.update_in(cx, |editor, window, cx| {
21955 let snapshot = editor.buffer().read(cx).snapshot(cx);
21956 editor.tasks.insert(
21957 (buffer.read(cx).remote_id(), 3),
21958 RunnableTasks {
21959 templates: vec![],
21960 offset: snapshot.anchor_before(43),
21961 column: 0,
21962 extra_variables: HashMap::default(),
21963 context_range: BufferOffset(43)..BufferOffset(85),
21964 },
21965 );
21966 editor.tasks.insert(
21967 (buffer.read(cx).remote_id(), 8),
21968 RunnableTasks {
21969 templates: vec![],
21970 offset: snapshot.anchor_before(86),
21971 column: 0,
21972 extra_variables: HashMap::default(),
21973 context_range: BufferOffset(86)..BufferOffset(191),
21974 },
21975 );
21976
21977 // Test finding task when cursor is inside function body
21978 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21979 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
21980 });
21981 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
21982 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
21983
21984 // Test finding task when cursor is on function name
21985 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21986 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
21987 });
21988 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
21989 assert_eq!(row, 8, "Should find task when cursor is on function name");
21990 });
21991}
21992
21993#[gpui::test]
21994async fn test_folding_buffers(cx: &mut TestAppContext) {
21995 init_test(cx, |_| {});
21996
21997 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
21998 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
21999 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
22000
22001 let fs = FakeFs::new(cx.executor());
22002 fs.insert_tree(
22003 path!("/a"),
22004 json!({
22005 "first.rs": sample_text_1,
22006 "second.rs": sample_text_2,
22007 "third.rs": sample_text_3,
22008 }),
22009 )
22010 .await;
22011 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22012 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22013 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22014 let worktree = project.update(cx, |project, cx| {
22015 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22016 assert_eq!(worktrees.len(), 1);
22017 worktrees.pop().unwrap()
22018 });
22019 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22020
22021 let buffer_1 = project
22022 .update(cx, |project, cx| {
22023 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
22024 })
22025 .await
22026 .unwrap();
22027 let buffer_2 = project
22028 .update(cx, |project, cx| {
22029 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
22030 })
22031 .await
22032 .unwrap();
22033 let buffer_3 = project
22034 .update(cx, |project, cx| {
22035 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
22036 })
22037 .await
22038 .unwrap();
22039
22040 let multi_buffer = cx.new(|cx| {
22041 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22042 multi_buffer.push_excerpts(
22043 buffer_1.clone(),
22044 [
22045 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22046 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22047 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22048 ],
22049 cx,
22050 );
22051 multi_buffer.push_excerpts(
22052 buffer_2.clone(),
22053 [
22054 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22055 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22056 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22057 ],
22058 cx,
22059 );
22060 multi_buffer.push_excerpts(
22061 buffer_3.clone(),
22062 [
22063 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22064 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22065 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22066 ],
22067 cx,
22068 );
22069 multi_buffer
22070 });
22071 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22072 Editor::new(
22073 EditorMode::full(),
22074 multi_buffer.clone(),
22075 Some(project.clone()),
22076 window,
22077 cx,
22078 )
22079 });
22080
22081 assert_eq!(
22082 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22083 "\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",
22084 );
22085
22086 multi_buffer_editor.update(cx, |editor, cx| {
22087 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
22088 });
22089 assert_eq!(
22090 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22091 "\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",
22092 "After folding the first buffer, its text should not be displayed"
22093 );
22094
22095 multi_buffer_editor.update(cx, |editor, cx| {
22096 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
22097 });
22098 assert_eq!(
22099 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22100 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
22101 "After folding the second buffer, its text should not be displayed"
22102 );
22103
22104 multi_buffer_editor.update(cx, |editor, cx| {
22105 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
22106 });
22107 assert_eq!(
22108 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22109 "\n\n\n\n\n",
22110 "After folding the third buffer, its text should not be displayed"
22111 );
22112
22113 // Emulate selection inside the fold logic, that should work
22114 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22115 editor
22116 .snapshot(window, cx)
22117 .next_line_boundary(Point::new(0, 4));
22118 });
22119
22120 multi_buffer_editor.update(cx, |editor, cx| {
22121 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
22122 });
22123 assert_eq!(
22124 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22125 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
22126 "After unfolding the second buffer, its text should be displayed"
22127 );
22128
22129 // Typing inside of buffer 1 causes that buffer to be unfolded.
22130 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22131 assert_eq!(
22132 multi_buffer
22133 .read(cx)
22134 .snapshot(cx)
22135 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
22136 .collect::<String>(),
22137 "bbbb"
22138 );
22139 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
22140 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
22141 });
22142 editor.handle_input("B", window, cx);
22143 });
22144
22145 assert_eq!(
22146 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22147 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
22148 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
22149 );
22150
22151 multi_buffer_editor.update(cx, |editor, cx| {
22152 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
22153 });
22154 assert_eq!(
22155 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22156 "\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",
22157 "After unfolding the all buffers, all original text should be displayed"
22158 );
22159}
22160
22161#[gpui::test]
22162async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
22163 init_test(cx, |_| {});
22164
22165 let sample_text_1 = "1111\n2222\n3333".to_string();
22166 let sample_text_2 = "4444\n5555\n6666".to_string();
22167 let sample_text_3 = "7777\n8888\n9999".to_string();
22168
22169 let fs = FakeFs::new(cx.executor());
22170 fs.insert_tree(
22171 path!("/a"),
22172 json!({
22173 "first.rs": sample_text_1,
22174 "second.rs": sample_text_2,
22175 "third.rs": sample_text_3,
22176 }),
22177 )
22178 .await;
22179 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22180 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22181 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22182 let worktree = project.update(cx, |project, cx| {
22183 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22184 assert_eq!(worktrees.len(), 1);
22185 worktrees.pop().unwrap()
22186 });
22187 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22188
22189 let buffer_1 = project
22190 .update(cx, |project, cx| {
22191 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
22192 })
22193 .await
22194 .unwrap();
22195 let buffer_2 = project
22196 .update(cx, |project, cx| {
22197 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
22198 })
22199 .await
22200 .unwrap();
22201 let buffer_3 = project
22202 .update(cx, |project, cx| {
22203 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
22204 })
22205 .await
22206 .unwrap();
22207
22208 let multi_buffer = cx.new(|cx| {
22209 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22210 multi_buffer.push_excerpts(
22211 buffer_1.clone(),
22212 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22213 cx,
22214 );
22215 multi_buffer.push_excerpts(
22216 buffer_2.clone(),
22217 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22218 cx,
22219 );
22220 multi_buffer.push_excerpts(
22221 buffer_3.clone(),
22222 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22223 cx,
22224 );
22225 multi_buffer
22226 });
22227
22228 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22229 Editor::new(
22230 EditorMode::full(),
22231 multi_buffer,
22232 Some(project.clone()),
22233 window,
22234 cx,
22235 )
22236 });
22237
22238 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
22239 assert_eq!(
22240 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22241 full_text,
22242 );
22243
22244 multi_buffer_editor.update(cx, |editor, cx| {
22245 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
22246 });
22247 assert_eq!(
22248 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22249 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
22250 "After folding the first buffer, its text should not be displayed"
22251 );
22252
22253 multi_buffer_editor.update(cx, |editor, cx| {
22254 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
22255 });
22256
22257 assert_eq!(
22258 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22259 "\n\n\n\n\n\n7777\n8888\n9999",
22260 "After folding the second buffer, its text should not be displayed"
22261 );
22262
22263 multi_buffer_editor.update(cx, |editor, cx| {
22264 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
22265 });
22266 assert_eq!(
22267 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22268 "\n\n\n\n\n",
22269 "After folding the third buffer, its text should not be displayed"
22270 );
22271
22272 multi_buffer_editor.update(cx, |editor, cx| {
22273 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
22274 });
22275 assert_eq!(
22276 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22277 "\n\n\n\n4444\n5555\n6666\n\n",
22278 "After unfolding the second buffer, its text should be displayed"
22279 );
22280
22281 multi_buffer_editor.update(cx, |editor, cx| {
22282 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
22283 });
22284 assert_eq!(
22285 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22286 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
22287 "After unfolding the first buffer, its text should be displayed"
22288 );
22289
22290 multi_buffer_editor.update(cx, |editor, cx| {
22291 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
22292 });
22293 assert_eq!(
22294 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22295 full_text,
22296 "After unfolding all buffers, all original text should be displayed"
22297 );
22298}
22299
22300#[gpui::test]
22301async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
22302 init_test(cx, |_| {});
22303
22304 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
22305
22306 let fs = FakeFs::new(cx.executor());
22307 fs.insert_tree(
22308 path!("/a"),
22309 json!({
22310 "main.rs": sample_text,
22311 }),
22312 )
22313 .await;
22314 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22315 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22316 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22317 let worktree = project.update(cx, |project, cx| {
22318 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22319 assert_eq!(worktrees.len(), 1);
22320 worktrees.pop().unwrap()
22321 });
22322 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22323
22324 let buffer_1 = project
22325 .update(cx, |project, cx| {
22326 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22327 })
22328 .await
22329 .unwrap();
22330
22331 let multi_buffer = cx.new(|cx| {
22332 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22333 multi_buffer.push_excerpts(
22334 buffer_1.clone(),
22335 [ExcerptRange::new(
22336 Point::new(0, 0)
22337 ..Point::new(
22338 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
22339 0,
22340 ),
22341 )],
22342 cx,
22343 );
22344 multi_buffer
22345 });
22346 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22347 Editor::new(
22348 EditorMode::full(),
22349 multi_buffer,
22350 Some(project.clone()),
22351 window,
22352 cx,
22353 )
22354 });
22355
22356 let selection_range = Point::new(1, 0)..Point::new(2, 0);
22357 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22358 enum TestHighlight {}
22359 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
22360 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
22361 editor.highlight_text::<TestHighlight>(
22362 vec![highlight_range.clone()],
22363 HighlightStyle::color(Hsla::green()),
22364 cx,
22365 );
22366 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22367 s.select_ranges(Some(highlight_range))
22368 });
22369 });
22370
22371 let full_text = format!("\n\n{sample_text}");
22372 assert_eq!(
22373 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22374 full_text,
22375 );
22376}
22377
22378#[gpui::test]
22379async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
22380 init_test(cx, |_| {});
22381 cx.update(|cx| {
22382 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
22383 "keymaps/default-linux.json",
22384 cx,
22385 )
22386 .unwrap();
22387 cx.bind_keys(default_key_bindings);
22388 });
22389
22390 let (editor, cx) = cx.add_window_view(|window, cx| {
22391 let multi_buffer = MultiBuffer::build_multi(
22392 [
22393 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
22394 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
22395 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
22396 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
22397 ],
22398 cx,
22399 );
22400 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
22401
22402 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
22403 // fold all but the second buffer, so that we test navigating between two
22404 // adjacent folded buffers, as well as folded buffers at the start and
22405 // end the multibuffer
22406 editor.fold_buffer(buffer_ids[0], cx);
22407 editor.fold_buffer(buffer_ids[2], cx);
22408 editor.fold_buffer(buffer_ids[3], cx);
22409
22410 editor
22411 });
22412 cx.simulate_resize(size(px(1000.), px(1000.)));
22413
22414 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
22415 cx.assert_excerpts_with_selections(indoc! {"
22416 [EXCERPT]
22417 ˇ[FOLDED]
22418 [EXCERPT]
22419 a1
22420 b1
22421 [EXCERPT]
22422 [FOLDED]
22423 [EXCERPT]
22424 [FOLDED]
22425 "
22426 });
22427 cx.simulate_keystroke("down");
22428 cx.assert_excerpts_with_selections(indoc! {"
22429 [EXCERPT]
22430 [FOLDED]
22431 [EXCERPT]
22432 ˇa1
22433 b1
22434 [EXCERPT]
22435 [FOLDED]
22436 [EXCERPT]
22437 [FOLDED]
22438 "
22439 });
22440 cx.simulate_keystroke("down");
22441 cx.assert_excerpts_with_selections(indoc! {"
22442 [EXCERPT]
22443 [FOLDED]
22444 [EXCERPT]
22445 a1
22446 ˇb1
22447 [EXCERPT]
22448 [FOLDED]
22449 [EXCERPT]
22450 [FOLDED]
22451 "
22452 });
22453 cx.simulate_keystroke("down");
22454 cx.assert_excerpts_with_selections(indoc! {"
22455 [EXCERPT]
22456 [FOLDED]
22457 [EXCERPT]
22458 a1
22459 b1
22460 ˇ[EXCERPT]
22461 [FOLDED]
22462 [EXCERPT]
22463 [FOLDED]
22464 "
22465 });
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 for _ in 0..5 {
22480 cx.simulate_keystroke("down");
22481 cx.assert_excerpts_with_selections(indoc! {"
22482 [EXCERPT]
22483 [FOLDED]
22484 [EXCERPT]
22485 a1
22486 b1
22487 [EXCERPT]
22488 [FOLDED]
22489 [EXCERPT]
22490 ˇ[FOLDED]
22491 "
22492 });
22493 }
22494
22495 cx.simulate_keystroke("up");
22496 cx.assert_excerpts_with_selections(indoc! {"
22497 [EXCERPT]
22498 [FOLDED]
22499 [EXCERPT]
22500 a1
22501 b1
22502 [EXCERPT]
22503 ˇ[FOLDED]
22504 [EXCERPT]
22505 [FOLDED]
22506 "
22507 });
22508 cx.simulate_keystroke("up");
22509 cx.assert_excerpts_with_selections(indoc! {"
22510 [EXCERPT]
22511 [FOLDED]
22512 [EXCERPT]
22513 a1
22514 b1
22515 ˇ[EXCERPT]
22516 [FOLDED]
22517 [EXCERPT]
22518 [FOLDED]
22519 "
22520 });
22521 cx.simulate_keystroke("up");
22522 cx.assert_excerpts_with_selections(indoc! {"
22523 [EXCERPT]
22524 [FOLDED]
22525 [EXCERPT]
22526 a1
22527 ˇb1
22528 [EXCERPT]
22529 [FOLDED]
22530 [EXCERPT]
22531 [FOLDED]
22532 "
22533 });
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 for _ in 0..5 {
22548 cx.simulate_keystroke("up");
22549 cx.assert_excerpts_with_selections(indoc! {"
22550 [EXCERPT]
22551 ˇ[FOLDED]
22552 [EXCERPT]
22553 a1
22554 b1
22555 [EXCERPT]
22556 [FOLDED]
22557 [EXCERPT]
22558 [FOLDED]
22559 "
22560 });
22561 }
22562}
22563
22564#[gpui::test]
22565async fn test_edit_prediction_text(cx: &mut TestAppContext) {
22566 init_test(cx, |_| {});
22567
22568 // Simple insertion
22569 assert_highlighted_edits(
22570 "Hello, world!",
22571 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
22572 true,
22573 cx,
22574 |highlighted_edits, cx| {
22575 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
22576 assert_eq!(highlighted_edits.highlights.len(), 1);
22577 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
22578 assert_eq!(
22579 highlighted_edits.highlights[0].1.background_color,
22580 Some(cx.theme().status().created_background)
22581 );
22582 },
22583 )
22584 .await;
22585
22586 // Replacement
22587 assert_highlighted_edits(
22588 "This is a test.",
22589 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
22590 false,
22591 cx,
22592 |highlighted_edits, cx| {
22593 assert_eq!(highlighted_edits.text, "That is a test.");
22594 assert_eq!(highlighted_edits.highlights.len(), 1);
22595 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
22596 assert_eq!(
22597 highlighted_edits.highlights[0].1.background_color,
22598 Some(cx.theme().status().created_background)
22599 );
22600 },
22601 )
22602 .await;
22603
22604 // Multiple edits
22605 assert_highlighted_edits(
22606 "Hello, world!",
22607 vec![
22608 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
22609 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
22610 ],
22611 false,
22612 cx,
22613 |highlighted_edits, cx| {
22614 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
22615 assert_eq!(highlighted_edits.highlights.len(), 2);
22616 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
22617 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
22618 assert_eq!(
22619 highlighted_edits.highlights[0].1.background_color,
22620 Some(cx.theme().status().created_background)
22621 );
22622 assert_eq!(
22623 highlighted_edits.highlights[1].1.background_color,
22624 Some(cx.theme().status().created_background)
22625 );
22626 },
22627 )
22628 .await;
22629
22630 // Multiple lines with edits
22631 assert_highlighted_edits(
22632 "First line\nSecond line\nThird line\nFourth line",
22633 vec![
22634 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
22635 (
22636 Point::new(2, 0)..Point::new(2, 10),
22637 "New third line".to_string(),
22638 ),
22639 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
22640 ],
22641 false,
22642 cx,
22643 |highlighted_edits, cx| {
22644 assert_eq!(
22645 highlighted_edits.text,
22646 "Second modified\nNew third line\nFourth updated line"
22647 );
22648 assert_eq!(highlighted_edits.highlights.len(), 3);
22649 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
22650 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
22651 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
22652 for highlight in &highlighted_edits.highlights {
22653 assert_eq!(
22654 highlight.1.background_color,
22655 Some(cx.theme().status().created_background)
22656 );
22657 }
22658 },
22659 )
22660 .await;
22661}
22662
22663#[gpui::test]
22664async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
22665 init_test(cx, |_| {});
22666
22667 // Deletion
22668 assert_highlighted_edits(
22669 "Hello, world!",
22670 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
22671 true,
22672 cx,
22673 |highlighted_edits, cx| {
22674 assert_eq!(highlighted_edits.text, "Hello, world!");
22675 assert_eq!(highlighted_edits.highlights.len(), 1);
22676 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
22677 assert_eq!(
22678 highlighted_edits.highlights[0].1.background_color,
22679 Some(cx.theme().status().deleted_background)
22680 );
22681 },
22682 )
22683 .await;
22684
22685 // Insertion
22686 assert_highlighted_edits(
22687 "Hello, world!",
22688 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
22689 true,
22690 cx,
22691 |highlighted_edits, cx| {
22692 assert_eq!(highlighted_edits.highlights.len(), 1);
22693 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
22694 assert_eq!(
22695 highlighted_edits.highlights[0].1.background_color,
22696 Some(cx.theme().status().created_background)
22697 );
22698 },
22699 )
22700 .await;
22701}
22702
22703async fn assert_highlighted_edits(
22704 text: &str,
22705 edits: Vec<(Range<Point>, String)>,
22706 include_deletions: bool,
22707 cx: &mut TestAppContext,
22708 assertion_fn: impl Fn(HighlightedText, &App),
22709) {
22710 let window = cx.add_window(|window, cx| {
22711 let buffer = MultiBuffer::build_simple(text, cx);
22712 Editor::new(EditorMode::full(), buffer, None, window, cx)
22713 });
22714 let cx = &mut VisualTestContext::from_window(*window, cx);
22715
22716 let (buffer, snapshot) = window
22717 .update(cx, |editor, _window, cx| {
22718 (
22719 editor.buffer().clone(),
22720 editor.buffer().read(cx).snapshot(cx),
22721 )
22722 })
22723 .unwrap();
22724
22725 let edits = edits
22726 .into_iter()
22727 .map(|(range, edit)| {
22728 (
22729 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
22730 edit,
22731 )
22732 })
22733 .collect::<Vec<_>>();
22734
22735 let text_anchor_edits = edits
22736 .clone()
22737 .into_iter()
22738 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
22739 .collect::<Vec<_>>();
22740
22741 let edit_preview = window
22742 .update(cx, |_, _window, cx| {
22743 buffer
22744 .read(cx)
22745 .as_singleton()
22746 .unwrap()
22747 .read(cx)
22748 .preview_edits(text_anchor_edits.into(), cx)
22749 })
22750 .unwrap()
22751 .await;
22752
22753 cx.update(|_window, cx| {
22754 let highlighted_edits = edit_prediction_edit_text(
22755 snapshot.as_singleton().unwrap().2,
22756 &edits,
22757 &edit_preview,
22758 include_deletions,
22759 cx,
22760 );
22761 assertion_fn(highlighted_edits, cx)
22762 });
22763}
22764
22765#[track_caller]
22766fn assert_breakpoint(
22767 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
22768 path: &Arc<Path>,
22769 expected: Vec<(u32, Breakpoint)>,
22770) {
22771 if expected.is_empty() {
22772 assert!(!breakpoints.contains_key(path), "{}", path.display());
22773 } else {
22774 let mut breakpoint = breakpoints
22775 .get(path)
22776 .unwrap()
22777 .iter()
22778 .map(|breakpoint| {
22779 (
22780 breakpoint.row,
22781 Breakpoint {
22782 message: breakpoint.message.clone(),
22783 state: breakpoint.state,
22784 condition: breakpoint.condition.clone(),
22785 hit_condition: breakpoint.hit_condition.clone(),
22786 },
22787 )
22788 })
22789 .collect::<Vec<_>>();
22790
22791 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
22792
22793 assert_eq!(expected, breakpoint);
22794 }
22795}
22796
22797fn add_log_breakpoint_at_cursor(
22798 editor: &mut Editor,
22799 log_message: &str,
22800 window: &mut Window,
22801 cx: &mut Context<Editor>,
22802) {
22803 let (anchor, bp) = editor
22804 .breakpoints_at_cursors(window, cx)
22805 .first()
22806 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
22807 .unwrap_or_else(|| {
22808 let snapshot = editor.snapshot(window, cx);
22809 let cursor_position: Point =
22810 editor.selections.newest(&snapshot.display_snapshot).head();
22811
22812 let breakpoint_position = snapshot
22813 .buffer_snapshot()
22814 .anchor_before(Point::new(cursor_position.row, 0));
22815
22816 (breakpoint_position, Breakpoint::new_log(log_message))
22817 });
22818
22819 editor.edit_breakpoint_at_anchor(
22820 anchor,
22821 bp,
22822 BreakpointEditAction::EditLogMessage(log_message.into()),
22823 cx,
22824 );
22825}
22826
22827#[gpui::test]
22828async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
22829 init_test(cx, |_| {});
22830
22831 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22832 let fs = FakeFs::new(cx.executor());
22833 fs.insert_tree(
22834 path!("/a"),
22835 json!({
22836 "main.rs": sample_text,
22837 }),
22838 )
22839 .await;
22840 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22841 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22842 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22843
22844 let fs = FakeFs::new(cx.executor());
22845 fs.insert_tree(
22846 path!("/a"),
22847 json!({
22848 "main.rs": sample_text,
22849 }),
22850 )
22851 .await;
22852 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22853 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22854 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22855 let worktree_id = workspace
22856 .update(cx, |workspace, _window, cx| {
22857 workspace.project().update(cx, |project, cx| {
22858 project.worktrees(cx).next().unwrap().read(cx).id()
22859 })
22860 })
22861 .unwrap();
22862
22863 let buffer = project
22864 .update(cx, |project, cx| {
22865 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22866 })
22867 .await
22868 .unwrap();
22869
22870 let (editor, cx) = cx.add_window_view(|window, cx| {
22871 Editor::new(
22872 EditorMode::full(),
22873 MultiBuffer::build_from_buffer(buffer, cx),
22874 Some(project.clone()),
22875 window,
22876 cx,
22877 )
22878 });
22879
22880 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22881 let abs_path = project.read_with(cx, |project, cx| {
22882 project
22883 .absolute_path(&project_path, cx)
22884 .map(Arc::from)
22885 .unwrap()
22886 });
22887
22888 // assert we can add breakpoint on the first line
22889 editor.update_in(cx, |editor, window, cx| {
22890 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22891 editor.move_to_end(&MoveToEnd, window, cx);
22892 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22893 });
22894
22895 let breakpoints = editor.update(cx, |editor, cx| {
22896 editor
22897 .breakpoint_store()
22898 .as_ref()
22899 .unwrap()
22900 .read(cx)
22901 .all_source_breakpoints(cx)
22902 });
22903
22904 assert_eq!(1, breakpoints.len());
22905 assert_breakpoint(
22906 &breakpoints,
22907 &abs_path,
22908 vec![
22909 (0, Breakpoint::new_standard()),
22910 (3, Breakpoint::new_standard()),
22911 ],
22912 );
22913
22914 editor.update_in(cx, |editor, window, cx| {
22915 editor.move_to_beginning(&MoveToBeginning, window, cx);
22916 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22917 });
22918
22919 let breakpoints = editor.update(cx, |editor, cx| {
22920 editor
22921 .breakpoint_store()
22922 .as_ref()
22923 .unwrap()
22924 .read(cx)
22925 .all_source_breakpoints(cx)
22926 });
22927
22928 assert_eq!(1, breakpoints.len());
22929 assert_breakpoint(
22930 &breakpoints,
22931 &abs_path,
22932 vec![(3, Breakpoint::new_standard())],
22933 );
22934
22935 editor.update_in(cx, |editor, window, cx| {
22936 editor.move_to_end(&MoveToEnd, window, cx);
22937 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22938 });
22939
22940 let breakpoints = editor.update(cx, |editor, cx| {
22941 editor
22942 .breakpoint_store()
22943 .as_ref()
22944 .unwrap()
22945 .read(cx)
22946 .all_source_breakpoints(cx)
22947 });
22948
22949 assert_eq!(0, breakpoints.len());
22950 assert_breakpoint(&breakpoints, &abs_path, vec![]);
22951}
22952
22953#[gpui::test]
22954async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
22955 init_test(cx, |_| {});
22956
22957 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22958
22959 let fs = FakeFs::new(cx.executor());
22960 fs.insert_tree(
22961 path!("/a"),
22962 json!({
22963 "main.rs": sample_text,
22964 }),
22965 )
22966 .await;
22967 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22968 let (workspace, cx) =
22969 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22970
22971 let worktree_id = workspace.update(cx, |workspace, cx| {
22972 workspace.project().update(cx, |project, cx| {
22973 project.worktrees(cx).next().unwrap().read(cx).id()
22974 })
22975 });
22976
22977 let buffer = project
22978 .update(cx, |project, cx| {
22979 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22980 })
22981 .await
22982 .unwrap();
22983
22984 let (editor, cx) = cx.add_window_view(|window, cx| {
22985 Editor::new(
22986 EditorMode::full(),
22987 MultiBuffer::build_from_buffer(buffer, cx),
22988 Some(project.clone()),
22989 window,
22990 cx,
22991 )
22992 });
22993
22994 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22995 let abs_path = project.read_with(cx, |project, cx| {
22996 project
22997 .absolute_path(&project_path, cx)
22998 .map(Arc::from)
22999 .unwrap()
23000 });
23001
23002 editor.update_in(cx, |editor, window, cx| {
23003 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
23004 });
23005
23006 let breakpoints = editor.update(cx, |editor, cx| {
23007 editor
23008 .breakpoint_store()
23009 .as_ref()
23010 .unwrap()
23011 .read(cx)
23012 .all_source_breakpoints(cx)
23013 });
23014
23015 assert_breakpoint(
23016 &breakpoints,
23017 &abs_path,
23018 vec![(0, Breakpoint::new_log("hello world"))],
23019 );
23020
23021 // Removing a log message from a log breakpoint should remove it
23022 editor.update_in(cx, |editor, window, cx| {
23023 add_log_breakpoint_at_cursor(editor, "", window, cx);
23024 });
23025
23026 let breakpoints = editor.update(cx, |editor, cx| {
23027 editor
23028 .breakpoint_store()
23029 .as_ref()
23030 .unwrap()
23031 .read(cx)
23032 .all_source_breakpoints(cx)
23033 });
23034
23035 assert_breakpoint(&breakpoints, &abs_path, vec![]);
23036
23037 editor.update_in(cx, |editor, window, cx| {
23038 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23039 editor.move_to_end(&MoveToEnd, window, cx);
23040 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23041 // Not adding a log message to a standard breakpoint shouldn't remove it
23042 add_log_breakpoint_at_cursor(editor, "", window, cx);
23043 });
23044
23045 let breakpoints = editor.update(cx, |editor, cx| {
23046 editor
23047 .breakpoint_store()
23048 .as_ref()
23049 .unwrap()
23050 .read(cx)
23051 .all_source_breakpoints(cx)
23052 });
23053
23054 assert_breakpoint(
23055 &breakpoints,
23056 &abs_path,
23057 vec![
23058 (0, Breakpoint::new_standard()),
23059 (3, Breakpoint::new_standard()),
23060 ],
23061 );
23062
23063 editor.update_in(cx, |editor, window, cx| {
23064 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
23065 });
23066
23067 let breakpoints = editor.update(cx, |editor, cx| {
23068 editor
23069 .breakpoint_store()
23070 .as_ref()
23071 .unwrap()
23072 .read(cx)
23073 .all_source_breakpoints(cx)
23074 });
23075
23076 assert_breakpoint(
23077 &breakpoints,
23078 &abs_path,
23079 vec![
23080 (0, Breakpoint::new_standard()),
23081 (3, Breakpoint::new_log("hello world")),
23082 ],
23083 );
23084
23085 editor.update_in(cx, |editor, window, cx| {
23086 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
23087 });
23088
23089 let breakpoints = editor.update(cx, |editor, cx| {
23090 editor
23091 .breakpoint_store()
23092 .as_ref()
23093 .unwrap()
23094 .read(cx)
23095 .all_source_breakpoints(cx)
23096 });
23097
23098 assert_breakpoint(
23099 &breakpoints,
23100 &abs_path,
23101 vec![
23102 (0, Breakpoint::new_standard()),
23103 (3, Breakpoint::new_log("hello Earth!!")),
23104 ],
23105 );
23106}
23107
23108/// This also tests that Editor::breakpoint_at_cursor_head is working properly
23109/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
23110/// or when breakpoints were placed out of order. This tests for a regression too
23111#[gpui::test]
23112async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
23113 init_test(cx, |_| {});
23114
23115 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
23116 let fs = FakeFs::new(cx.executor());
23117 fs.insert_tree(
23118 path!("/a"),
23119 json!({
23120 "main.rs": sample_text,
23121 }),
23122 )
23123 .await;
23124 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23125 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23126 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23127
23128 let fs = FakeFs::new(cx.executor());
23129 fs.insert_tree(
23130 path!("/a"),
23131 json!({
23132 "main.rs": sample_text,
23133 }),
23134 )
23135 .await;
23136 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23137 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23138 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23139 let worktree_id = workspace
23140 .update(cx, |workspace, _window, cx| {
23141 workspace.project().update(cx, |project, cx| {
23142 project.worktrees(cx).next().unwrap().read(cx).id()
23143 })
23144 })
23145 .unwrap();
23146
23147 let buffer = project
23148 .update(cx, |project, cx| {
23149 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23150 })
23151 .await
23152 .unwrap();
23153
23154 let (editor, cx) = cx.add_window_view(|window, cx| {
23155 Editor::new(
23156 EditorMode::full(),
23157 MultiBuffer::build_from_buffer(buffer, cx),
23158 Some(project.clone()),
23159 window,
23160 cx,
23161 )
23162 });
23163
23164 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
23165 let abs_path = project.read_with(cx, |project, cx| {
23166 project
23167 .absolute_path(&project_path, cx)
23168 .map(Arc::from)
23169 .unwrap()
23170 });
23171
23172 // assert we can add breakpoint on the first line
23173 editor.update_in(cx, |editor, window, cx| {
23174 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23175 editor.move_to_end(&MoveToEnd, window, cx);
23176 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23177 editor.move_up(&MoveUp, window, cx);
23178 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23179 });
23180
23181 let breakpoints = editor.update(cx, |editor, cx| {
23182 editor
23183 .breakpoint_store()
23184 .as_ref()
23185 .unwrap()
23186 .read(cx)
23187 .all_source_breakpoints(cx)
23188 });
23189
23190 assert_eq!(1, breakpoints.len());
23191 assert_breakpoint(
23192 &breakpoints,
23193 &abs_path,
23194 vec![
23195 (0, Breakpoint::new_standard()),
23196 (2, Breakpoint::new_standard()),
23197 (3, Breakpoint::new_standard()),
23198 ],
23199 );
23200
23201 editor.update_in(cx, |editor, window, cx| {
23202 editor.move_to_beginning(&MoveToBeginning, window, cx);
23203 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23204 editor.move_to_end(&MoveToEnd, window, cx);
23205 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23206 // Disabling a breakpoint that doesn't exist should do nothing
23207 editor.move_up(&MoveUp, window, cx);
23208 editor.move_up(&MoveUp, window, cx);
23209 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23210 });
23211
23212 let breakpoints = editor.update(cx, |editor, cx| {
23213 editor
23214 .breakpoint_store()
23215 .as_ref()
23216 .unwrap()
23217 .read(cx)
23218 .all_source_breakpoints(cx)
23219 });
23220
23221 let disable_breakpoint = {
23222 let mut bp = Breakpoint::new_standard();
23223 bp.state = BreakpointState::Disabled;
23224 bp
23225 };
23226
23227 assert_eq!(1, breakpoints.len());
23228 assert_breakpoint(
23229 &breakpoints,
23230 &abs_path,
23231 vec![
23232 (0, disable_breakpoint.clone()),
23233 (2, Breakpoint::new_standard()),
23234 (3, disable_breakpoint.clone()),
23235 ],
23236 );
23237
23238 editor.update_in(cx, |editor, window, cx| {
23239 editor.move_to_beginning(&MoveToBeginning, window, cx);
23240 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
23241 editor.move_to_end(&MoveToEnd, window, cx);
23242 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
23243 editor.move_up(&MoveUp, window, cx);
23244 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23245 });
23246
23247 let breakpoints = editor.update(cx, |editor, cx| {
23248 editor
23249 .breakpoint_store()
23250 .as_ref()
23251 .unwrap()
23252 .read(cx)
23253 .all_source_breakpoints(cx)
23254 });
23255
23256 assert_eq!(1, breakpoints.len());
23257 assert_breakpoint(
23258 &breakpoints,
23259 &abs_path,
23260 vec![
23261 (0, Breakpoint::new_standard()),
23262 (2, disable_breakpoint),
23263 (3, Breakpoint::new_standard()),
23264 ],
23265 );
23266}
23267
23268#[gpui::test]
23269async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
23270 init_test(cx, |_| {});
23271 let capabilities = lsp::ServerCapabilities {
23272 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
23273 prepare_provider: Some(true),
23274 work_done_progress_options: Default::default(),
23275 })),
23276 ..Default::default()
23277 };
23278 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23279
23280 cx.set_state(indoc! {"
23281 struct Fˇoo {}
23282 "});
23283
23284 cx.update_editor(|editor, _, cx| {
23285 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23286 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23287 editor.highlight_background::<DocumentHighlightRead>(
23288 &[highlight_range],
23289 |theme| theme.colors().editor_document_highlight_read_background,
23290 cx,
23291 );
23292 });
23293
23294 let mut prepare_rename_handler = cx
23295 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
23296 move |_, _, _| async move {
23297 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
23298 start: lsp::Position {
23299 line: 0,
23300 character: 7,
23301 },
23302 end: lsp::Position {
23303 line: 0,
23304 character: 10,
23305 },
23306 })))
23307 },
23308 );
23309 let prepare_rename_task = cx
23310 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23311 .expect("Prepare rename was not started");
23312 prepare_rename_handler.next().await.unwrap();
23313 prepare_rename_task.await.expect("Prepare rename failed");
23314
23315 let mut rename_handler =
23316 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23317 let edit = lsp::TextEdit {
23318 range: lsp::Range {
23319 start: lsp::Position {
23320 line: 0,
23321 character: 7,
23322 },
23323 end: lsp::Position {
23324 line: 0,
23325 character: 10,
23326 },
23327 },
23328 new_text: "FooRenamed".to_string(),
23329 };
23330 Ok(Some(lsp::WorkspaceEdit::new(
23331 // Specify the same edit twice
23332 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
23333 )))
23334 });
23335 let rename_task = cx
23336 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23337 .expect("Confirm rename was not started");
23338 rename_handler.next().await.unwrap();
23339 rename_task.await.expect("Confirm rename failed");
23340 cx.run_until_parked();
23341
23342 // Despite two edits, only one is actually applied as those are identical
23343 cx.assert_editor_state(indoc! {"
23344 struct FooRenamedˇ {}
23345 "});
23346}
23347
23348#[gpui::test]
23349async fn test_rename_without_prepare(cx: &mut TestAppContext) {
23350 init_test(cx, |_| {});
23351 // These capabilities indicate that the server does not support prepare rename.
23352 let capabilities = lsp::ServerCapabilities {
23353 rename_provider: Some(lsp::OneOf::Left(true)),
23354 ..Default::default()
23355 };
23356 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23357
23358 cx.set_state(indoc! {"
23359 struct Fˇoo {}
23360 "});
23361
23362 cx.update_editor(|editor, _window, cx| {
23363 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23364 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23365 editor.highlight_background::<DocumentHighlightRead>(
23366 &[highlight_range],
23367 |theme| theme.colors().editor_document_highlight_read_background,
23368 cx,
23369 );
23370 });
23371
23372 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23373 .expect("Prepare rename was not started")
23374 .await
23375 .expect("Prepare rename failed");
23376
23377 let mut rename_handler =
23378 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23379 let edit = lsp::TextEdit {
23380 range: lsp::Range {
23381 start: lsp::Position {
23382 line: 0,
23383 character: 7,
23384 },
23385 end: lsp::Position {
23386 line: 0,
23387 character: 10,
23388 },
23389 },
23390 new_text: "FooRenamed".to_string(),
23391 };
23392 Ok(Some(lsp::WorkspaceEdit::new(
23393 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
23394 )))
23395 });
23396 let rename_task = cx
23397 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23398 .expect("Confirm rename was not started");
23399 rename_handler.next().await.unwrap();
23400 rename_task.await.expect("Confirm rename failed");
23401 cx.run_until_parked();
23402
23403 // Correct range is renamed, as `surrounding_word` is used to find it.
23404 cx.assert_editor_state(indoc! {"
23405 struct FooRenamedˇ {}
23406 "});
23407}
23408
23409#[gpui::test]
23410async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
23411 init_test(cx, |_| {});
23412 let mut cx = EditorTestContext::new(cx).await;
23413
23414 let language = Arc::new(
23415 Language::new(
23416 LanguageConfig::default(),
23417 Some(tree_sitter_html::LANGUAGE.into()),
23418 )
23419 .with_brackets_query(
23420 r#"
23421 ("<" @open "/>" @close)
23422 ("</" @open ">" @close)
23423 ("<" @open ">" @close)
23424 ("\"" @open "\"" @close)
23425 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
23426 "#,
23427 )
23428 .unwrap(),
23429 );
23430 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23431
23432 cx.set_state(indoc! {"
23433 <span>ˇ</span>
23434 "});
23435 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23436 cx.assert_editor_state(indoc! {"
23437 <span>
23438 ˇ
23439 </span>
23440 "});
23441
23442 cx.set_state(indoc! {"
23443 <span><span></span>ˇ</span>
23444 "});
23445 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23446 cx.assert_editor_state(indoc! {"
23447 <span><span></span>
23448 ˇ</span>
23449 "});
23450
23451 cx.set_state(indoc! {"
23452 <span>ˇ
23453 </span>
23454 "});
23455 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23456 cx.assert_editor_state(indoc! {"
23457 <span>
23458 ˇ
23459 </span>
23460 "});
23461}
23462
23463#[gpui::test(iterations = 10)]
23464async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
23465 init_test(cx, |_| {});
23466
23467 let fs = FakeFs::new(cx.executor());
23468 fs.insert_tree(
23469 path!("/dir"),
23470 json!({
23471 "a.ts": "a",
23472 }),
23473 )
23474 .await;
23475
23476 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
23477 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23478 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23479
23480 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23481 language_registry.add(Arc::new(Language::new(
23482 LanguageConfig {
23483 name: "TypeScript".into(),
23484 matcher: LanguageMatcher {
23485 path_suffixes: vec!["ts".to_string()],
23486 ..Default::default()
23487 },
23488 ..Default::default()
23489 },
23490 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
23491 )));
23492 let mut fake_language_servers = language_registry.register_fake_lsp(
23493 "TypeScript",
23494 FakeLspAdapter {
23495 capabilities: lsp::ServerCapabilities {
23496 code_lens_provider: Some(lsp::CodeLensOptions {
23497 resolve_provider: Some(true),
23498 }),
23499 execute_command_provider: Some(lsp::ExecuteCommandOptions {
23500 commands: vec!["_the/command".to_string()],
23501 ..lsp::ExecuteCommandOptions::default()
23502 }),
23503 ..lsp::ServerCapabilities::default()
23504 },
23505 ..FakeLspAdapter::default()
23506 },
23507 );
23508
23509 let editor = workspace
23510 .update(cx, |workspace, window, cx| {
23511 workspace.open_abs_path(
23512 PathBuf::from(path!("/dir/a.ts")),
23513 OpenOptions::default(),
23514 window,
23515 cx,
23516 )
23517 })
23518 .unwrap()
23519 .await
23520 .unwrap()
23521 .downcast::<Editor>()
23522 .unwrap();
23523 cx.executor().run_until_parked();
23524
23525 let fake_server = fake_language_servers.next().await.unwrap();
23526
23527 let buffer = editor.update(cx, |editor, cx| {
23528 editor
23529 .buffer()
23530 .read(cx)
23531 .as_singleton()
23532 .expect("have opened a single file by path")
23533 });
23534
23535 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
23536 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
23537 drop(buffer_snapshot);
23538 let actions = cx
23539 .update_window(*workspace, |_, window, cx| {
23540 project.code_actions(&buffer, anchor..anchor, window, cx)
23541 })
23542 .unwrap();
23543
23544 fake_server
23545 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23546 Ok(Some(vec![
23547 lsp::CodeLens {
23548 range: lsp::Range::default(),
23549 command: Some(lsp::Command {
23550 title: "Code lens command".to_owned(),
23551 command: "_the/command".to_owned(),
23552 arguments: None,
23553 }),
23554 data: None,
23555 },
23556 lsp::CodeLens {
23557 range: lsp::Range::default(),
23558 command: Some(lsp::Command {
23559 title: "Command not in capabilities".to_owned(),
23560 command: "not in capabilities".to_owned(),
23561 arguments: None,
23562 }),
23563 data: None,
23564 },
23565 lsp::CodeLens {
23566 range: lsp::Range {
23567 start: lsp::Position {
23568 line: 1,
23569 character: 1,
23570 },
23571 end: lsp::Position {
23572 line: 1,
23573 character: 1,
23574 },
23575 },
23576 command: Some(lsp::Command {
23577 title: "Command not in range".to_owned(),
23578 command: "_the/command".to_owned(),
23579 arguments: None,
23580 }),
23581 data: None,
23582 },
23583 ]))
23584 })
23585 .next()
23586 .await;
23587
23588 let actions = actions.await.unwrap();
23589 assert_eq!(
23590 actions.len(),
23591 1,
23592 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
23593 );
23594 let action = actions[0].clone();
23595 let apply = project.update(cx, |project, cx| {
23596 project.apply_code_action(buffer.clone(), action, true, cx)
23597 });
23598
23599 // Resolving the code action does not populate its edits. In absence of
23600 // edits, we must execute the given command.
23601 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
23602 |mut lens, _| async move {
23603 let lens_command = lens.command.as_mut().expect("should have a command");
23604 assert_eq!(lens_command.title, "Code lens command");
23605 lens_command.arguments = Some(vec![json!("the-argument")]);
23606 Ok(lens)
23607 },
23608 );
23609
23610 // While executing the command, the language server sends the editor
23611 // a `workspaceEdit` request.
23612 fake_server
23613 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
23614 let fake = fake_server.clone();
23615 move |params, _| {
23616 assert_eq!(params.command, "_the/command");
23617 let fake = fake.clone();
23618 async move {
23619 fake.server
23620 .request::<lsp::request::ApplyWorkspaceEdit>(
23621 lsp::ApplyWorkspaceEditParams {
23622 label: None,
23623 edit: lsp::WorkspaceEdit {
23624 changes: Some(
23625 [(
23626 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
23627 vec![lsp::TextEdit {
23628 range: lsp::Range::new(
23629 lsp::Position::new(0, 0),
23630 lsp::Position::new(0, 0),
23631 ),
23632 new_text: "X".into(),
23633 }],
23634 )]
23635 .into_iter()
23636 .collect(),
23637 ),
23638 ..lsp::WorkspaceEdit::default()
23639 },
23640 },
23641 )
23642 .await
23643 .into_response()
23644 .unwrap();
23645 Ok(Some(json!(null)))
23646 }
23647 }
23648 })
23649 .next()
23650 .await;
23651
23652 // Applying the code lens command returns a project transaction containing the edits
23653 // sent by the language server in its `workspaceEdit` request.
23654 let transaction = apply.await.unwrap();
23655 assert!(transaction.0.contains_key(&buffer));
23656 buffer.update(cx, |buffer, cx| {
23657 assert_eq!(buffer.text(), "Xa");
23658 buffer.undo(cx);
23659 assert_eq!(buffer.text(), "a");
23660 });
23661
23662 let actions_after_edits = cx
23663 .update_window(*workspace, |_, window, cx| {
23664 project.code_actions(&buffer, anchor..anchor, window, cx)
23665 })
23666 .unwrap()
23667 .await
23668 .unwrap();
23669 assert_eq!(
23670 actions, actions_after_edits,
23671 "For the same selection, same code lens actions should be returned"
23672 );
23673
23674 let _responses =
23675 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23676 panic!("No more code lens requests are expected");
23677 });
23678 editor.update_in(cx, |editor, window, cx| {
23679 editor.select_all(&SelectAll, window, cx);
23680 });
23681 cx.executor().run_until_parked();
23682 let new_actions = cx
23683 .update_window(*workspace, |_, window, cx| {
23684 project.code_actions(&buffer, anchor..anchor, window, cx)
23685 })
23686 .unwrap()
23687 .await
23688 .unwrap();
23689 assert_eq!(
23690 actions, new_actions,
23691 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
23692 );
23693}
23694
23695#[gpui::test]
23696async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
23697 init_test(cx, |_| {});
23698
23699 let fs = FakeFs::new(cx.executor());
23700 let main_text = r#"fn main() {
23701println!("1");
23702println!("2");
23703println!("3");
23704println!("4");
23705println!("5");
23706}"#;
23707 let lib_text = "mod foo {}";
23708 fs.insert_tree(
23709 path!("/a"),
23710 json!({
23711 "lib.rs": lib_text,
23712 "main.rs": main_text,
23713 }),
23714 )
23715 .await;
23716
23717 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23718 let (workspace, cx) =
23719 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23720 let worktree_id = workspace.update(cx, |workspace, cx| {
23721 workspace.project().update(cx, |project, cx| {
23722 project.worktrees(cx).next().unwrap().read(cx).id()
23723 })
23724 });
23725
23726 let expected_ranges = vec![
23727 Point::new(0, 0)..Point::new(0, 0),
23728 Point::new(1, 0)..Point::new(1, 1),
23729 Point::new(2, 0)..Point::new(2, 2),
23730 Point::new(3, 0)..Point::new(3, 3),
23731 ];
23732
23733 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23734 let editor_1 = workspace
23735 .update_in(cx, |workspace, window, cx| {
23736 workspace.open_path(
23737 (worktree_id, rel_path("main.rs")),
23738 Some(pane_1.downgrade()),
23739 true,
23740 window,
23741 cx,
23742 )
23743 })
23744 .unwrap()
23745 .await
23746 .downcast::<Editor>()
23747 .unwrap();
23748 pane_1.update(cx, |pane, cx| {
23749 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23750 open_editor.update(cx, |editor, cx| {
23751 assert_eq!(
23752 editor.display_text(cx),
23753 main_text,
23754 "Original main.rs text on initial open",
23755 );
23756 assert_eq!(
23757 editor
23758 .selections
23759 .all::<Point>(&editor.display_snapshot(cx))
23760 .into_iter()
23761 .map(|s| s.range())
23762 .collect::<Vec<_>>(),
23763 vec![Point::zero()..Point::zero()],
23764 "Default selections on initial open",
23765 );
23766 })
23767 });
23768 editor_1.update_in(cx, |editor, window, cx| {
23769 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23770 s.select_ranges(expected_ranges.clone());
23771 });
23772 });
23773
23774 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
23775 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
23776 });
23777 let editor_2 = workspace
23778 .update_in(cx, |workspace, window, cx| {
23779 workspace.open_path(
23780 (worktree_id, rel_path("main.rs")),
23781 Some(pane_2.downgrade()),
23782 true,
23783 window,
23784 cx,
23785 )
23786 })
23787 .unwrap()
23788 .await
23789 .downcast::<Editor>()
23790 .unwrap();
23791 pane_2.update(cx, |pane, cx| {
23792 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23793 open_editor.update(cx, |editor, cx| {
23794 assert_eq!(
23795 editor.display_text(cx),
23796 main_text,
23797 "Original main.rs text on initial open in another panel",
23798 );
23799 assert_eq!(
23800 editor
23801 .selections
23802 .all::<Point>(&editor.display_snapshot(cx))
23803 .into_iter()
23804 .map(|s| s.range())
23805 .collect::<Vec<_>>(),
23806 vec![Point::zero()..Point::zero()],
23807 "Default selections on initial open in another panel",
23808 );
23809 })
23810 });
23811
23812 editor_2.update_in(cx, |editor, window, cx| {
23813 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
23814 });
23815
23816 let _other_editor_1 = workspace
23817 .update_in(cx, |workspace, window, cx| {
23818 workspace.open_path(
23819 (worktree_id, rel_path("lib.rs")),
23820 Some(pane_1.downgrade()),
23821 true,
23822 window,
23823 cx,
23824 )
23825 })
23826 .unwrap()
23827 .await
23828 .downcast::<Editor>()
23829 .unwrap();
23830 pane_1
23831 .update_in(cx, |pane, window, cx| {
23832 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23833 })
23834 .await
23835 .unwrap();
23836 drop(editor_1);
23837 pane_1.update(cx, |pane, cx| {
23838 pane.active_item()
23839 .unwrap()
23840 .downcast::<Editor>()
23841 .unwrap()
23842 .update(cx, |editor, cx| {
23843 assert_eq!(
23844 editor.display_text(cx),
23845 lib_text,
23846 "Other file should be open and active",
23847 );
23848 });
23849 assert_eq!(pane.items().count(), 1, "No other editors should be open");
23850 });
23851
23852 let _other_editor_2 = workspace
23853 .update_in(cx, |workspace, window, cx| {
23854 workspace.open_path(
23855 (worktree_id, rel_path("lib.rs")),
23856 Some(pane_2.downgrade()),
23857 true,
23858 window,
23859 cx,
23860 )
23861 })
23862 .unwrap()
23863 .await
23864 .downcast::<Editor>()
23865 .unwrap();
23866 pane_2
23867 .update_in(cx, |pane, window, cx| {
23868 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23869 })
23870 .await
23871 .unwrap();
23872 drop(editor_2);
23873 pane_2.update(cx, |pane, cx| {
23874 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23875 open_editor.update(cx, |editor, cx| {
23876 assert_eq!(
23877 editor.display_text(cx),
23878 lib_text,
23879 "Other file should be open and active in another panel too",
23880 );
23881 });
23882 assert_eq!(
23883 pane.items().count(),
23884 1,
23885 "No other editors should be open in another pane",
23886 );
23887 });
23888
23889 let _editor_1_reopened = workspace
23890 .update_in(cx, |workspace, window, cx| {
23891 workspace.open_path(
23892 (worktree_id, rel_path("main.rs")),
23893 Some(pane_1.downgrade()),
23894 true,
23895 window,
23896 cx,
23897 )
23898 })
23899 .unwrap()
23900 .await
23901 .downcast::<Editor>()
23902 .unwrap();
23903 let _editor_2_reopened = workspace
23904 .update_in(cx, |workspace, window, cx| {
23905 workspace.open_path(
23906 (worktree_id, rel_path("main.rs")),
23907 Some(pane_2.downgrade()),
23908 true,
23909 window,
23910 cx,
23911 )
23912 })
23913 .unwrap()
23914 .await
23915 .downcast::<Editor>()
23916 .unwrap();
23917 pane_1.update(cx, |pane, cx| {
23918 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23919 open_editor.update(cx, |editor, cx| {
23920 assert_eq!(
23921 editor.display_text(cx),
23922 main_text,
23923 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
23924 );
23925 assert_eq!(
23926 editor
23927 .selections
23928 .all::<Point>(&editor.display_snapshot(cx))
23929 .into_iter()
23930 .map(|s| s.range())
23931 .collect::<Vec<_>>(),
23932 expected_ranges,
23933 "Previous editor in the 1st panel had selections and should get them restored on reopen",
23934 );
23935 })
23936 });
23937 pane_2.update(cx, |pane, cx| {
23938 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23939 open_editor.update(cx, |editor, cx| {
23940 assert_eq!(
23941 editor.display_text(cx),
23942 r#"fn main() {
23943⋯rintln!("1");
23944⋯intln!("2");
23945⋯ntln!("3");
23946println!("4");
23947println!("5");
23948}"#,
23949 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
23950 );
23951 assert_eq!(
23952 editor
23953 .selections
23954 .all::<Point>(&editor.display_snapshot(cx))
23955 .into_iter()
23956 .map(|s| s.range())
23957 .collect::<Vec<_>>(),
23958 vec![Point::zero()..Point::zero()],
23959 "Previous editor in the 2nd pane had no selections changed hence should restore none",
23960 );
23961 })
23962 });
23963}
23964
23965#[gpui::test]
23966async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
23967 init_test(cx, |_| {});
23968
23969 let fs = FakeFs::new(cx.executor());
23970 let main_text = r#"fn main() {
23971println!("1");
23972println!("2");
23973println!("3");
23974println!("4");
23975println!("5");
23976}"#;
23977 let lib_text = "mod foo {}";
23978 fs.insert_tree(
23979 path!("/a"),
23980 json!({
23981 "lib.rs": lib_text,
23982 "main.rs": main_text,
23983 }),
23984 )
23985 .await;
23986
23987 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23988 let (workspace, cx) =
23989 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23990 let worktree_id = workspace.update(cx, |workspace, cx| {
23991 workspace.project().update(cx, |project, cx| {
23992 project.worktrees(cx).next().unwrap().read(cx).id()
23993 })
23994 });
23995
23996 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23997 let editor = workspace
23998 .update_in(cx, |workspace, window, cx| {
23999 workspace.open_path(
24000 (worktree_id, rel_path("main.rs")),
24001 Some(pane.downgrade()),
24002 true,
24003 window,
24004 cx,
24005 )
24006 })
24007 .unwrap()
24008 .await
24009 .downcast::<Editor>()
24010 .unwrap();
24011 pane.update(cx, |pane, cx| {
24012 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24013 open_editor.update(cx, |editor, cx| {
24014 assert_eq!(
24015 editor.display_text(cx),
24016 main_text,
24017 "Original main.rs text on initial open",
24018 );
24019 })
24020 });
24021 editor.update_in(cx, |editor, window, cx| {
24022 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
24023 });
24024
24025 cx.update_global(|store: &mut SettingsStore, cx| {
24026 store.update_user_settings(cx, |s| {
24027 s.workspace.restore_on_file_reopen = Some(false);
24028 });
24029 });
24030 editor.update_in(cx, |editor, window, cx| {
24031 editor.fold_ranges(
24032 vec![
24033 Point::new(1, 0)..Point::new(1, 1),
24034 Point::new(2, 0)..Point::new(2, 2),
24035 Point::new(3, 0)..Point::new(3, 3),
24036 ],
24037 false,
24038 window,
24039 cx,
24040 );
24041 });
24042 pane.update_in(cx, |pane, window, cx| {
24043 pane.close_all_items(&CloseAllItems::default(), window, cx)
24044 })
24045 .await
24046 .unwrap();
24047 pane.update(cx, |pane, _| {
24048 assert!(pane.active_item().is_none());
24049 });
24050 cx.update_global(|store: &mut SettingsStore, cx| {
24051 store.update_user_settings(cx, |s| {
24052 s.workspace.restore_on_file_reopen = Some(true);
24053 });
24054 });
24055
24056 let _editor_reopened = workspace
24057 .update_in(cx, |workspace, window, cx| {
24058 workspace.open_path(
24059 (worktree_id, rel_path("main.rs")),
24060 Some(pane.downgrade()),
24061 true,
24062 window,
24063 cx,
24064 )
24065 })
24066 .unwrap()
24067 .await
24068 .downcast::<Editor>()
24069 .unwrap();
24070 pane.update(cx, |pane, cx| {
24071 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24072 open_editor.update(cx, |editor, cx| {
24073 assert_eq!(
24074 editor.display_text(cx),
24075 main_text,
24076 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
24077 );
24078 })
24079 });
24080}
24081
24082#[gpui::test]
24083async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
24084 struct EmptyModalView {
24085 focus_handle: gpui::FocusHandle,
24086 }
24087 impl EventEmitter<DismissEvent> for EmptyModalView {}
24088 impl Render for EmptyModalView {
24089 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
24090 div()
24091 }
24092 }
24093 impl Focusable for EmptyModalView {
24094 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
24095 self.focus_handle.clone()
24096 }
24097 }
24098 impl workspace::ModalView for EmptyModalView {}
24099 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
24100 EmptyModalView {
24101 focus_handle: cx.focus_handle(),
24102 }
24103 }
24104
24105 init_test(cx, |_| {});
24106
24107 let fs = FakeFs::new(cx.executor());
24108 let project = Project::test(fs, [], cx).await;
24109 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24110 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
24111 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24112 let editor = cx.new_window_entity(|window, cx| {
24113 Editor::new(
24114 EditorMode::full(),
24115 buffer,
24116 Some(project.clone()),
24117 window,
24118 cx,
24119 )
24120 });
24121 workspace
24122 .update(cx, |workspace, window, cx| {
24123 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
24124 })
24125 .unwrap();
24126 editor.update_in(cx, |editor, window, cx| {
24127 editor.open_context_menu(&OpenContextMenu, window, cx);
24128 assert!(editor.mouse_context_menu.is_some());
24129 });
24130 workspace
24131 .update(cx, |workspace, window, cx| {
24132 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
24133 })
24134 .unwrap();
24135 cx.read(|cx| {
24136 assert!(editor.read(cx).mouse_context_menu.is_none());
24137 });
24138}
24139
24140fn set_linked_edit_ranges(
24141 opening: (Point, Point),
24142 closing: (Point, Point),
24143 editor: &mut Editor,
24144 cx: &mut Context<Editor>,
24145) {
24146 let Some((buffer, _)) = editor
24147 .buffer
24148 .read(cx)
24149 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
24150 else {
24151 panic!("Failed to get buffer for selection position");
24152 };
24153 let buffer = buffer.read(cx);
24154 let buffer_id = buffer.remote_id();
24155 let opening_range = buffer.anchor_before(opening.0)..buffer.anchor_after(opening.1);
24156 let closing_range = buffer.anchor_before(closing.0)..buffer.anchor_after(closing.1);
24157 let mut linked_ranges = HashMap::default();
24158 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
24159 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
24160}
24161
24162#[gpui::test]
24163async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
24164 init_test(cx, |_| {});
24165
24166 let fs = FakeFs::new(cx.executor());
24167 fs.insert_file(path!("/file.html"), Default::default())
24168 .await;
24169
24170 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
24171
24172 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24173 let html_language = Arc::new(Language::new(
24174 LanguageConfig {
24175 name: "HTML".into(),
24176 matcher: LanguageMatcher {
24177 path_suffixes: vec!["html".to_string()],
24178 ..LanguageMatcher::default()
24179 },
24180 brackets: BracketPairConfig {
24181 pairs: vec![BracketPair {
24182 start: "<".into(),
24183 end: ">".into(),
24184 close: true,
24185 ..Default::default()
24186 }],
24187 ..Default::default()
24188 },
24189 ..Default::default()
24190 },
24191 Some(tree_sitter_html::LANGUAGE.into()),
24192 ));
24193 language_registry.add(html_language);
24194 let mut fake_servers = language_registry.register_fake_lsp(
24195 "HTML",
24196 FakeLspAdapter {
24197 capabilities: lsp::ServerCapabilities {
24198 completion_provider: Some(lsp::CompletionOptions {
24199 resolve_provider: Some(true),
24200 ..Default::default()
24201 }),
24202 ..Default::default()
24203 },
24204 ..Default::default()
24205 },
24206 );
24207
24208 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24209 let cx = &mut VisualTestContext::from_window(*workspace, cx);
24210
24211 let worktree_id = workspace
24212 .update(cx, |workspace, _window, cx| {
24213 workspace.project().update(cx, |project, cx| {
24214 project.worktrees(cx).next().unwrap().read(cx).id()
24215 })
24216 })
24217 .unwrap();
24218 project
24219 .update(cx, |project, cx| {
24220 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
24221 })
24222 .await
24223 .unwrap();
24224 let editor = workspace
24225 .update(cx, |workspace, window, cx| {
24226 workspace.open_path((worktree_id, rel_path("file.html")), None, true, window, cx)
24227 })
24228 .unwrap()
24229 .await
24230 .unwrap()
24231 .downcast::<Editor>()
24232 .unwrap();
24233
24234 let fake_server = fake_servers.next().await.unwrap();
24235 editor.update_in(cx, |editor, window, cx| {
24236 editor.set_text("<ad></ad>", window, cx);
24237 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
24238 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
24239 });
24240 set_linked_edit_ranges(
24241 (Point::new(0, 1), Point::new(0, 3)),
24242 (Point::new(0, 6), Point::new(0, 8)),
24243 editor,
24244 cx,
24245 );
24246 });
24247 let mut completion_handle =
24248 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
24249 Ok(Some(lsp::CompletionResponse::Array(vec![
24250 lsp::CompletionItem {
24251 label: "head".to_string(),
24252 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
24253 lsp::InsertReplaceEdit {
24254 new_text: "head".to_string(),
24255 insert: lsp::Range::new(
24256 lsp::Position::new(0, 1),
24257 lsp::Position::new(0, 3),
24258 ),
24259 replace: lsp::Range::new(
24260 lsp::Position::new(0, 1),
24261 lsp::Position::new(0, 3),
24262 ),
24263 },
24264 )),
24265 ..Default::default()
24266 },
24267 ])))
24268 });
24269 editor.update_in(cx, |editor, window, cx| {
24270 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
24271 });
24272 cx.run_until_parked();
24273 completion_handle.next().await.unwrap();
24274 editor.update(cx, |editor, _| {
24275 assert!(
24276 editor.context_menu_visible(),
24277 "Completion menu should be visible"
24278 );
24279 });
24280 editor.update_in(cx, |editor, window, cx| {
24281 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
24282 });
24283 cx.executor().run_until_parked();
24284 editor.update(cx, |editor, cx| {
24285 assert_eq!(editor.text(cx), "<head></head>");
24286 });
24287}
24288
24289#[gpui::test]
24290async fn test_linked_edits_on_typing_punctuation(cx: &mut TestAppContext) {
24291 init_test(cx, |_| {});
24292
24293 let mut cx = EditorTestContext::new(cx).await;
24294 let language = Arc::new(Language::new(
24295 LanguageConfig {
24296 name: "TSX".into(),
24297 matcher: LanguageMatcher {
24298 path_suffixes: vec!["tsx".to_string()],
24299 ..LanguageMatcher::default()
24300 },
24301 brackets: BracketPairConfig {
24302 pairs: vec![BracketPair {
24303 start: "<".into(),
24304 end: ">".into(),
24305 close: true,
24306 ..Default::default()
24307 }],
24308 ..Default::default()
24309 },
24310 linked_edit_characters: HashSet::from_iter(['.']),
24311 ..Default::default()
24312 },
24313 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
24314 ));
24315 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24316
24317 // Test typing > does not extend linked pair
24318 cx.set_state("<divˇ<div></div>");
24319 cx.update_editor(|editor, _, cx| {
24320 set_linked_edit_ranges(
24321 (Point::new(0, 1), Point::new(0, 4)),
24322 (Point::new(0, 11), Point::new(0, 14)),
24323 editor,
24324 cx,
24325 );
24326 });
24327 cx.update_editor(|editor, window, cx| {
24328 editor.handle_input(">", window, cx);
24329 });
24330 cx.assert_editor_state("<div>ˇ<div></div>");
24331
24332 // Test typing . do extend linked pair
24333 cx.set_state("<Animatedˇ></Animated>");
24334 cx.update_editor(|editor, _, cx| {
24335 set_linked_edit_ranges(
24336 (Point::new(0, 1), Point::new(0, 9)),
24337 (Point::new(0, 12), Point::new(0, 20)),
24338 editor,
24339 cx,
24340 );
24341 });
24342 cx.update_editor(|editor, window, cx| {
24343 editor.handle_input(".", window, cx);
24344 });
24345 cx.assert_editor_state("<Animated.ˇ></Animated.>");
24346 cx.update_editor(|editor, _, cx| {
24347 set_linked_edit_ranges(
24348 (Point::new(0, 1), Point::new(0, 10)),
24349 (Point::new(0, 13), Point::new(0, 21)),
24350 editor,
24351 cx,
24352 );
24353 });
24354 cx.update_editor(|editor, window, cx| {
24355 editor.handle_input("V", window, cx);
24356 });
24357 cx.assert_editor_state("<Animated.Vˇ></Animated.V>");
24358}
24359
24360#[gpui::test]
24361async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
24362 init_test(cx, |_| {});
24363
24364 let fs = FakeFs::new(cx.executor());
24365 fs.insert_tree(
24366 path!("/root"),
24367 json!({
24368 "a": {
24369 "main.rs": "fn main() {}",
24370 },
24371 "foo": {
24372 "bar": {
24373 "external_file.rs": "pub mod external {}",
24374 }
24375 }
24376 }),
24377 )
24378 .await;
24379
24380 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
24381 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24382 language_registry.add(rust_lang());
24383 let _fake_servers = language_registry.register_fake_lsp(
24384 "Rust",
24385 FakeLspAdapter {
24386 ..FakeLspAdapter::default()
24387 },
24388 );
24389 let (workspace, cx) =
24390 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24391 let worktree_id = workspace.update(cx, |workspace, cx| {
24392 workspace.project().update(cx, |project, cx| {
24393 project.worktrees(cx).next().unwrap().read(cx).id()
24394 })
24395 });
24396
24397 let assert_language_servers_count =
24398 |expected: usize, context: &str, cx: &mut VisualTestContext| {
24399 project.update(cx, |project, cx| {
24400 let current = project
24401 .lsp_store()
24402 .read(cx)
24403 .as_local()
24404 .unwrap()
24405 .language_servers
24406 .len();
24407 assert_eq!(expected, current, "{context}");
24408 });
24409 };
24410
24411 assert_language_servers_count(
24412 0,
24413 "No servers should be running before any file is open",
24414 cx,
24415 );
24416 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24417 let main_editor = workspace
24418 .update_in(cx, |workspace, window, cx| {
24419 workspace.open_path(
24420 (worktree_id, rel_path("main.rs")),
24421 Some(pane.downgrade()),
24422 true,
24423 window,
24424 cx,
24425 )
24426 })
24427 .unwrap()
24428 .await
24429 .downcast::<Editor>()
24430 .unwrap();
24431 pane.update(cx, |pane, cx| {
24432 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24433 open_editor.update(cx, |editor, cx| {
24434 assert_eq!(
24435 editor.display_text(cx),
24436 "fn main() {}",
24437 "Original main.rs text on initial open",
24438 );
24439 });
24440 assert_eq!(open_editor, main_editor);
24441 });
24442 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
24443
24444 let external_editor = workspace
24445 .update_in(cx, |workspace, window, cx| {
24446 workspace.open_abs_path(
24447 PathBuf::from("/root/foo/bar/external_file.rs"),
24448 OpenOptions::default(),
24449 window,
24450 cx,
24451 )
24452 })
24453 .await
24454 .expect("opening external file")
24455 .downcast::<Editor>()
24456 .expect("downcasted external file's open element to editor");
24457 pane.update(cx, |pane, cx| {
24458 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24459 open_editor.update(cx, |editor, cx| {
24460 assert_eq!(
24461 editor.display_text(cx),
24462 "pub mod external {}",
24463 "External file is open now",
24464 );
24465 });
24466 assert_eq!(open_editor, external_editor);
24467 });
24468 assert_language_servers_count(
24469 1,
24470 "Second, external, *.rs file should join the existing server",
24471 cx,
24472 );
24473
24474 pane.update_in(cx, |pane, window, cx| {
24475 pane.close_active_item(&CloseActiveItem::default(), window, cx)
24476 })
24477 .await
24478 .unwrap();
24479 pane.update_in(cx, |pane, window, cx| {
24480 pane.navigate_backward(&Default::default(), window, cx);
24481 });
24482 cx.run_until_parked();
24483 pane.update(cx, |pane, cx| {
24484 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24485 open_editor.update(cx, |editor, cx| {
24486 assert_eq!(
24487 editor.display_text(cx),
24488 "pub mod external {}",
24489 "External file is open now",
24490 );
24491 });
24492 });
24493 assert_language_servers_count(
24494 1,
24495 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
24496 cx,
24497 );
24498
24499 cx.update(|_, cx| {
24500 workspace::reload(cx);
24501 });
24502 assert_language_servers_count(
24503 1,
24504 "After reloading the worktree with local and external files opened, only one project should be started",
24505 cx,
24506 );
24507}
24508
24509#[gpui::test]
24510async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
24511 init_test(cx, |_| {});
24512
24513 let mut cx = EditorTestContext::new(cx).await;
24514 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24515 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24516
24517 // test cursor move to start of each line on tab
24518 // for `if`, `elif`, `else`, `while`, `with` and `for`
24519 cx.set_state(indoc! {"
24520 def main():
24521 ˇ for item in items:
24522 ˇ while item.active:
24523 ˇ if item.value > 10:
24524 ˇ continue
24525 ˇ elif item.value < 0:
24526 ˇ break
24527 ˇ else:
24528 ˇ with item.context() as ctx:
24529 ˇ yield count
24530 ˇ else:
24531 ˇ log('while else')
24532 ˇ else:
24533 ˇ log('for else')
24534 "});
24535 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24536 cx.assert_editor_state(indoc! {"
24537 def main():
24538 ˇfor item in items:
24539 ˇwhile item.active:
24540 ˇif item.value > 10:
24541 ˇcontinue
24542 ˇelif item.value < 0:
24543 ˇbreak
24544 ˇelse:
24545 ˇwith item.context() as ctx:
24546 ˇyield count
24547 ˇelse:
24548 ˇlog('while else')
24549 ˇelse:
24550 ˇlog('for else')
24551 "});
24552 // test relative indent is preserved when tab
24553 // for `if`, `elif`, `else`, `while`, `with` and `for`
24554 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24555 cx.assert_editor_state(indoc! {"
24556 def main():
24557 ˇfor item in items:
24558 ˇwhile item.active:
24559 ˇif item.value > 10:
24560 ˇcontinue
24561 ˇelif item.value < 0:
24562 ˇbreak
24563 ˇelse:
24564 ˇwith item.context() as ctx:
24565 ˇyield count
24566 ˇelse:
24567 ˇlog('while else')
24568 ˇelse:
24569 ˇlog('for else')
24570 "});
24571
24572 // test cursor move to start of each line on tab
24573 // for `try`, `except`, `else`, `finally`, `match` and `def`
24574 cx.set_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 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24588 cx.assert_editor_state(indoc! {"
24589 def main():
24590 ˇtry:
24591 ˇfetch()
24592 ˇexcept ValueError:
24593 ˇhandle_error()
24594 ˇelse:
24595 ˇmatch value:
24596 ˇcase _:
24597 ˇfinally:
24598 ˇdef status():
24599 ˇreturn 0
24600 "});
24601 // test relative indent is preserved when tab
24602 // for `try`, `except`, `else`, `finally`, `match` and `def`
24603 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24604 cx.assert_editor_state(indoc! {"
24605 def main():
24606 ˇtry:
24607 ˇfetch()
24608 ˇexcept ValueError:
24609 ˇhandle_error()
24610 ˇelse:
24611 ˇmatch value:
24612 ˇcase _:
24613 ˇfinally:
24614 ˇdef status():
24615 ˇreturn 0
24616 "});
24617}
24618
24619#[gpui::test]
24620async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
24621 init_test(cx, |_| {});
24622
24623 let mut cx = EditorTestContext::new(cx).await;
24624 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24625 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24626
24627 // test `else` auto outdents when typed inside `if` block
24628 cx.set_state(indoc! {"
24629 def main():
24630 if i == 2:
24631 return
24632 ˇ
24633 "});
24634 cx.update_editor(|editor, window, cx| {
24635 editor.handle_input("else:", window, cx);
24636 });
24637 cx.assert_editor_state(indoc! {"
24638 def main():
24639 if i == 2:
24640 return
24641 else:ˇ
24642 "});
24643
24644 // test `except` auto outdents when typed inside `try` block
24645 cx.set_state(indoc! {"
24646 def main():
24647 try:
24648 i = 2
24649 ˇ
24650 "});
24651 cx.update_editor(|editor, window, cx| {
24652 editor.handle_input("except:", window, cx);
24653 });
24654 cx.assert_editor_state(indoc! {"
24655 def main():
24656 try:
24657 i = 2
24658 except:ˇ
24659 "});
24660
24661 // test `else` auto outdents when typed inside `except` block
24662 cx.set_state(indoc! {"
24663 def main():
24664 try:
24665 i = 2
24666 except:
24667 j = 2
24668 ˇ
24669 "});
24670 cx.update_editor(|editor, window, cx| {
24671 editor.handle_input("else:", window, cx);
24672 });
24673 cx.assert_editor_state(indoc! {"
24674 def main():
24675 try:
24676 i = 2
24677 except:
24678 j = 2
24679 else:ˇ
24680 "});
24681
24682 // test `finally` auto outdents when typed inside `else` block
24683 cx.set_state(indoc! {"
24684 def main():
24685 try:
24686 i = 2
24687 except:
24688 j = 2
24689 else:
24690 k = 2
24691 ˇ
24692 "});
24693 cx.update_editor(|editor, window, cx| {
24694 editor.handle_input("finally:", window, cx);
24695 });
24696 cx.assert_editor_state(indoc! {"
24697 def main():
24698 try:
24699 i = 2
24700 except:
24701 j = 2
24702 else:
24703 k = 2
24704 finally:ˇ
24705 "});
24706
24707 // test `else` does not outdents when typed inside `except` block right after for block
24708 cx.set_state(indoc! {"
24709 def main():
24710 try:
24711 i = 2
24712 except:
24713 for i in range(n):
24714 pass
24715 ˇ
24716 "});
24717 cx.update_editor(|editor, window, cx| {
24718 editor.handle_input("else:", window, cx);
24719 });
24720 cx.assert_editor_state(indoc! {"
24721 def main():
24722 try:
24723 i = 2
24724 except:
24725 for i in range(n):
24726 pass
24727 else:ˇ
24728 "});
24729
24730 // test `finally` auto outdents when typed inside `else` block right after for block
24731 cx.set_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 ˇ
24741 "});
24742 cx.update_editor(|editor, window, cx| {
24743 editor.handle_input("finally:", window, cx);
24744 });
24745 cx.assert_editor_state(indoc! {"
24746 def main():
24747 try:
24748 i = 2
24749 except:
24750 j = 2
24751 else:
24752 for i in range(n):
24753 pass
24754 finally:ˇ
24755 "});
24756
24757 // test `except` outdents to inner "try" block
24758 cx.set_state(indoc! {"
24759 def main():
24760 try:
24761 i = 2
24762 if i == 2:
24763 try:
24764 i = 3
24765 ˇ
24766 "});
24767 cx.update_editor(|editor, window, cx| {
24768 editor.handle_input("except:", window, cx);
24769 });
24770 cx.assert_editor_state(indoc! {"
24771 def main():
24772 try:
24773 i = 2
24774 if i == 2:
24775 try:
24776 i = 3
24777 except:ˇ
24778 "});
24779
24780 // test `except` outdents to outer "try" block
24781 cx.set_state(indoc! {"
24782 def main():
24783 try:
24784 i = 2
24785 if i == 2:
24786 try:
24787 i = 3
24788 ˇ
24789 "});
24790 cx.update_editor(|editor, window, cx| {
24791 editor.handle_input("except:", window, cx);
24792 });
24793 cx.assert_editor_state(indoc! {"
24794 def main():
24795 try:
24796 i = 2
24797 if i == 2:
24798 try:
24799 i = 3
24800 except:ˇ
24801 "});
24802
24803 // test `else` stays at correct indent when typed after `for` block
24804 cx.set_state(indoc! {"
24805 def main():
24806 for i in range(10):
24807 if i == 3:
24808 break
24809 ˇ
24810 "});
24811 cx.update_editor(|editor, window, cx| {
24812 editor.handle_input("else:", window, cx);
24813 });
24814 cx.assert_editor_state(indoc! {"
24815 def main():
24816 for i in range(10):
24817 if i == 3:
24818 break
24819 else:ˇ
24820 "});
24821
24822 // test does not outdent on typing after line with square brackets
24823 cx.set_state(indoc! {"
24824 def f() -> list[str]:
24825 ˇ
24826 "});
24827 cx.update_editor(|editor, window, cx| {
24828 editor.handle_input("a", window, cx);
24829 });
24830 cx.assert_editor_state(indoc! {"
24831 def f() -> list[str]:
24832 aˇ
24833 "});
24834
24835 // test does not outdent on typing : after case keyword
24836 cx.set_state(indoc! {"
24837 match 1:
24838 caseˇ
24839 "});
24840 cx.update_editor(|editor, window, cx| {
24841 editor.handle_input(":", window, cx);
24842 });
24843 cx.assert_editor_state(indoc! {"
24844 match 1:
24845 case:ˇ
24846 "});
24847}
24848
24849#[gpui::test]
24850async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
24851 init_test(cx, |_| {});
24852 update_test_language_settings(cx, |settings| {
24853 settings.defaults.extend_comment_on_newline = Some(false);
24854 });
24855 let mut cx = EditorTestContext::new(cx).await;
24856 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24857 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24858
24859 // test correct indent after newline on comment
24860 cx.set_state(indoc! {"
24861 # COMMENT:ˇ
24862 "});
24863 cx.update_editor(|editor, window, cx| {
24864 editor.newline(&Newline, window, cx);
24865 });
24866 cx.assert_editor_state(indoc! {"
24867 # COMMENT:
24868 ˇ
24869 "});
24870
24871 // test correct indent after newline in brackets
24872 cx.set_state(indoc! {"
24873 {ˇ}
24874 "});
24875 cx.update_editor(|editor, window, cx| {
24876 editor.newline(&Newline, window, cx);
24877 });
24878 cx.run_until_parked();
24879 cx.assert_editor_state(indoc! {"
24880 {
24881 ˇ
24882 }
24883 "});
24884
24885 cx.set_state(indoc! {"
24886 (ˇ)
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 (
24894 ˇ
24895 )
24896 "});
24897
24898 // do not indent after empty lists or dictionaries
24899 cx.set_state(indoc! {"
24900 a = []ˇ
24901 "});
24902 cx.update_editor(|editor, window, cx| {
24903 editor.newline(&Newline, window, cx);
24904 });
24905 cx.run_until_parked();
24906 cx.assert_editor_state(indoc! {"
24907 a = []
24908 ˇ
24909 "});
24910}
24911
24912#[gpui::test]
24913async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
24914 init_test(cx, |_| {});
24915
24916 let mut cx = EditorTestContext::new(cx).await;
24917 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24918 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24919
24920 // test cursor move to start of each line on tab
24921 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
24922 cx.set_state(indoc! {"
24923 function main() {
24924 ˇ for item in $items; do
24925 ˇ while [ -n \"$item\" ]; do
24926 ˇ if [ \"$value\" -gt 10 ]; then
24927 ˇ continue
24928 ˇ elif [ \"$value\" -lt 0 ]; then
24929 ˇ break
24930 ˇ else
24931 ˇ echo \"$item\"
24932 ˇ fi
24933 ˇ done
24934 ˇ done
24935 ˇ}
24936 "});
24937 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24938 cx.assert_editor_state(indoc! {"
24939 function main() {
24940 ˇfor item in $items; do
24941 ˇwhile [ -n \"$item\" ]; do
24942 ˇif [ \"$value\" -gt 10 ]; then
24943 ˇcontinue
24944 ˇelif [ \"$value\" -lt 0 ]; then
24945 ˇbreak
24946 ˇelse
24947 ˇecho \"$item\"
24948 ˇfi
24949 ˇdone
24950 ˇdone
24951 ˇ}
24952 "});
24953 // test relative indent is preserved when tab
24954 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24955 cx.assert_editor_state(indoc! {"
24956 function main() {
24957 ˇfor item in $items; do
24958 ˇwhile [ -n \"$item\" ]; do
24959 ˇif [ \"$value\" -gt 10 ]; then
24960 ˇcontinue
24961 ˇelif [ \"$value\" -lt 0 ]; then
24962 ˇbreak
24963 ˇelse
24964 ˇecho \"$item\"
24965 ˇfi
24966 ˇdone
24967 ˇdone
24968 ˇ}
24969 "});
24970
24971 // test cursor move to start of each line on tab
24972 // for `case` statement with patterns
24973 cx.set_state(indoc! {"
24974 function handle() {
24975 ˇ case \"$1\" in
24976 ˇ start)
24977 ˇ echo \"a\"
24978 ˇ ;;
24979 ˇ stop)
24980 ˇ echo \"b\"
24981 ˇ ;;
24982 ˇ *)
24983 ˇ echo \"c\"
24984 ˇ ;;
24985 ˇ esac
24986 ˇ}
24987 "});
24988 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24989 cx.assert_editor_state(indoc! {"
24990 function handle() {
24991 ˇcase \"$1\" in
24992 ˇstart)
24993 ˇecho \"a\"
24994 ˇ;;
24995 ˇstop)
24996 ˇecho \"b\"
24997 ˇ;;
24998 ˇ*)
24999 ˇecho \"c\"
25000 ˇ;;
25001 ˇesac
25002 ˇ}
25003 "});
25004}
25005
25006#[gpui::test]
25007async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
25008 init_test(cx, |_| {});
25009
25010 let mut cx = EditorTestContext::new(cx).await;
25011 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25012 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25013
25014 // test indents on comment insert
25015 cx.set_state(indoc! {"
25016 function main() {
25017 ˇ for item in $items; do
25018 ˇ while [ -n \"$item\" ]; do
25019 ˇ if [ \"$value\" -gt 10 ]; then
25020 ˇ continue
25021 ˇ elif [ \"$value\" -lt 0 ]; then
25022 ˇ break
25023 ˇ else
25024 ˇ echo \"$item\"
25025 ˇ fi
25026 ˇ done
25027 ˇ done
25028 ˇ}
25029 "});
25030 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
25031 cx.assert_editor_state(indoc! {"
25032 function main() {
25033 #ˇ for item in $items; do
25034 #ˇ while [ -n \"$item\" ]; do
25035 #ˇ if [ \"$value\" -gt 10 ]; then
25036 #ˇ continue
25037 #ˇ elif [ \"$value\" -lt 0 ]; then
25038 #ˇ break
25039 #ˇ else
25040 #ˇ echo \"$item\"
25041 #ˇ fi
25042 #ˇ done
25043 #ˇ done
25044 #ˇ}
25045 "});
25046}
25047
25048#[gpui::test]
25049async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
25050 init_test(cx, |_| {});
25051
25052 let mut cx = EditorTestContext::new(cx).await;
25053 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25054 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25055
25056 // test `else` auto outdents when typed inside `if` block
25057 cx.set_state(indoc! {"
25058 if [ \"$1\" = \"test\" ]; then
25059 echo \"foo bar\"
25060 ˇ
25061 "});
25062 cx.update_editor(|editor, window, cx| {
25063 editor.handle_input("else", window, cx);
25064 });
25065 cx.assert_editor_state(indoc! {"
25066 if [ \"$1\" = \"test\" ]; then
25067 echo \"foo bar\"
25068 elseˇ
25069 "});
25070
25071 // test `elif` auto outdents when typed inside `if` block
25072 cx.set_state(indoc! {"
25073 if [ \"$1\" = \"test\" ]; then
25074 echo \"foo bar\"
25075 ˇ
25076 "});
25077 cx.update_editor(|editor, window, cx| {
25078 editor.handle_input("elif", window, cx);
25079 });
25080 cx.assert_editor_state(indoc! {"
25081 if [ \"$1\" = \"test\" ]; then
25082 echo \"foo bar\"
25083 elifˇ
25084 "});
25085
25086 // test `fi` auto outdents when typed inside `else` block
25087 cx.set_state(indoc! {"
25088 if [ \"$1\" = \"test\" ]; then
25089 echo \"foo bar\"
25090 else
25091 echo \"bar baz\"
25092 ˇ
25093 "});
25094 cx.update_editor(|editor, window, cx| {
25095 editor.handle_input("fi", window, cx);
25096 });
25097 cx.assert_editor_state(indoc! {"
25098 if [ \"$1\" = \"test\" ]; then
25099 echo \"foo bar\"
25100 else
25101 echo \"bar baz\"
25102 fiˇ
25103 "});
25104
25105 // test `done` auto outdents when typed inside `while` block
25106 cx.set_state(indoc! {"
25107 while read line; do
25108 echo \"$line\"
25109 ˇ
25110 "});
25111 cx.update_editor(|editor, window, cx| {
25112 editor.handle_input("done", window, cx);
25113 });
25114 cx.assert_editor_state(indoc! {"
25115 while read line; do
25116 echo \"$line\"
25117 doneˇ
25118 "});
25119
25120 // test `done` auto outdents when typed inside `for` block
25121 cx.set_state(indoc! {"
25122 for file in *.txt; do
25123 cat \"$file\"
25124 ˇ
25125 "});
25126 cx.update_editor(|editor, window, cx| {
25127 editor.handle_input("done", window, cx);
25128 });
25129 cx.assert_editor_state(indoc! {"
25130 for file in *.txt; do
25131 cat \"$file\"
25132 doneˇ
25133 "});
25134
25135 // test `esac` auto outdents when typed inside `case` block
25136 cx.set_state(indoc! {"
25137 case \"$1\" in
25138 start)
25139 echo \"foo bar\"
25140 ;;
25141 stop)
25142 echo \"bar baz\"
25143 ;;
25144 ˇ
25145 "});
25146 cx.update_editor(|editor, window, cx| {
25147 editor.handle_input("esac", window, cx);
25148 });
25149 cx.assert_editor_state(indoc! {"
25150 case \"$1\" in
25151 start)
25152 echo \"foo bar\"
25153 ;;
25154 stop)
25155 echo \"bar baz\"
25156 ;;
25157 esacˇ
25158 "});
25159
25160 // test `*)` auto outdents when typed inside `case` block
25161 cx.set_state(indoc! {"
25162 case \"$1\" in
25163 start)
25164 echo \"foo bar\"
25165 ;;
25166 ˇ
25167 "});
25168 cx.update_editor(|editor, window, cx| {
25169 editor.handle_input("*)", window, cx);
25170 });
25171 cx.assert_editor_state(indoc! {"
25172 case \"$1\" in
25173 start)
25174 echo \"foo bar\"
25175 ;;
25176 *)ˇ
25177 "});
25178
25179 // test `fi` outdents to correct level with nested if blocks
25180 cx.set_state(indoc! {"
25181 if [ \"$1\" = \"test\" ]; then
25182 echo \"outer if\"
25183 if [ \"$2\" = \"debug\" ]; then
25184 echo \"inner if\"
25185 ˇ
25186 "});
25187 cx.update_editor(|editor, window, cx| {
25188 editor.handle_input("fi", window, cx);
25189 });
25190 cx.assert_editor_state(indoc! {"
25191 if [ \"$1\" = \"test\" ]; then
25192 echo \"outer if\"
25193 if [ \"$2\" = \"debug\" ]; then
25194 echo \"inner if\"
25195 fiˇ
25196 "});
25197}
25198
25199#[gpui::test]
25200async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
25201 init_test(cx, |_| {});
25202 update_test_language_settings(cx, |settings| {
25203 settings.defaults.extend_comment_on_newline = Some(false);
25204 });
25205 let mut cx = EditorTestContext::new(cx).await;
25206 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25207 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25208
25209 // test correct indent after newline on comment
25210 cx.set_state(indoc! {"
25211 # COMMENT:ˇ
25212 "});
25213 cx.update_editor(|editor, window, cx| {
25214 editor.newline(&Newline, window, cx);
25215 });
25216 cx.assert_editor_state(indoc! {"
25217 # COMMENT:
25218 ˇ
25219 "});
25220
25221 // test correct indent after newline after `then`
25222 cx.set_state(indoc! {"
25223
25224 if [ \"$1\" = \"test\" ]; thenˇ
25225 "});
25226 cx.update_editor(|editor, window, cx| {
25227 editor.newline(&Newline, window, cx);
25228 });
25229 cx.run_until_parked();
25230 cx.assert_editor_state(indoc! {"
25231
25232 if [ \"$1\" = \"test\" ]; then
25233 ˇ
25234 "});
25235
25236 // test correct indent after newline after `else`
25237 cx.set_state(indoc! {"
25238 if [ \"$1\" = \"test\" ]; then
25239 elseˇ
25240 "});
25241 cx.update_editor(|editor, window, cx| {
25242 editor.newline(&Newline, window, cx);
25243 });
25244 cx.run_until_parked();
25245 cx.assert_editor_state(indoc! {"
25246 if [ \"$1\" = \"test\" ]; then
25247 else
25248 ˇ
25249 "});
25250
25251 // test correct indent after newline after `elif`
25252 cx.set_state(indoc! {"
25253 if [ \"$1\" = \"test\" ]; then
25254 elifˇ
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 if [ \"$1\" = \"test\" ]; then
25262 elif
25263 ˇ
25264 "});
25265
25266 // test correct indent after newline after `do`
25267 cx.set_state(indoc! {"
25268 for file in *.txt; doˇ
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 for file in *.txt; do
25276 ˇ
25277 "});
25278
25279 // test correct indent after newline after case pattern
25280 cx.set_state(indoc! {"
25281 case \"$1\" in
25282 start)ˇ
25283 "});
25284 cx.update_editor(|editor, window, cx| {
25285 editor.newline(&Newline, window, cx);
25286 });
25287 cx.run_until_parked();
25288 cx.assert_editor_state(indoc! {"
25289 case \"$1\" in
25290 start)
25291 ˇ
25292 "});
25293
25294 // test correct indent after newline after case pattern
25295 cx.set_state(indoc! {"
25296 case \"$1\" in
25297 start)
25298 ;;
25299 *)ˇ
25300 "});
25301 cx.update_editor(|editor, window, cx| {
25302 editor.newline(&Newline, window, cx);
25303 });
25304 cx.run_until_parked();
25305 cx.assert_editor_state(indoc! {"
25306 case \"$1\" in
25307 start)
25308 ;;
25309 *)
25310 ˇ
25311 "});
25312
25313 // test correct indent after newline after function opening brace
25314 cx.set_state(indoc! {"
25315 function 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 function test() {
25323 ˇ
25324 }
25325 "});
25326
25327 // test no extra indent after semicolon on same line
25328 cx.set_state(indoc! {"
25329 echo \"test\";ˇ
25330 "});
25331 cx.update_editor(|editor, window, cx| {
25332 editor.newline(&Newline, window, cx);
25333 });
25334 cx.run_until_parked();
25335 cx.assert_editor_state(indoc! {"
25336 echo \"test\";
25337 ˇ
25338 "});
25339}
25340
25341fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
25342 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
25343 point..point
25344}
25345
25346#[track_caller]
25347fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
25348 let (text, ranges) = marked_text_ranges(marked_text, true);
25349 assert_eq!(editor.text(cx), text);
25350 assert_eq!(
25351 editor.selections.ranges(&editor.display_snapshot(cx)),
25352 ranges,
25353 "Assert selections are {}",
25354 marked_text
25355 );
25356}
25357
25358pub fn handle_signature_help_request(
25359 cx: &mut EditorLspTestContext,
25360 mocked_response: lsp::SignatureHelp,
25361) -> impl Future<Output = ()> + use<> {
25362 let mut request =
25363 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
25364 let mocked_response = mocked_response.clone();
25365 async move { Ok(Some(mocked_response)) }
25366 });
25367
25368 async move {
25369 request.next().await;
25370 }
25371}
25372
25373#[track_caller]
25374pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
25375 cx.update_editor(|editor, _, _| {
25376 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
25377 let entries = menu.entries.borrow();
25378 let entries = entries
25379 .iter()
25380 .map(|entry| entry.string.as_str())
25381 .collect::<Vec<_>>();
25382 assert_eq!(entries, expected);
25383 } else {
25384 panic!("Expected completions menu");
25385 }
25386 });
25387}
25388
25389/// Handle completion request passing a marked string specifying where the completion
25390/// should be triggered from using '|' character, what range should be replaced, and what completions
25391/// should be returned using '<' and '>' to delimit the range.
25392///
25393/// Also see `handle_completion_request_with_insert_and_replace`.
25394#[track_caller]
25395pub fn handle_completion_request(
25396 marked_string: &str,
25397 completions: Vec<&'static str>,
25398 is_incomplete: bool,
25399 counter: Arc<AtomicUsize>,
25400 cx: &mut EditorLspTestContext,
25401) -> impl Future<Output = ()> {
25402 let complete_from_marker: TextRangeMarker = '|'.into();
25403 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25404 let (_, mut marked_ranges) = marked_text_ranges_by(
25405 marked_string,
25406 vec![complete_from_marker.clone(), replace_range_marker.clone()],
25407 );
25408
25409 let complete_from_position =
25410 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25411 let replace_range =
25412 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25413
25414 let mut request =
25415 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25416 let completions = completions.clone();
25417 counter.fetch_add(1, atomic::Ordering::Release);
25418 async move {
25419 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25420 assert_eq!(
25421 params.text_document_position.position,
25422 complete_from_position
25423 );
25424 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
25425 is_incomplete,
25426 item_defaults: None,
25427 items: completions
25428 .iter()
25429 .map(|completion_text| lsp::CompletionItem {
25430 label: completion_text.to_string(),
25431 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
25432 range: replace_range,
25433 new_text: completion_text.to_string(),
25434 })),
25435 ..Default::default()
25436 })
25437 .collect(),
25438 })))
25439 }
25440 });
25441
25442 async move {
25443 request.next().await;
25444 }
25445}
25446
25447/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
25448/// given instead, which also contains an `insert` range.
25449///
25450/// This function uses markers to define ranges:
25451/// - `|` marks the cursor position
25452/// - `<>` marks the replace range
25453/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
25454pub fn handle_completion_request_with_insert_and_replace(
25455 cx: &mut EditorLspTestContext,
25456 marked_string: &str,
25457 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
25458 counter: Arc<AtomicUsize>,
25459) -> impl Future<Output = ()> {
25460 let complete_from_marker: TextRangeMarker = '|'.into();
25461 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25462 let insert_range_marker: TextRangeMarker = ('{', '}').into();
25463
25464 let (_, mut marked_ranges) = marked_text_ranges_by(
25465 marked_string,
25466 vec![
25467 complete_from_marker.clone(),
25468 replace_range_marker.clone(),
25469 insert_range_marker.clone(),
25470 ],
25471 );
25472
25473 let complete_from_position =
25474 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25475 let replace_range =
25476 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25477
25478 let insert_range = match marked_ranges.remove(&insert_range_marker) {
25479 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
25480 _ => lsp::Range {
25481 start: replace_range.start,
25482 end: complete_from_position,
25483 },
25484 };
25485
25486 let mut request =
25487 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25488 let completions = completions.clone();
25489 counter.fetch_add(1, atomic::Ordering::Release);
25490 async move {
25491 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25492 assert_eq!(
25493 params.text_document_position.position, complete_from_position,
25494 "marker `|` position doesn't match",
25495 );
25496 Ok(Some(lsp::CompletionResponse::Array(
25497 completions
25498 .iter()
25499 .map(|(label, new_text)| lsp::CompletionItem {
25500 label: label.to_string(),
25501 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
25502 lsp::InsertReplaceEdit {
25503 insert: insert_range,
25504 replace: replace_range,
25505 new_text: new_text.to_string(),
25506 },
25507 )),
25508 ..Default::default()
25509 })
25510 .collect(),
25511 )))
25512 }
25513 });
25514
25515 async move {
25516 request.next().await;
25517 }
25518}
25519
25520fn handle_resolve_completion_request(
25521 cx: &mut EditorLspTestContext,
25522 edits: Option<Vec<(&'static str, &'static str)>>,
25523) -> impl Future<Output = ()> {
25524 let edits = edits.map(|edits| {
25525 edits
25526 .iter()
25527 .map(|(marked_string, new_text)| {
25528 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
25529 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
25530 lsp::TextEdit::new(replace_range, new_text.to_string())
25531 })
25532 .collect::<Vec<_>>()
25533 });
25534
25535 let mut request =
25536 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
25537 let edits = edits.clone();
25538 async move {
25539 Ok(lsp::CompletionItem {
25540 additional_text_edits: edits,
25541 ..Default::default()
25542 })
25543 }
25544 });
25545
25546 async move {
25547 request.next().await;
25548 }
25549}
25550
25551pub(crate) fn update_test_language_settings(
25552 cx: &mut TestAppContext,
25553 f: impl Fn(&mut AllLanguageSettingsContent),
25554) {
25555 cx.update(|cx| {
25556 SettingsStore::update_global(cx, |store, cx| {
25557 store.update_user_settings(cx, |settings| f(&mut settings.project.all_languages));
25558 });
25559 });
25560}
25561
25562pub(crate) fn update_test_project_settings(
25563 cx: &mut TestAppContext,
25564 f: impl Fn(&mut ProjectSettingsContent),
25565) {
25566 cx.update(|cx| {
25567 SettingsStore::update_global(cx, |store, cx| {
25568 store.update_user_settings(cx, |settings| f(&mut settings.project));
25569 });
25570 });
25571}
25572
25573pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
25574 cx.update(|cx| {
25575 assets::Assets.load_test_fonts(cx);
25576 let store = SettingsStore::test(cx);
25577 cx.set_global(store);
25578 theme::init(theme::LoadThemes::JustBase, cx);
25579 release_channel::init(SemanticVersion::default(), cx);
25580 client::init_settings(cx);
25581 language::init(cx);
25582 Project::init_settings(cx);
25583 workspace::init_settings(cx);
25584 crate::init(cx);
25585 });
25586 zlog::init_test();
25587 update_test_language_settings(cx, f);
25588}
25589
25590#[track_caller]
25591fn assert_hunk_revert(
25592 not_reverted_text_with_selections: &str,
25593 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
25594 expected_reverted_text_with_selections: &str,
25595 base_text: &str,
25596 cx: &mut EditorLspTestContext,
25597) {
25598 cx.set_state(not_reverted_text_with_selections);
25599 cx.set_head_text(base_text);
25600 cx.executor().run_until_parked();
25601
25602 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
25603 let snapshot = editor.snapshot(window, cx);
25604 let reverted_hunk_statuses = snapshot
25605 .buffer_snapshot()
25606 .diff_hunks_in_range(0..snapshot.buffer_snapshot().len())
25607 .map(|hunk| hunk.status().kind)
25608 .collect::<Vec<_>>();
25609
25610 editor.git_restore(&Default::default(), window, cx);
25611 reverted_hunk_statuses
25612 });
25613 cx.executor().run_until_parked();
25614 cx.assert_editor_state(expected_reverted_text_with_selections);
25615 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
25616}
25617
25618#[gpui::test(iterations = 10)]
25619async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
25620 init_test(cx, |_| {});
25621
25622 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
25623 let counter = diagnostic_requests.clone();
25624
25625 let fs = FakeFs::new(cx.executor());
25626 fs.insert_tree(
25627 path!("/a"),
25628 json!({
25629 "first.rs": "fn main() { let a = 5; }",
25630 "second.rs": "// Test file",
25631 }),
25632 )
25633 .await;
25634
25635 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25636 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25637 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25638
25639 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25640 language_registry.add(rust_lang());
25641 let mut fake_servers = language_registry.register_fake_lsp(
25642 "Rust",
25643 FakeLspAdapter {
25644 capabilities: lsp::ServerCapabilities {
25645 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
25646 lsp::DiagnosticOptions {
25647 identifier: None,
25648 inter_file_dependencies: true,
25649 workspace_diagnostics: true,
25650 work_done_progress_options: Default::default(),
25651 },
25652 )),
25653 ..Default::default()
25654 },
25655 ..Default::default()
25656 },
25657 );
25658
25659 let editor = workspace
25660 .update(cx, |workspace, window, cx| {
25661 workspace.open_abs_path(
25662 PathBuf::from(path!("/a/first.rs")),
25663 OpenOptions::default(),
25664 window,
25665 cx,
25666 )
25667 })
25668 .unwrap()
25669 .await
25670 .unwrap()
25671 .downcast::<Editor>()
25672 .unwrap();
25673 let fake_server = fake_servers.next().await.unwrap();
25674 let server_id = fake_server.server.server_id();
25675 let mut first_request = fake_server
25676 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
25677 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
25678 let result_id = Some(new_result_id.to_string());
25679 assert_eq!(
25680 params.text_document.uri,
25681 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25682 );
25683 async move {
25684 Ok(lsp::DocumentDiagnosticReportResult::Report(
25685 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
25686 related_documents: None,
25687 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
25688 items: Vec::new(),
25689 result_id,
25690 },
25691 }),
25692 ))
25693 }
25694 });
25695
25696 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
25697 project.update(cx, |project, cx| {
25698 let buffer_id = editor
25699 .read(cx)
25700 .buffer()
25701 .read(cx)
25702 .as_singleton()
25703 .expect("created a singleton buffer")
25704 .read(cx)
25705 .remote_id();
25706 let buffer_result_id = project
25707 .lsp_store()
25708 .read(cx)
25709 .result_id(server_id, buffer_id, cx);
25710 assert_eq!(expected, buffer_result_id);
25711 });
25712 };
25713
25714 ensure_result_id(None, cx);
25715 cx.executor().advance_clock(Duration::from_millis(60));
25716 cx.executor().run_until_parked();
25717 assert_eq!(
25718 diagnostic_requests.load(atomic::Ordering::Acquire),
25719 1,
25720 "Opening file should trigger diagnostic request"
25721 );
25722 first_request
25723 .next()
25724 .await
25725 .expect("should have sent the first diagnostics pull request");
25726 ensure_result_id(Some("1".to_string()), cx);
25727
25728 // Editing should trigger diagnostics
25729 editor.update_in(cx, |editor, window, cx| {
25730 editor.handle_input("2", window, cx)
25731 });
25732 cx.executor().advance_clock(Duration::from_millis(60));
25733 cx.executor().run_until_parked();
25734 assert_eq!(
25735 diagnostic_requests.load(atomic::Ordering::Acquire),
25736 2,
25737 "Editing should trigger diagnostic request"
25738 );
25739 ensure_result_id(Some("2".to_string()), cx);
25740
25741 // Moving cursor should not trigger diagnostic request
25742 editor.update_in(cx, |editor, window, cx| {
25743 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25744 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
25745 });
25746 });
25747 cx.executor().advance_clock(Duration::from_millis(60));
25748 cx.executor().run_until_parked();
25749 assert_eq!(
25750 diagnostic_requests.load(atomic::Ordering::Acquire),
25751 2,
25752 "Cursor movement should not trigger diagnostic request"
25753 );
25754 ensure_result_id(Some("2".to_string()), cx);
25755 // Multiple rapid edits should be debounced
25756 for _ in 0..5 {
25757 editor.update_in(cx, |editor, window, cx| {
25758 editor.handle_input("x", window, cx)
25759 });
25760 }
25761 cx.executor().advance_clock(Duration::from_millis(60));
25762 cx.executor().run_until_parked();
25763
25764 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
25765 assert!(
25766 final_requests <= 4,
25767 "Multiple rapid edits should be debounced (got {final_requests} requests)",
25768 );
25769 ensure_result_id(Some(final_requests.to_string()), cx);
25770}
25771
25772#[gpui::test]
25773async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
25774 // Regression test for issue #11671
25775 // Previously, adding a cursor after moving multiple cursors would reset
25776 // the cursor count instead of adding to the existing cursors.
25777 init_test(cx, |_| {});
25778 let mut cx = EditorTestContext::new(cx).await;
25779
25780 // Create a simple buffer with cursor at start
25781 cx.set_state(indoc! {"
25782 ˇaaaa
25783 bbbb
25784 cccc
25785 dddd
25786 eeee
25787 ffff
25788 gggg
25789 hhhh"});
25790
25791 // Add 2 cursors below (so we have 3 total)
25792 cx.update_editor(|editor, window, cx| {
25793 editor.add_selection_below(&Default::default(), window, cx);
25794 editor.add_selection_below(&Default::default(), window, cx);
25795 });
25796
25797 // Verify we have 3 cursors
25798 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
25799 assert_eq!(
25800 initial_count, 3,
25801 "Should have 3 cursors after adding 2 below"
25802 );
25803
25804 // Move down one line
25805 cx.update_editor(|editor, window, cx| {
25806 editor.move_down(&MoveDown, window, cx);
25807 });
25808
25809 // Add another cursor below
25810 cx.update_editor(|editor, window, cx| {
25811 editor.add_selection_below(&Default::default(), window, cx);
25812 });
25813
25814 // Should now have 4 cursors (3 original + 1 new)
25815 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
25816 assert_eq!(
25817 final_count, 4,
25818 "Should have 4 cursors after moving and adding another"
25819 );
25820}
25821
25822#[gpui::test]
25823async fn test_add_selection_skip_soft_wrap_option(cx: &mut TestAppContext) {
25824 init_test(cx, |_| {});
25825
25826 let mut cx = EditorTestContext::new(cx).await;
25827
25828 cx.set_state(indoc!(
25829 r#"ˇThis is a very long line that will be wrapped when soft wrapping is enabled
25830 Second line here"#
25831 ));
25832
25833 cx.update_editor(|editor, window, cx| {
25834 // Enable soft wrapping with a narrow width to force soft wrapping and
25835 // confirm that more than 2 rows are being displayed.
25836 editor.set_wrap_width(Some(100.0.into()), cx);
25837 assert!(editor.display_text(cx).lines().count() > 2);
25838
25839 editor.add_selection_below(
25840 &AddSelectionBelow {
25841 skip_soft_wrap: true,
25842 },
25843 window,
25844 cx,
25845 );
25846
25847 assert_eq!(
25848 display_ranges(editor, cx),
25849 &[
25850 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
25851 DisplayPoint::new(DisplayRow(8), 0)..DisplayPoint::new(DisplayRow(8), 0),
25852 ]
25853 );
25854
25855 editor.add_selection_above(
25856 &AddSelectionAbove {
25857 skip_soft_wrap: true,
25858 },
25859 window,
25860 cx,
25861 );
25862
25863 assert_eq!(
25864 display_ranges(editor, cx),
25865 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
25866 );
25867
25868 editor.add_selection_below(
25869 &AddSelectionBelow {
25870 skip_soft_wrap: false,
25871 },
25872 window,
25873 cx,
25874 );
25875
25876 assert_eq!(
25877 display_ranges(editor, cx),
25878 &[
25879 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
25880 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
25881 ]
25882 );
25883
25884 editor.add_selection_above(
25885 &AddSelectionAbove {
25886 skip_soft_wrap: false,
25887 },
25888 window,
25889 cx,
25890 );
25891
25892 assert_eq!(
25893 display_ranges(editor, cx),
25894 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
25895 );
25896 });
25897}
25898
25899#[gpui::test(iterations = 10)]
25900async fn test_document_colors(cx: &mut TestAppContext) {
25901 let expected_color = Rgba {
25902 r: 0.33,
25903 g: 0.33,
25904 b: 0.33,
25905 a: 0.33,
25906 };
25907
25908 init_test(cx, |_| {});
25909
25910 let fs = FakeFs::new(cx.executor());
25911 fs.insert_tree(
25912 path!("/a"),
25913 json!({
25914 "first.rs": "fn main() { let a = 5; }",
25915 }),
25916 )
25917 .await;
25918
25919 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25920 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25921 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25922
25923 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25924 language_registry.add(rust_lang());
25925 let mut fake_servers = language_registry.register_fake_lsp(
25926 "Rust",
25927 FakeLspAdapter {
25928 capabilities: lsp::ServerCapabilities {
25929 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
25930 ..lsp::ServerCapabilities::default()
25931 },
25932 name: "rust-analyzer",
25933 ..FakeLspAdapter::default()
25934 },
25935 );
25936 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
25937 "Rust",
25938 FakeLspAdapter {
25939 capabilities: lsp::ServerCapabilities {
25940 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
25941 ..lsp::ServerCapabilities::default()
25942 },
25943 name: "not-rust-analyzer",
25944 ..FakeLspAdapter::default()
25945 },
25946 );
25947
25948 let editor = workspace
25949 .update(cx, |workspace, window, cx| {
25950 workspace.open_abs_path(
25951 PathBuf::from(path!("/a/first.rs")),
25952 OpenOptions::default(),
25953 window,
25954 cx,
25955 )
25956 })
25957 .unwrap()
25958 .await
25959 .unwrap()
25960 .downcast::<Editor>()
25961 .unwrap();
25962 let fake_language_server = fake_servers.next().await.unwrap();
25963 let fake_language_server_without_capabilities =
25964 fake_servers_without_capabilities.next().await.unwrap();
25965 let requests_made = Arc::new(AtomicUsize::new(0));
25966 let closure_requests_made = Arc::clone(&requests_made);
25967 let mut color_request_handle = fake_language_server
25968 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
25969 let requests_made = Arc::clone(&closure_requests_made);
25970 async move {
25971 assert_eq!(
25972 params.text_document.uri,
25973 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25974 );
25975 requests_made.fetch_add(1, atomic::Ordering::Release);
25976 Ok(vec![
25977 lsp::ColorInformation {
25978 range: lsp::Range {
25979 start: lsp::Position {
25980 line: 0,
25981 character: 0,
25982 },
25983 end: lsp::Position {
25984 line: 0,
25985 character: 1,
25986 },
25987 },
25988 color: lsp::Color {
25989 red: 0.33,
25990 green: 0.33,
25991 blue: 0.33,
25992 alpha: 0.33,
25993 },
25994 },
25995 lsp::ColorInformation {
25996 range: lsp::Range {
25997 start: lsp::Position {
25998 line: 0,
25999 character: 0,
26000 },
26001 end: lsp::Position {
26002 line: 0,
26003 character: 1,
26004 },
26005 },
26006 color: lsp::Color {
26007 red: 0.33,
26008 green: 0.33,
26009 blue: 0.33,
26010 alpha: 0.33,
26011 },
26012 },
26013 ])
26014 }
26015 });
26016
26017 let _handle = fake_language_server_without_capabilities
26018 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
26019 panic!("Should not be called");
26020 });
26021 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26022 color_request_handle.next().await.unwrap();
26023 cx.run_until_parked();
26024 assert_eq!(
26025 1,
26026 requests_made.load(atomic::Ordering::Acquire),
26027 "Should query for colors once per editor open"
26028 );
26029 editor.update_in(cx, |editor, _, cx| {
26030 assert_eq!(
26031 vec![expected_color],
26032 extract_color_inlays(editor, cx),
26033 "Should have an initial inlay"
26034 );
26035 });
26036
26037 // opening another file in a split should not influence the LSP query counter
26038 workspace
26039 .update(cx, |workspace, window, cx| {
26040 assert_eq!(
26041 workspace.panes().len(),
26042 1,
26043 "Should have one pane with one editor"
26044 );
26045 workspace.move_item_to_pane_in_direction(
26046 &MoveItemToPaneInDirection {
26047 direction: SplitDirection::Right,
26048 focus: false,
26049 clone: true,
26050 },
26051 window,
26052 cx,
26053 );
26054 })
26055 .unwrap();
26056 cx.run_until_parked();
26057 workspace
26058 .update(cx, |workspace, _, cx| {
26059 let panes = workspace.panes();
26060 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
26061 for pane in panes {
26062 let editor = pane
26063 .read(cx)
26064 .active_item()
26065 .and_then(|item| item.downcast::<Editor>())
26066 .expect("Should have opened an editor in each split");
26067 let editor_file = editor
26068 .read(cx)
26069 .buffer()
26070 .read(cx)
26071 .as_singleton()
26072 .expect("test deals with singleton buffers")
26073 .read(cx)
26074 .file()
26075 .expect("test buffese should have a file")
26076 .path();
26077 assert_eq!(
26078 editor_file.as_ref(),
26079 rel_path("first.rs"),
26080 "Both editors should be opened for the same file"
26081 )
26082 }
26083 })
26084 .unwrap();
26085
26086 cx.executor().advance_clock(Duration::from_millis(500));
26087 let save = editor.update_in(cx, |editor, window, cx| {
26088 editor.move_to_end(&MoveToEnd, window, cx);
26089 editor.handle_input("dirty", window, cx);
26090 editor.save(
26091 SaveOptions {
26092 format: true,
26093 autosave: true,
26094 },
26095 project.clone(),
26096 window,
26097 cx,
26098 )
26099 });
26100 save.await.unwrap();
26101
26102 color_request_handle.next().await.unwrap();
26103 cx.run_until_parked();
26104 assert_eq!(
26105 2,
26106 requests_made.load(atomic::Ordering::Acquire),
26107 "Should query for colors once per save (deduplicated) and once per formatting after save"
26108 );
26109
26110 drop(editor);
26111 let close = workspace
26112 .update(cx, |workspace, window, cx| {
26113 workspace.active_pane().update(cx, |pane, cx| {
26114 pane.close_active_item(&CloseActiveItem::default(), window, cx)
26115 })
26116 })
26117 .unwrap();
26118 close.await.unwrap();
26119 let close = workspace
26120 .update(cx, |workspace, window, cx| {
26121 workspace.active_pane().update(cx, |pane, cx| {
26122 pane.close_active_item(&CloseActiveItem::default(), window, cx)
26123 })
26124 })
26125 .unwrap();
26126 close.await.unwrap();
26127 assert_eq!(
26128 2,
26129 requests_made.load(atomic::Ordering::Acquire),
26130 "After saving and closing all editors, no extra requests should be made"
26131 );
26132 workspace
26133 .update(cx, |workspace, _, cx| {
26134 assert!(
26135 workspace.active_item(cx).is_none(),
26136 "Should close all editors"
26137 )
26138 })
26139 .unwrap();
26140
26141 workspace
26142 .update(cx, |workspace, window, cx| {
26143 workspace.active_pane().update(cx, |pane, cx| {
26144 pane.navigate_backward(&workspace::GoBack, window, cx);
26145 })
26146 })
26147 .unwrap();
26148 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26149 cx.run_until_parked();
26150 let editor = workspace
26151 .update(cx, |workspace, _, cx| {
26152 workspace
26153 .active_item(cx)
26154 .expect("Should have reopened the editor again after navigating back")
26155 .downcast::<Editor>()
26156 .expect("Should be an editor")
26157 })
26158 .unwrap();
26159
26160 assert_eq!(
26161 2,
26162 requests_made.load(atomic::Ordering::Acquire),
26163 "Cache should be reused on buffer close and reopen"
26164 );
26165 editor.update(cx, |editor, cx| {
26166 assert_eq!(
26167 vec![expected_color],
26168 extract_color_inlays(editor, cx),
26169 "Should have an initial inlay"
26170 );
26171 });
26172
26173 drop(color_request_handle);
26174 let closure_requests_made = Arc::clone(&requests_made);
26175 let mut empty_color_request_handle = fake_language_server
26176 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
26177 let requests_made = Arc::clone(&closure_requests_made);
26178 async move {
26179 assert_eq!(
26180 params.text_document.uri,
26181 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
26182 );
26183 requests_made.fetch_add(1, atomic::Ordering::Release);
26184 Ok(Vec::new())
26185 }
26186 });
26187 let save = editor.update_in(cx, |editor, window, cx| {
26188 editor.move_to_end(&MoveToEnd, window, cx);
26189 editor.handle_input("dirty_again", window, cx);
26190 editor.save(
26191 SaveOptions {
26192 format: false,
26193 autosave: true,
26194 },
26195 project.clone(),
26196 window,
26197 cx,
26198 )
26199 });
26200 save.await.unwrap();
26201
26202 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26203 empty_color_request_handle.next().await.unwrap();
26204 cx.run_until_parked();
26205 assert_eq!(
26206 3,
26207 requests_made.load(atomic::Ordering::Acquire),
26208 "Should query for colors once per save only, as formatting was not requested"
26209 );
26210 editor.update(cx, |editor, cx| {
26211 assert_eq!(
26212 Vec::<Rgba>::new(),
26213 extract_color_inlays(editor, cx),
26214 "Should clear all colors when the server returns an empty response"
26215 );
26216 });
26217}
26218
26219#[gpui::test]
26220async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
26221 init_test(cx, |_| {});
26222 let (editor, cx) = cx.add_window_view(Editor::single_line);
26223 editor.update_in(cx, |editor, window, cx| {
26224 editor.set_text("oops\n\nwow\n", window, cx)
26225 });
26226 cx.run_until_parked();
26227 editor.update(cx, |editor, cx| {
26228 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
26229 });
26230 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
26231 cx.run_until_parked();
26232 editor.update(cx, |editor, cx| {
26233 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
26234 });
26235}
26236
26237#[gpui::test]
26238async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
26239 init_test(cx, |_| {});
26240
26241 cx.update(|cx| {
26242 register_project_item::<Editor>(cx);
26243 });
26244
26245 let fs = FakeFs::new(cx.executor());
26246 fs.insert_tree("/root1", json!({})).await;
26247 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
26248 .await;
26249
26250 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
26251 let (workspace, cx) =
26252 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
26253
26254 let worktree_id = project.update(cx, |project, cx| {
26255 project.worktrees(cx).next().unwrap().read(cx).id()
26256 });
26257
26258 let handle = workspace
26259 .update_in(cx, |workspace, window, cx| {
26260 let project_path = (worktree_id, rel_path("one.pdf"));
26261 workspace.open_path(project_path, None, true, window, cx)
26262 })
26263 .await
26264 .unwrap();
26265
26266 assert_eq!(
26267 handle.to_any().entity_type(),
26268 TypeId::of::<InvalidItemView>()
26269 );
26270}
26271
26272#[gpui::test]
26273async fn test_select_next_prev_syntax_node(cx: &mut TestAppContext) {
26274 init_test(cx, |_| {});
26275
26276 let language = Arc::new(Language::new(
26277 LanguageConfig::default(),
26278 Some(tree_sitter_rust::LANGUAGE.into()),
26279 ));
26280
26281 // Test hierarchical sibling navigation
26282 let text = r#"
26283 fn outer() {
26284 if condition {
26285 let a = 1;
26286 }
26287 let b = 2;
26288 }
26289
26290 fn another() {
26291 let c = 3;
26292 }
26293 "#;
26294
26295 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
26296 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
26297 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
26298
26299 // Wait for parsing to complete
26300 editor
26301 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
26302 .await;
26303
26304 editor.update_in(cx, |editor, window, cx| {
26305 // Start by selecting "let a = 1;" inside the if block
26306 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26307 s.select_display_ranges([
26308 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 26)
26309 ]);
26310 });
26311
26312 let initial_selection = editor
26313 .selections
26314 .display_ranges(&editor.display_snapshot(cx));
26315 assert_eq!(initial_selection.len(), 1, "Should have one selection");
26316
26317 // Test select next sibling - should move up levels to find the next sibling
26318 // Since "let a = 1;" has no siblings in the if block, it should move up
26319 // to find "let b = 2;" which is a sibling of the if block
26320 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
26321 let next_selection = editor
26322 .selections
26323 .display_ranges(&editor.display_snapshot(cx));
26324
26325 // Should have a selection and it should be different from the initial
26326 assert_eq!(
26327 next_selection.len(),
26328 1,
26329 "Should have one selection after next"
26330 );
26331 assert_ne!(
26332 next_selection[0], initial_selection[0],
26333 "Next sibling selection should be different"
26334 );
26335
26336 // Test hierarchical navigation by going to the end of the current function
26337 // and trying to navigate to the next function
26338 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26339 s.select_display_ranges([
26340 DisplayPoint::new(DisplayRow(5), 12)..DisplayPoint::new(DisplayRow(5), 22)
26341 ]);
26342 });
26343
26344 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
26345 let function_next_selection = editor
26346 .selections
26347 .display_ranges(&editor.display_snapshot(cx));
26348
26349 // Should move to the next function
26350 assert_eq!(
26351 function_next_selection.len(),
26352 1,
26353 "Should have one selection after function next"
26354 );
26355
26356 // Test select previous sibling navigation
26357 editor.select_prev_syntax_node(&SelectPreviousSyntaxNode, window, cx);
26358 let prev_selection = editor
26359 .selections
26360 .display_ranges(&editor.display_snapshot(cx));
26361
26362 // Should have a selection and it should be different
26363 assert_eq!(
26364 prev_selection.len(),
26365 1,
26366 "Should have one selection after prev"
26367 );
26368 assert_ne!(
26369 prev_selection[0], function_next_selection[0],
26370 "Previous sibling selection should be different from next"
26371 );
26372 });
26373}
26374
26375#[gpui::test]
26376async fn test_next_prev_document_highlight(cx: &mut TestAppContext) {
26377 init_test(cx, |_| {});
26378
26379 let mut cx = EditorTestContext::new(cx).await;
26380 cx.set_state(
26381 "let ˇvariable = 42;
26382let another = variable + 1;
26383let result = variable * 2;",
26384 );
26385
26386 // Set up document highlights manually (simulating LSP response)
26387 cx.update_editor(|editor, _window, cx| {
26388 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
26389
26390 // Create highlights for "variable" occurrences
26391 let highlight_ranges = [
26392 Point::new(0, 4)..Point::new(0, 12), // First "variable"
26393 Point::new(1, 14)..Point::new(1, 22), // Second "variable"
26394 Point::new(2, 13)..Point::new(2, 21), // Third "variable"
26395 ];
26396
26397 let anchor_ranges: Vec<_> = highlight_ranges
26398 .iter()
26399 .map(|range| range.clone().to_anchors(&buffer_snapshot))
26400 .collect();
26401
26402 editor.highlight_background::<DocumentHighlightRead>(
26403 &anchor_ranges,
26404 |theme| theme.colors().editor_document_highlight_read_background,
26405 cx,
26406 );
26407 });
26408
26409 // Go to next highlight - should move to second "variable"
26410 cx.update_editor(|editor, window, cx| {
26411 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26412 });
26413 cx.assert_editor_state(
26414 "let variable = 42;
26415let another = ˇvariable + 1;
26416let result = variable * 2;",
26417 );
26418
26419 // Go to next highlight - should move to third "variable"
26420 cx.update_editor(|editor, window, cx| {
26421 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26422 });
26423 cx.assert_editor_state(
26424 "let variable = 42;
26425let another = variable + 1;
26426let result = ˇvariable * 2;",
26427 );
26428
26429 // Go to next highlight - should stay at third "variable" (no wrap-around)
26430 cx.update_editor(|editor, window, cx| {
26431 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26432 });
26433 cx.assert_editor_state(
26434 "let variable = 42;
26435let another = variable + 1;
26436let result = ˇvariable * 2;",
26437 );
26438
26439 // Now test going backwards from third position
26440 cx.update_editor(|editor, window, cx| {
26441 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26442 });
26443 cx.assert_editor_state(
26444 "let variable = 42;
26445let another = ˇvariable + 1;
26446let result = variable * 2;",
26447 );
26448
26449 // Go to previous highlight - should move to first "variable"
26450 cx.update_editor(|editor, window, cx| {
26451 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26452 });
26453 cx.assert_editor_state(
26454 "let ˇvariable = 42;
26455let another = variable + 1;
26456let result = variable * 2;",
26457 );
26458
26459 // Go to previous highlight - should stay on first "variable"
26460 cx.update_editor(|editor, window, cx| {
26461 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26462 });
26463 cx.assert_editor_state(
26464 "let ˇvariable = 42;
26465let another = variable + 1;
26466let result = variable * 2;",
26467 );
26468}
26469
26470#[gpui::test]
26471async fn test_paste_url_from_other_app_creates_markdown_link_over_selected_text(
26472 cx: &mut gpui::TestAppContext,
26473) {
26474 init_test(cx, |_| {});
26475
26476 let url = "https://zed.dev";
26477
26478 let markdown_language = Arc::new(Language::new(
26479 LanguageConfig {
26480 name: "Markdown".into(),
26481 ..LanguageConfig::default()
26482 },
26483 None,
26484 ));
26485
26486 let mut cx = EditorTestContext::new(cx).await;
26487 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26488 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)");
26489
26490 cx.update_editor(|editor, window, cx| {
26491 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26492 editor.paste(&Paste, window, cx);
26493 });
26494
26495 cx.assert_editor_state(&format!(
26496 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)"
26497 ));
26498}
26499
26500#[gpui::test]
26501async fn test_paste_url_from_zed_copy_creates_markdown_link_over_selected_text(
26502 cx: &mut gpui::TestAppContext,
26503) {
26504 init_test(cx, |_| {});
26505
26506 let url = "https://zed.dev";
26507
26508 let markdown_language = Arc::new(Language::new(
26509 LanguageConfig {
26510 name: "Markdown".into(),
26511 ..LanguageConfig::default()
26512 },
26513 None,
26514 ));
26515
26516 let mut cx = EditorTestContext::new(cx).await;
26517 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26518 cx.set_state(&format!(
26519 "Hello, editor.\nZed is great (see this link: )\n«{url}ˇ»"
26520 ));
26521
26522 cx.update_editor(|editor, window, cx| {
26523 editor.copy(&Copy, window, cx);
26524 });
26525
26526 cx.set_state(&format!(
26527 "Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)\n{url}"
26528 ));
26529
26530 cx.update_editor(|editor, window, cx| {
26531 editor.paste(&Paste, window, cx);
26532 });
26533
26534 cx.assert_editor_state(&format!(
26535 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)\n{url}"
26536 ));
26537}
26538
26539#[gpui::test]
26540async fn test_paste_url_from_other_app_replaces_existing_url_without_creating_markdown_link(
26541 cx: &mut gpui::TestAppContext,
26542) {
26543 init_test(cx, |_| {});
26544
26545 let url = "https://zed.dev";
26546
26547 let markdown_language = Arc::new(Language::new(
26548 LanguageConfig {
26549 name: "Markdown".into(),
26550 ..LanguageConfig::default()
26551 },
26552 None,
26553 ));
26554
26555 let mut cx = EditorTestContext::new(cx).await;
26556 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26557 cx.set_state("Please visit zed's homepage: «https://www.apple.comˇ»");
26558
26559 cx.update_editor(|editor, window, cx| {
26560 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26561 editor.paste(&Paste, window, cx);
26562 });
26563
26564 cx.assert_editor_state(&format!("Please visit zed's homepage: {url}ˇ"));
26565}
26566
26567#[gpui::test]
26568async fn test_paste_plain_text_from_other_app_replaces_selection_without_creating_markdown_link(
26569 cx: &mut gpui::TestAppContext,
26570) {
26571 init_test(cx, |_| {});
26572
26573 let text = "Awesome";
26574
26575 let markdown_language = Arc::new(Language::new(
26576 LanguageConfig {
26577 name: "Markdown".into(),
26578 ..LanguageConfig::default()
26579 },
26580 None,
26581 ));
26582
26583 let mut cx = EditorTestContext::new(cx).await;
26584 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26585 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat»");
26586
26587 cx.update_editor(|editor, window, cx| {
26588 cx.write_to_clipboard(ClipboardItem::new_string(text.to_string()));
26589 editor.paste(&Paste, window, cx);
26590 });
26591
26592 cx.assert_editor_state(&format!("Hello, {text}ˇ.\nZed is {text}ˇ"));
26593}
26594
26595#[gpui::test]
26596async fn test_paste_url_from_other_app_without_creating_markdown_link_in_non_markdown_language(
26597 cx: &mut gpui::TestAppContext,
26598) {
26599 init_test(cx, |_| {});
26600
26601 let url = "https://zed.dev";
26602
26603 let markdown_language = Arc::new(Language::new(
26604 LanguageConfig {
26605 name: "Rust".into(),
26606 ..LanguageConfig::default()
26607 },
26608 None,
26609 ));
26610
26611 let mut cx = EditorTestContext::new(cx).await;
26612 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26613 cx.set_state("// Hello, «editorˇ».\n// Zed is «ˇgreat» (see this link: ˇ)");
26614
26615 cx.update_editor(|editor, window, cx| {
26616 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26617 editor.paste(&Paste, window, cx);
26618 });
26619
26620 cx.assert_editor_state(&format!(
26621 "// Hello, {url}ˇ.\n// Zed is {url}ˇ (see this link: {url}ˇ)"
26622 ));
26623}
26624
26625#[gpui::test]
26626async fn test_paste_url_from_other_app_creates_markdown_link_selectively_in_multi_buffer(
26627 cx: &mut TestAppContext,
26628) {
26629 init_test(cx, |_| {});
26630
26631 let url = "https://zed.dev";
26632
26633 let markdown_language = Arc::new(Language::new(
26634 LanguageConfig {
26635 name: "Markdown".into(),
26636 ..LanguageConfig::default()
26637 },
26638 None,
26639 ));
26640
26641 let (editor, cx) = cx.add_window_view(|window, cx| {
26642 let multi_buffer = MultiBuffer::build_multi(
26643 [
26644 ("this will embed -> link", vec![Point::row_range(0..1)]),
26645 ("this will replace -> link", vec![Point::row_range(0..1)]),
26646 ],
26647 cx,
26648 );
26649 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
26650 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26651 s.select_ranges(vec![
26652 Point::new(0, 19)..Point::new(0, 23),
26653 Point::new(1, 21)..Point::new(1, 25),
26654 ])
26655 });
26656 let first_buffer_id = multi_buffer
26657 .read(cx)
26658 .excerpt_buffer_ids()
26659 .into_iter()
26660 .next()
26661 .unwrap();
26662 let first_buffer = multi_buffer.read(cx).buffer(first_buffer_id).unwrap();
26663 first_buffer.update(cx, |buffer, cx| {
26664 buffer.set_language(Some(markdown_language.clone()), cx);
26665 });
26666
26667 editor
26668 });
26669 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
26670
26671 cx.update_editor(|editor, window, cx| {
26672 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26673 editor.paste(&Paste, window, cx);
26674 });
26675
26676 cx.assert_editor_state(&format!(
26677 "this will embed -> [link]({url})ˇ\nthis will replace -> {url}ˇ"
26678 ));
26679}
26680
26681#[gpui::test]
26682async fn test_race_in_multibuffer_save(cx: &mut TestAppContext) {
26683 init_test(cx, |_| {});
26684
26685 let fs = FakeFs::new(cx.executor());
26686 fs.insert_tree(
26687 path!("/project"),
26688 json!({
26689 "first.rs": "# First Document\nSome content here.",
26690 "second.rs": "Plain text content for second file.",
26691 }),
26692 )
26693 .await;
26694
26695 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
26696 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
26697 let cx = &mut VisualTestContext::from_window(*workspace, cx);
26698
26699 let language = rust_lang();
26700 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
26701 language_registry.add(language.clone());
26702 let mut fake_servers = language_registry.register_fake_lsp(
26703 "Rust",
26704 FakeLspAdapter {
26705 ..FakeLspAdapter::default()
26706 },
26707 );
26708
26709 let buffer1 = project
26710 .update(cx, |project, cx| {
26711 project.open_local_buffer(PathBuf::from(path!("/project/first.rs")), cx)
26712 })
26713 .await
26714 .unwrap();
26715 let buffer2 = project
26716 .update(cx, |project, cx| {
26717 project.open_local_buffer(PathBuf::from(path!("/project/second.rs")), cx)
26718 })
26719 .await
26720 .unwrap();
26721
26722 let multi_buffer = cx.new(|cx| {
26723 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
26724 multi_buffer.set_excerpts_for_path(
26725 PathKey::for_buffer(&buffer1, cx),
26726 buffer1.clone(),
26727 [Point::zero()..buffer1.read(cx).max_point()],
26728 3,
26729 cx,
26730 );
26731 multi_buffer.set_excerpts_for_path(
26732 PathKey::for_buffer(&buffer2, cx),
26733 buffer2.clone(),
26734 [Point::zero()..buffer1.read(cx).max_point()],
26735 3,
26736 cx,
26737 );
26738 multi_buffer
26739 });
26740
26741 let (editor, cx) = cx.add_window_view(|window, cx| {
26742 Editor::new(
26743 EditorMode::full(),
26744 multi_buffer,
26745 Some(project.clone()),
26746 window,
26747 cx,
26748 )
26749 });
26750
26751 let fake_language_server = fake_servers.next().await.unwrap();
26752
26753 buffer1.update(cx, |buffer, cx| buffer.edit([(0..0, "hello!")], None, cx));
26754
26755 let save = editor.update_in(cx, |editor, window, cx| {
26756 assert!(editor.is_dirty(cx));
26757
26758 editor.save(
26759 SaveOptions {
26760 format: true,
26761 autosave: true,
26762 },
26763 project,
26764 window,
26765 cx,
26766 )
26767 });
26768 let (start_edit_tx, start_edit_rx) = oneshot::channel();
26769 let (done_edit_tx, done_edit_rx) = oneshot::channel();
26770 let mut done_edit_rx = Some(done_edit_rx);
26771 let mut start_edit_tx = Some(start_edit_tx);
26772
26773 fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| {
26774 start_edit_tx.take().unwrap().send(()).unwrap();
26775 let done_edit_rx = done_edit_rx.take().unwrap();
26776 async move {
26777 done_edit_rx.await.unwrap();
26778 Ok(None)
26779 }
26780 });
26781
26782 start_edit_rx.await.unwrap();
26783 buffer2
26784 .update(cx, |buffer, cx| buffer.edit([(0..0, "world!")], None, cx))
26785 .unwrap();
26786
26787 done_edit_tx.send(()).unwrap();
26788
26789 save.await.unwrap();
26790 cx.update(|_, cx| assert!(editor.is_dirty(cx)));
26791}
26792
26793#[track_caller]
26794fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
26795 editor
26796 .all_inlays(cx)
26797 .into_iter()
26798 .filter_map(|inlay| inlay.get_color())
26799 .map(Rgba::from)
26800 .collect()
26801}
26802
26803#[gpui::test]
26804fn test_duplicate_line_up_on_last_line_without_newline(cx: &mut TestAppContext) {
26805 init_test(cx, |_| {});
26806
26807 let editor = cx.add_window(|window, cx| {
26808 let buffer = MultiBuffer::build_simple("line1\nline2", cx);
26809 build_editor(buffer, window, cx)
26810 });
26811
26812 editor
26813 .update(cx, |editor, window, cx| {
26814 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26815 s.select_display_ranges([
26816 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
26817 ])
26818 });
26819
26820 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
26821
26822 assert_eq!(
26823 editor.display_text(cx),
26824 "line1\nline2\nline2",
26825 "Duplicating last line upward should create duplicate above, not on same line"
26826 );
26827
26828 assert_eq!(
26829 editor
26830 .selections
26831 .display_ranges(&editor.display_snapshot(cx)),
26832 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)],
26833 "Selection should move to the duplicated line"
26834 );
26835 })
26836 .unwrap();
26837}
26838
26839#[gpui::test]
26840async fn test_copy_line_without_trailing_newline(cx: &mut TestAppContext) {
26841 init_test(cx, |_| {});
26842
26843 let mut cx = EditorTestContext::new(cx).await;
26844
26845 cx.set_state("line1\nline2ˇ");
26846
26847 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
26848
26849 let clipboard_text = cx
26850 .read_from_clipboard()
26851 .and_then(|item| item.text().as_deref().map(str::to_string));
26852
26853 assert_eq!(
26854 clipboard_text,
26855 Some("line2\n".to_string()),
26856 "Copying a line without trailing newline should include a newline"
26857 );
26858
26859 cx.set_state("line1\nˇ");
26860
26861 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
26862
26863 cx.assert_editor_state("line1\nline2\nˇ");
26864}
26865
26866#[gpui::test]
26867async fn test_end_of_editor_context(cx: &mut TestAppContext) {
26868 init_test(cx, |_| {});
26869
26870 let mut cx = EditorTestContext::new(cx).await;
26871
26872 cx.set_state("line1\nline2ˇ");
26873 cx.update_editor(|e, window, cx| {
26874 e.set_mode(EditorMode::SingleLine);
26875 assert!(e.key_context(window, cx).contains("end_of_input"));
26876 });
26877 cx.set_state("ˇline1\nline2");
26878 cx.update_editor(|e, window, cx| {
26879 assert!(!e.key_context(window, cx).contains("end_of_input"));
26880 });
26881 cx.set_state("line1ˇ\nline2");
26882 cx.update_editor(|e, window, cx| {
26883 assert!(!e.key_context(window, cx).contains("end_of_input"));
26884 });
26885}
26886
26887#[gpui::test]
26888async fn test_next_prev_reference(cx: &mut TestAppContext) {
26889 const CYCLE_POSITIONS: &[&'static str] = &[
26890 indoc! {"
26891 fn foo() {
26892 let ˇabc = 123;
26893 let x = abc + 1;
26894 let y = abc + 2;
26895 let z = abc + 2;
26896 }
26897 "},
26898 indoc! {"
26899 fn foo() {
26900 let abc = 123;
26901 let x = ˇabc + 1;
26902 let y = abc + 2;
26903 let z = abc + 2;
26904 }
26905 "},
26906 indoc! {"
26907 fn foo() {
26908 let abc = 123;
26909 let x = abc + 1;
26910 let y = ˇabc + 2;
26911 let z = abc + 2;
26912 }
26913 "},
26914 indoc! {"
26915 fn foo() {
26916 let abc = 123;
26917 let x = abc + 1;
26918 let y = abc + 2;
26919 let z = ˇabc + 2;
26920 }
26921 "},
26922 ];
26923
26924 init_test(cx, |_| {});
26925
26926 let mut cx = EditorLspTestContext::new_rust(
26927 lsp::ServerCapabilities {
26928 references_provider: Some(lsp::OneOf::Left(true)),
26929 ..Default::default()
26930 },
26931 cx,
26932 )
26933 .await;
26934
26935 // importantly, the cursor is in the middle
26936 cx.set_state(indoc! {"
26937 fn foo() {
26938 let aˇbc = 123;
26939 let x = abc + 1;
26940 let y = abc + 2;
26941 let z = abc + 2;
26942 }
26943 "});
26944
26945 let reference_ranges = [
26946 lsp::Position::new(1, 8),
26947 lsp::Position::new(2, 12),
26948 lsp::Position::new(3, 12),
26949 lsp::Position::new(4, 12),
26950 ]
26951 .map(|start| lsp::Range::new(start, lsp::Position::new(start.line, start.character + 3)));
26952
26953 cx.lsp
26954 .set_request_handler::<lsp::request::References, _, _>(move |params, _cx| async move {
26955 Ok(Some(
26956 reference_ranges
26957 .map(|range| lsp::Location {
26958 uri: params.text_document_position.text_document.uri.clone(),
26959 range,
26960 })
26961 .to_vec(),
26962 ))
26963 });
26964
26965 let _move = async |direction, count, cx: &mut EditorLspTestContext| {
26966 cx.update_editor(|editor, window, cx| {
26967 editor.go_to_reference_before_or_after_position(direction, count, window, cx)
26968 })
26969 .unwrap()
26970 .await
26971 .unwrap()
26972 };
26973
26974 _move(Direction::Next, 1, &mut cx).await;
26975 cx.assert_editor_state(CYCLE_POSITIONS[1]);
26976
26977 _move(Direction::Next, 1, &mut cx).await;
26978 cx.assert_editor_state(CYCLE_POSITIONS[2]);
26979
26980 _move(Direction::Next, 1, &mut cx).await;
26981 cx.assert_editor_state(CYCLE_POSITIONS[3]);
26982
26983 // loops back to the start
26984 _move(Direction::Next, 1, &mut cx).await;
26985 cx.assert_editor_state(CYCLE_POSITIONS[0]);
26986
26987 // loops back to the end
26988 _move(Direction::Prev, 1, &mut cx).await;
26989 cx.assert_editor_state(CYCLE_POSITIONS[3]);
26990
26991 _move(Direction::Prev, 1, &mut cx).await;
26992 cx.assert_editor_state(CYCLE_POSITIONS[2]);
26993
26994 _move(Direction::Prev, 1, &mut cx).await;
26995 cx.assert_editor_state(CYCLE_POSITIONS[1]);
26996
26997 _move(Direction::Prev, 1, &mut cx).await;
26998 cx.assert_editor_state(CYCLE_POSITIONS[0]);
26999
27000 _move(Direction::Next, 3, &mut cx).await;
27001 cx.assert_editor_state(CYCLE_POSITIONS[3]);
27002
27003 _move(Direction::Prev, 2, &mut cx).await;
27004 cx.assert_editor_state(CYCLE_POSITIONS[1]);
27005}