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_snippet_tabstop_navigation_with_placeholders(cx: &mut TestAppContext) {
11085 init_test(cx, |_| {});
11086
11087 fn assert_state(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
11088 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
11089 assert_eq!(editor.text(cx), expected_text);
11090 assert_eq!(
11091 editor
11092 .selections
11093 .ranges::<usize>(&editor.display_snapshot(cx)),
11094 selection_ranges
11095 );
11096 }
11097
11098 let (text, insertion_ranges) = marked_text_ranges(
11099 indoc! {"
11100 ˇ
11101 "},
11102 false,
11103 );
11104
11105 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
11106 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11107
11108 _ = editor.update_in(cx, |editor, window, cx| {
11109 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2; $3").unwrap();
11110
11111 editor
11112 .insert_snippet(&insertion_ranges, snippet, window, cx)
11113 .unwrap();
11114
11115 assert_state(
11116 editor,
11117 cx,
11118 indoc! {"
11119 type «» = ;•
11120 "},
11121 );
11122
11123 assert!(
11124 editor.context_menu_visible(),
11125 "Context menu should be visible for placeholder choices"
11126 );
11127
11128 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11129
11130 assert_state(
11131 editor,
11132 cx,
11133 indoc! {"
11134 type = «»;•
11135 "},
11136 );
11137
11138 assert!(
11139 !editor.context_menu_visible(),
11140 "Context menu should be hidden after moving to next tabstop"
11141 );
11142
11143 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11144
11145 assert_state(
11146 editor,
11147 cx,
11148 indoc! {"
11149 type = ; ˇ
11150 "},
11151 );
11152
11153 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11154
11155 assert_state(
11156 editor,
11157 cx,
11158 indoc! {"
11159 type = ; ˇ
11160 "},
11161 );
11162 });
11163
11164 _ = editor.update_in(cx, |editor, window, cx| {
11165 editor.select_all(&SelectAll, window, cx);
11166 editor.backspace(&Backspace, window, cx);
11167
11168 let snippet = Snippet::parse("fn ${1|,foo,bar|} = ${2:value}; $3").unwrap();
11169 let insertion_ranges = editor
11170 .selections
11171 .all(&editor.display_snapshot(cx))
11172 .iter()
11173 .map(|s| s.range())
11174 .collect::<Vec<_>>();
11175
11176 editor
11177 .insert_snippet(&insertion_ranges, snippet, window, cx)
11178 .unwrap();
11179
11180 assert_state(editor, cx, "fn «» = value;•");
11181
11182 assert!(
11183 editor.context_menu_visible(),
11184 "Context menu should be visible for placeholder choices"
11185 );
11186
11187 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11188
11189 assert_state(editor, cx, "fn = «valueˇ»;•");
11190
11191 editor.previous_snippet_tabstop(&PreviousSnippetTabstop, window, cx);
11192
11193 assert_state(editor, cx, "fn «» = value;•");
11194
11195 assert!(
11196 editor.context_menu_visible(),
11197 "Context menu should be visible again after returning to first tabstop"
11198 );
11199
11200 editor.previous_snippet_tabstop(&PreviousSnippetTabstop, window, cx);
11201
11202 assert_state(editor, cx, "fn «» = value;•");
11203 });
11204}
11205
11206#[gpui::test]
11207async fn test_snippets(cx: &mut TestAppContext) {
11208 init_test(cx, |_| {});
11209
11210 let mut cx = EditorTestContext::new(cx).await;
11211
11212 cx.set_state(indoc! {"
11213 a.ˇ b
11214 a.ˇ b
11215 a.ˇ b
11216 "});
11217
11218 cx.update_editor(|editor, window, cx| {
11219 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
11220 let insertion_ranges = editor
11221 .selections
11222 .all(&editor.display_snapshot(cx))
11223 .iter()
11224 .map(|s| s.range())
11225 .collect::<Vec<_>>();
11226 editor
11227 .insert_snippet(&insertion_ranges, snippet, window, cx)
11228 .unwrap();
11229 });
11230
11231 cx.assert_editor_state(indoc! {"
11232 a.f(«oneˇ», two, «threeˇ») b
11233 a.f(«oneˇ», two, «threeˇ») b
11234 a.f(«oneˇ», two, «threeˇ») b
11235 "});
11236
11237 // Can't move earlier than the first tab stop
11238 cx.update_editor(|editor, window, cx| {
11239 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
11240 });
11241 cx.assert_editor_state(indoc! {"
11242 a.f(«oneˇ», two, «threeˇ») b
11243 a.f(«oneˇ», two, «threeˇ») b
11244 a.f(«oneˇ», two, «threeˇ») b
11245 "});
11246
11247 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11248 cx.assert_editor_state(indoc! {"
11249 a.f(one, «twoˇ», three) b
11250 a.f(one, «twoˇ», three) b
11251 a.f(one, «twoˇ», three) b
11252 "});
11253
11254 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
11255 cx.assert_editor_state(indoc! {"
11256 a.f(«oneˇ», two, «threeˇ») b
11257 a.f(«oneˇ», two, «threeˇ») b
11258 a.f(«oneˇ», two, «threeˇ») b
11259 "});
11260
11261 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11262 cx.assert_editor_state(indoc! {"
11263 a.f(one, «twoˇ», three) b
11264 a.f(one, «twoˇ», three) b
11265 a.f(one, «twoˇ», three) b
11266 "});
11267 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11268 cx.assert_editor_state(indoc! {"
11269 a.f(one, two, three)ˇ b
11270 a.f(one, two, three)ˇ b
11271 a.f(one, two, three)ˇ b
11272 "});
11273
11274 // As soon as the last tab stop is reached, snippet state is gone
11275 cx.update_editor(|editor, window, cx| {
11276 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
11277 });
11278 cx.assert_editor_state(indoc! {"
11279 a.f(one, two, three)ˇ b
11280 a.f(one, two, three)ˇ b
11281 a.f(one, two, three)ˇ b
11282 "});
11283}
11284
11285#[gpui::test]
11286async fn test_snippet_indentation(cx: &mut TestAppContext) {
11287 init_test(cx, |_| {});
11288
11289 let mut cx = EditorTestContext::new(cx).await;
11290
11291 cx.update_editor(|editor, window, cx| {
11292 let snippet = Snippet::parse(indoc! {"
11293 /*
11294 * Multiline comment with leading indentation
11295 *
11296 * $1
11297 */
11298 $0"})
11299 .unwrap();
11300 let insertion_ranges = editor
11301 .selections
11302 .all(&editor.display_snapshot(cx))
11303 .iter()
11304 .map(|s| s.range())
11305 .collect::<Vec<_>>();
11306 editor
11307 .insert_snippet(&insertion_ranges, snippet, window, cx)
11308 .unwrap();
11309 });
11310
11311 cx.assert_editor_state(indoc! {"
11312 /*
11313 * Multiline comment with leading indentation
11314 *
11315 * ˇ
11316 */
11317 "});
11318
11319 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11320 cx.assert_editor_state(indoc! {"
11321 /*
11322 * Multiline comment with leading indentation
11323 *
11324 *•
11325 */
11326 ˇ"});
11327}
11328
11329#[gpui::test]
11330async fn test_document_format_during_save(cx: &mut TestAppContext) {
11331 init_test(cx, |_| {});
11332
11333 let fs = FakeFs::new(cx.executor());
11334 fs.insert_file(path!("/file.rs"), Default::default()).await;
11335
11336 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
11337
11338 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11339 language_registry.add(rust_lang());
11340 let mut fake_servers = language_registry.register_fake_lsp(
11341 "Rust",
11342 FakeLspAdapter {
11343 capabilities: lsp::ServerCapabilities {
11344 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11345 ..Default::default()
11346 },
11347 ..Default::default()
11348 },
11349 );
11350
11351 let buffer = project
11352 .update(cx, |project, cx| {
11353 project.open_local_buffer(path!("/file.rs"), cx)
11354 })
11355 .await
11356 .unwrap();
11357
11358 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11359 let (editor, cx) = cx.add_window_view(|window, cx| {
11360 build_editor_with_project(project.clone(), buffer, window, cx)
11361 });
11362 editor.update_in(cx, |editor, window, cx| {
11363 editor.set_text("one\ntwo\nthree\n", window, cx)
11364 });
11365 assert!(cx.read(|cx| editor.is_dirty(cx)));
11366
11367 cx.executor().start_waiting();
11368 let fake_server = fake_servers.next().await.unwrap();
11369
11370 {
11371 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11372 move |params, _| async move {
11373 assert_eq!(
11374 params.text_document.uri,
11375 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11376 );
11377 assert_eq!(params.options.tab_size, 4);
11378 Ok(Some(vec![lsp::TextEdit::new(
11379 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11380 ", ".to_string(),
11381 )]))
11382 },
11383 );
11384 let save = editor
11385 .update_in(cx, |editor, window, cx| {
11386 editor.save(
11387 SaveOptions {
11388 format: true,
11389 autosave: false,
11390 },
11391 project.clone(),
11392 window,
11393 cx,
11394 )
11395 })
11396 .unwrap();
11397 cx.executor().start_waiting();
11398 save.await;
11399
11400 assert_eq!(
11401 editor.update(cx, |editor, cx| editor.text(cx)),
11402 "one, two\nthree\n"
11403 );
11404 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11405 }
11406
11407 {
11408 editor.update_in(cx, |editor, window, cx| {
11409 editor.set_text("one\ntwo\nthree\n", window, cx)
11410 });
11411 assert!(cx.read(|cx| editor.is_dirty(cx)));
11412
11413 // Ensure we can still save even if formatting hangs.
11414 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11415 move |params, _| async move {
11416 assert_eq!(
11417 params.text_document.uri,
11418 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11419 );
11420 futures::future::pending::<()>().await;
11421 unreachable!()
11422 },
11423 );
11424 let save = editor
11425 .update_in(cx, |editor, window, cx| {
11426 editor.save(
11427 SaveOptions {
11428 format: true,
11429 autosave: false,
11430 },
11431 project.clone(),
11432 window,
11433 cx,
11434 )
11435 })
11436 .unwrap();
11437 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11438 cx.executor().start_waiting();
11439 save.await;
11440 assert_eq!(
11441 editor.update(cx, |editor, cx| editor.text(cx)),
11442 "one\ntwo\nthree\n"
11443 );
11444 }
11445
11446 // Set rust language override and assert overridden tabsize is sent to language server
11447 update_test_language_settings(cx, |settings| {
11448 settings.languages.0.insert(
11449 "Rust".into(),
11450 LanguageSettingsContent {
11451 tab_size: NonZeroU32::new(8),
11452 ..Default::default()
11453 },
11454 );
11455 });
11456
11457 {
11458 editor.update_in(cx, |editor, window, cx| {
11459 editor.set_text("somehting_new\n", window, cx)
11460 });
11461 assert!(cx.read(|cx| editor.is_dirty(cx)));
11462 let _formatting_request_signal = fake_server
11463 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
11464 assert_eq!(
11465 params.text_document.uri,
11466 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11467 );
11468 assert_eq!(params.options.tab_size, 8);
11469 Ok(Some(vec![]))
11470 });
11471 let save = editor
11472 .update_in(cx, |editor, window, cx| {
11473 editor.save(
11474 SaveOptions {
11475 format: true,
11476 autosave: false,
11477 },
11478 project.clone(),
11479 window,
11480 cx,
11481 )
11482 })
11483 .unwrap();
11484 cx.executor().start_waiting();
11485 save.await;
11486 }
11487}
11488
11489#[gpui::test]
11490async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
11491 init_test(cx, |settings| {
11492 settings.defaults.ensure_final_newline_on_save = Some(false);
11493 });
11494
11495 let fs = FakeFs::new(cx.executor());
11496 fs.insert_file(path!("/file.txt"), "foo".into()).await;
11497
11498 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
11499
11500 let buffer = project
11501 .update(cx, |project, cx| {
11502 project.open_local_buffer(path!("/file.txt"), cx)
11503 })
11504 .await
11505 .unwrap();
11506
11507 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11508 let (editor, cx) = cx.add_window_view(|window, cx| {
11509 build_editor_with_project(project.clone(), buffer, window, cx)
11510 });
11511 editor.update_in(cx, |editor, window, cx| {
11512 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
11513 s.select_ranges([0..0])
11514 });
11515 });
11516 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11517
11518 editor.update_in(cx, |editor, window, cx| {
11519 editor.handle_input("\n", window, cx)
11520 });
11521 cx.run_until_parked();
11522 save(&editor, &project, cx).await;
11523 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11524
11525 editor.update_in(cx, |editor, window, cx| {
11526 editor.undo(&Default::default(), window, cx);
11527 });
11528 save(&editor, &project, cx).await;
11529 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11530
11531 editor.update_in(cx, |editor, window, cx| {
11532 editor.redo(&Default::default(), window, cx);
11533 });
11534 cx.run_until_parked();
11535 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11536
11537 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
11538 let save = editor
11539 .update_in(cx, |editor, window, cx| {
11540 editor.save(
11541 SaveOptions {
11542 format: true,
11543 autosave: false,
11544 },
11545 project.clone(),
11546 window,
11547 cx,
11548 )
11549 })
11550 .unwrap();
11551 cx.executor().start_waiting();
11552 save.await;
11553 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11554 }
11555}
11556
11557#[gpui::test]
11558async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
11559 init_test(cx, |_| {});
11560
11561 let cols = 4;
11562 let rows = 10;
11563 let sample_text_1 = sample_text(rows, cols, 'a');
11564 assert_eq!(
11565 sample_text_1,
11566 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
11567 );
11568 let sample_text_2 = sample_text(rows, cols, 'l');
11569 assert_eq!(
11570 sample_text_2,
11571 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
11572 );
11573 let sample_text_3 = sample_text(rows, cols, 'v');
11574 assert_eq!(
11575 sample_text_3,
11576 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
11577 );
11578
11579 let fs = FakeFs::new(cx.executor());
11580 fs.insert_tree(
11581 path!("/a"),
11582 json!({
11583 "main.rs": sample_text_1,
11584 "other.rs": sample_text_2,
11585 "lib.rs": sample_text_3,
11586 }),
11587 )
11588 .await;
11589
11590 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11591 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11592 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11593
11594 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11595 language_registry.add(rust_lang());
11596 let mut fake_servers = language_registry.register_fake_lsp(
11597 "Rust",
11598 FakeLspAdapter {
11599 capabilities: lsp::ServerCapabilities {
11600 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11601 ..Default::default()
11602 },
11603 ..Default::default()
11604 },
11605 );
11606
11607 let worktree = project.update(cx, |project, cx| {
11608 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
11609 assert_eq!(worktrees.len(), 1);
11610 worktrees.pop().unwrap()
11611 });
11612 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11613
11614 let buffer_1 = project
11615 .update(cx, |project, cx| {
11616 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
11617 })
11618 .await
11619 .unwrap();
11620 let buffer_2 = project
11621 .update(cx, |project, cx| {
11622 project.open_buffer((worktree_id, rel_path("other.rs")), cx)
11623 })
11624 .await
11625 .unwrap();
11626 let buffer_3 = project
11627 .update(cx, |project, cx| {
11628 project.open_buffer((worktree_id, rel_path("lib.rs")), cx)
11629 })
11630 .await
11631 .unwrap();
11632
11633 let multi_buffer = cx.new(|cx| {
11634 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11635 multi_buffer.push_excerpts(
11636 buffer_1.clone(),
11637 [
11638 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11639 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11640 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11641 ],
11642 cx,
11643 );
11644 multi_buffer.push_excerpts(
11645 buffer_2.clone(),
11646 [
11647 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11648 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11649 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11650 ],
11651 cx,
11652 );
11653 multi_buffer.push_excerpts(
11654 buffer_3.clone(),
11655 [
11656 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11657 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11658 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11659 ],
11660 cx,
11661 );
11662 multi_buffer
11663 });
11664 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
11665 Editor::new(
11666 EditorMode::full(),
11667 multi_buffer,
11668 Some(project.clone()),
11669 window,
11670 cx,
11671 )
11672 });
11673
11674 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11675 editor.change_selections(
11676 SelectionEffects::scroll(Autoscroll::Next),
11677 window,
11678 cx,
11679 |s| s.select_ranges(Some(1..2)),
11680 );
11681 editor.insert("|one|two|three|", window, cx);
11682 });
11683 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11684 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11685 editor.change_selections(
11686 SelectionEffects::scroll(Autoscroll::Next),
11687 window,
11688 cx,
11689 |s| s.select_ranges(Some(60..70)),
11690 );
11691 editor.insert("|four|five|six|", window, cx);
11692 });
11693 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11694
11695 // First two buffers should be edited, but not the third one.
11696 assert_eq!(
11697 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11698 "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}",
11699 );
11700 buffer_1.update(cx, |buffer, _| {
11701 assert!(buffer.is_dirty());
11702 assert_eq!(
11703 buffer.text(),
11704 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
11705 )
11706 });
11707 buffer_2.update(cx, |buffer, _| {
11708 assert!(buffer.is_dirty());
11709 assert_eq!(
11710 buffer.text(),
11711 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
11712 )
11713 });
11714 buffer_3.update(cx, |buffer, _| {
11715 assert!(!buffer.is_dirty());
11716 assert_eq!(buffer.text(), sample_text_3,)
11717 });
11718 cx.executor().run_until_parked();
11719
11720 cx.executor().start_waiting();
11721 let save = multi_buffer_editor
11722 .update_in(cx, |editor, window, cx| {
11723 editor.save(
11724 SaveOptions {
11725 format: true,
11726 autosave: false,
11727 },
11728 project.clone(),
11729 window,
11730 cx,
11731 )
11732 })
11733 .unwrap();
11734
11735 let fake_server = fake_servers.next().await.unwrap();
11736 fake_server
11737 .server
11738 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
11739 Ok(Some(vec![lsp::TextEdit::new(
11740 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11741 format!("[{} formatted]", params.text_document.uri),
11742 )]))
11743 })
11744 .detach();
11745 save.await;
11746
11747 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
11748 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
11749 assert_eq!(
11750 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11751 uri!(
11752 "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}"
11753 ),
11754 );
11755 buffer_1.update(cx, |buffer, _| {
11756 assert!(!buffer.is_dirty());
11757 assert_eq!(
11758 buffer.text(),
11759 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
11760 )
11761 });
11762 buffer_2.update(cx, |buffer, _| {
11763 assert!(!buffer.is_dirty());
11764 assert_eq!(
11765 buffer.text(),
11766 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
11767 )
11768 });
11769 buffer_3.update(cx, |buffer, _| {
11770 assert!(!buffer.is_dirty());
11771 assert_eq!(buffer.text(), sample_text_3,)
11772 });
11773}
11774
11775#[gpui::test]
11776async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
11777 init_test(cx, |_| {});
11778
11779 let fs = FakeFs::new(cx.executor());
11780 fs.insert_tree(
11781 path!("/dir"),
11782 json!({
11783 "file1.rs": "fn main() { println!(\"hello\"); }",
11784 "file2.rs": "fn test() { println!(\"test\"); }",
11785 "file3.rs": "fn other() { println!(\"other\"); }\n",
11786 }),
11787 )
11788 .await;
11789
11790 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
11791 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11792 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11793
11794 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11795 language_registry.add(rust_lang());
11796
11797 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
11798 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11799
11800 // Open three buffers
11801 let buffer_1 = project
11802 .update(cx, |project, cx| {
11803 project.open_buffer((worktree_id, rel_path("file1.rs")), cx)
11804 })
11805 .await
11806 .unwrap();
11807 let buffer_2 = project
11808 .update(cx, |project, cx| {
11809 project.open_buffer((worktree_id, rel_path("file2.rs")), cx)
11810 })
11811 .await
11812 .unwrap();
11813 let buffer_3 = project
11814 .update(cx, |project, cx| {
11815 project.open_buffer((worktree_id, rel_path("file3.rs")), cx)
11816 })
11817 .await
11818 .unwrap();
11819
11820 // Create a multi-buffer with all three buffers
11821 let multi_buffer = cx.new(|cx| {
11822 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11823 multi_buffer.push_excerpts(
11824 buffer_1.clone(),
11825 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11826 cx,
11827 );
11828 multi_buffer.push_excerpts(
11829 buffer_2.clone(),
11830 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11831 cx,
11832 );
11833 multi_buffer.push_excerpts(
11834 buffer_3.clone(),
11835 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11836 cx,
11837 );
11838 multi_buffer
11839 });
11840
11841 let editor = cx.new_window_entity(|window, cx| {
11842 Editor::new(
11843 EditorMode::full(),
11844 multi_buffer,
11845 Some(project.clone()),
11846 window,
11847 cx,
11848 )
11849 });
11850
11851 // Edit only the first buffer
11852 editor.update_in(cx, |editor, window, cx| {
11853 editor.change_selections(
11854 SelectionEffects::scroll(Autoscroll::Next),
11855 window,
11856 cx,
11857 |s| s.select_ranges(Some(10..10)),
11858 );
11859 editor.insert("// edited", window, cx);
11860 });
11861
11862 // Verify that only buffer 1 is dirty
11863 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
11864 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11865 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11866
11867 // Get write counts after file creation (files were created with initial content)
11868 // We expect each file to have been written once during creation
11869 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
11870 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
11871 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
11872
11873 // Perform autosave
11874 let save_task = editor.update_in(cx, |editor, window, cx| {
11875 editor.save(
11876 SaveOptions {
11877 format: true,
11878 autosave: true,
11879 },
11880 project.clone(),
11881 window,
11882 cx,
11883 )
11884 });
11885 save_task.await.unwrap();
11886
11887 // Only the dirty buffer should have been saved
11888 assert_eq!(
11889 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11890 1,
11891 "Buffer 1 was dirty, so it should have been written once during autosave"
11892 );
11893 assert_eq!(
11894 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11895 0,
11896 "Buffer 2 was clean, so it should not have been written during autosave"
11897 );
11898 assert_eq!(
11899 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11900 0,
11901 "Buffer 3 was clean, so it should not have been written during autosave"
11902 );
11903
11904 // Verify buffer states after autosave
11905 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11906 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11907 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11908
11909 // Now perform a manual save (format = true)
11910 let save_task = editor.update_in(cx, |editor, window, cx| {
11911 editor.save(
11912 SaveOptions {
11913 format: true,
11914 autosave: false,
11915 },
11916 project.clone(),
11917 window,
11918 cx,
11919 )
11920 });
11921 save_task.await.unwrap();
11922
11923 // During manual save, clean buffers don't get written to disk
11924 // They just get did_save called for language server notifications
11925 assert_eq!(
11926 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11927 1,
11928 "Buffer 1 should only have been written once total (during autosave, not manual save)"
11929 );
11930 assert_eq!(
11931 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11932 0,
11933 "Buffer 2 should not have been written at all"
11934 );
11935 assert_eq!(
11936 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11937 0,
11938 "Buffer 3 should not have been written at all"
11939 );
11940}
11941
11942async fn setup_range_format_test(
11943 cx: &mut TestAppContext,
11944) -> (
11945 Entity<Project>,
11946 Entity<Editor>,
11947 &mut gpui::VisualTestContext,
11948 lsp::FakeLanguageServer,
11949) {
11950 init_test(cx, |_| {});
11951
11952 let fs = FakeFs::new(cx.executor());
11953 fs.insert_file(path!("/file.rs"), Default::default()).await;
11954
11955 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11956
11957 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11958 language_registry.add(rust_lang());
11959 let mut fake_servers = language_registry.register_fake_lsp(
11960 "Rust",
11961 FakeLspAdapter {
11962 capabilities: lsp::ServerCapabilities {
11963 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
11964 ..lsp::ServerCapabilities::default()
11965 },
11966 ..FakeLspAdapter::default()
11967 },
11968 );
11969
11970 let buffer = project
11971 .update(cx, |project, cx| {
11972 project.open_local_buffer(path!("/file.rs"), cx)
11973 })
11974 .await
11975 .unwrap();
11976
11977 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11978 let (editor, cx) = cx.add_window_view(|window, cx| {
11979 build_editor_with_project(project.clone(), buffer, window, cx)
11980 });
11981
11982 cx.executor().start_waiting();
11983 let fake_server = fake_servers.next().await.unwrap();
11984
11985 (project, editor, cx, fake_server)
11986}
11987
11988#[gpui::test]
11989async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
11990 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11991
11992 editor.update_in(cx, |editor, window, cx| {
11993 editor.set_text("one\ntwo\nthree\n", window, cx)
11994 });
11995 assert!(cx.read(|cx| editor.is_dirty(cx)));
11996
11997 let save = editor
11998 .update_in(cx, |editor, window, cx| {
11999 editor.save(
12000 SaveOptions {
12001 format: true,
12002 autosave: false,
12003 },
12004 project.clone(),
12005 window,
12006 cx,
12007 )
12008 })
12009 .unwrap();
12010 fake_server
12011 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
12012 assert_eq!(
12013 params.text_document.uri,
12014 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12015 );
12016 assert_eq!(params.options.tab_size, 4);
12017 Ok(Some(vec![lsp::TextEdit::new(
12018 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12019 ", ".to_string(),
12020 )]))
12021 })
12022 .next()
12023 .await;
12024 cx.executor().start_waiting();
12025 save.await;
12026 assert_eq!(
12027 editor.update(cx, |editor, cx| editor.text(cx)),
12028 "one, two\nthree\n"
12029 );
12030 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12031}
12032
12033#[gpui::test]
12034async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
12035 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12036
12037 editor.update_in(cx, |editor, window, cx| {
12038 editor.set_text("one\ntwo\nthree\n", window, cx)
12039 });
12040 assert!(cx.read(|cx| editor.is_dirty(cx)));
12041
12042 // Test that save still works when formatting hangs
12043 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
12044 move |params, _| async move {
12045 assert_eq!(
12046 params.text_document.uri,
12047 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12048 );
12049 futures::future::pending::<()>().await;
12050 unreachable!()
12051 },
12052 );
12053 let save = editor
12054 .update_in(cx, |editor, window, cx| {
12055 editor.save(
12056 SaveOptions {
12057 format: true,
12058 autosave: false,
12059 },
12060 project.clone(),
12061 window,
12062 cx,
12063 )
12064 })
12065 .unwrap();
12066 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
12067 cx.executor().start_waiting();
12068 save.await;
12069 assert_eq!(
12070 editor.update(cx, |editor, cx| editor.text(cx)),
12071 "one\ntwo\nthree\n"
12072 );
12073 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12074}
12075
12076#[gpui::test]
12077async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
12078 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12079
12080 // Buffer starts clean, no formatting should be requested
12081 let save = editor
12082 .update_in(cx, |editor, window, cx| {
12083 editor.save(
12084 SaveOptions {
12085 format: false,
12086 autosave: false,
12087 },
12088 project.clone(),
12089 window,
12090 cx,
12091 )
12092 })
12093 .unwrap();
12094 let _pending_format_request = fake_server
12095 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
12096 panic!("Should not be invoked");
12097 })
12098 .next();
12099 cx.executor().start_waiting();
12100 save.await;
12101 cx.run_until_parked();
12102}
12103
12104#[gpui::test]
12105async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
12106 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12107
12108 // Set Rust language override and assert overridden tabsize is sent to language server
12109 update_test_language_settings(cx, |settings| {
12110 settings.languages.0.insert(
12111 "Rust".into(),
12112 LanguageSettingsContent {
12113 tab_size: NonZeroU32::new(8),
12114 ..Default::default()
12115 },
12116 );
12117 });
12118
12119 editor.update_in(cx, |editor, window, cx| {
12120 editor.set_text("something_new\n", window, cx)
12121 });
12122 assert!(cx.read(|cx| editor.is_dirty(cx)));
12123 let save = editor
12124 .update_in(cx, |editor, window, cx| {
12125 editor.save(
12126 SaveOptions {
12127 format: true,
12128 autosave: false,
12129 },
12130 project.clone(),
12131 window,
12132 cx,
12133 )
12134 })
12135 .unwrap();
12136 fake_server
12137 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
12138 assert_eq!(
12139 params.text_document.uri,
12140 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12141 );
12142 assert_eq!(params.options.tab_size, 8);
12143 Ok(Some(Vec::new()))
12144 })
12145 .next()
12146 .await;
12147 save.await;
12148}
12149
12150#[gpui::test]
12151async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
12152 init_test(cx, |settings| {
12153 settings.defaults.formatter = Some(FormatterList::Single(Formatter::LanguageServer(
12154 settings::LanguageServerFormatterSpecifier::Current,
12155 )))
12156 });
12157
12158 let fs = FakeFs::new(cx.executor());
12159 fs.insert_file(path!("/file.rs"), Default::default()).await;
12160
12161 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12162
12163 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12164 language_registry.add(Arc::new(Language::new(
12165 LanguageConfig {
12166 name: "Rust".into(),
12167 matcher: LanguageMatcher {
12168 path_suffixes: vec!["rs".to_string()],
12169 ..Default::default()
12170 },
12171 ..LanguageConfig::default()
12172 },
12173 Some(tree_sitter_rust::LANGUAGE.into()),
12174 )));
12175 update_test_language_settings(cx, |settings| {
12176 // Enable Prettier formatting for the same buffer, and ensure
12177 // LSP is called instead of Prettier.
12178 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
12179 });
12180 let mut fake_servers = language_registry.register_fake_lsp(
12181 "Rust",
12182 FakeLspAdapter {
12183 capabilities: lsp::ServerCapabilities {
12184 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12185 ..Default::default()
12186 },
12187 ..Default::default()
12188 },
12189 );
12190
12191 let buffer = project
12192 .update(cx, |project, cx| {
12193 project.open_local_buffer(path!("/file.rs"), cx)
12194 })
12195 .await
12196 .unwrap();
12197
12198 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12199 let (editor, cx) = cx.add_window_view(|window, cx| {
12200 build_editor_with_project(project.clone(), buffer, window, cx)
12201 });
12202 editor.update_in(cx, |editor, window, cx| {
12203 editor.set_text("one\ntwo\nthree\n", window, cx)
12204 });
12205
12206 cx.executor().start_waiting();
12207 let fake_server = fake_servers.next().await.unwrap();
12208
12209 let format = editor
12210 .update_in(cx, |editor, window, cx| {
12211 editor.perform_format(
12212 project.clone(),
12213 FormatTrigger::Manual,
12214 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12215 window,
12216 cx,
12217 )
12218 })
12219 .unwrap();
12220 fake_server
12221 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
12222 assert_eq!(
12223 params.text_document.uri,
12224 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12225 );
12226 assert_eq!(params.options.tab_size, 4);
12227 Ok(Some(vec![lsp::TextEdit::new(
12228 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12229 ", ".to_string(),
12230 )]))
12231 })
12232 .next()
12233 .await;
12234 cx.executor().start_waiting();
12235 format.await;
12236 assert_eq!(
12237 editor.update(cx, |editor, cx| editor.text(cx)),
12238 "one, two\nthree\n"
12239 );
12240
12241 editor.update_in(cx, |editor, window, cx| {
12242 editor.set_text("one\ntwo\nthree\n", window, cx)
12243 });
12244 // Ensure we don't lock if formatting hangs.
12245 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12246 move |params, _| async move {
12247 assert_eq!(
12248 params.text_document.uri,
12249 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12250 );
12251 futures::future::pending::<()>().await;
12252 unreachable!()
12253 },
12254 );
12255 let format = editor
12256 .update_in(cx, |editor, window, cx| {
12257 editor.perform_format(
12258 project,
12259 FormatTrigger::Manual,
12260 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12261 window,
12262 cx,
12263 )
12264 })
12265 .unwrap();
12266 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
12267 cx.executor().start_waiting();
12268 format.await;
12269 assert_eq!(
12270 editor.update(cx, |editor, cx| editor.text(cx)),
12271 "one\ntwo\nthree\n"
12272 );
12273}
12274
12275#[gpui::test]
12276async fn test_multiple_formatters(cx: &mut TestAppContext) {
12277 init_test(cx, |settings| {
12278 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
12279 settings.defaults.formatter = Some(FormatterList::Vec(vec![
12280 Formatter::LanguageServer(settings::LanguageServerFormatterSpecifier::Current),
12281 Formatter::CodeAction("code-action-1".into()),
12282 Formatter::CodeAction("code-action-2".into()),
12283 ]))
12284 });
12285
12286 let fs = FakeFs::new(cx.executor());
12287 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
12288 .await;
12289
12290 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12291 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12292 language_registry.add(rust_lang());
12293
12294 let mut fake_servers = language_registry.register_fake_lsp(
12295 "Rust",
12296 FakeLspAdapter {
12297 capabilities: lsp::ServerCapabilities {
12298 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12299 execute_command_provider: Some(lsp::ExecuteCommandOptions {
12300 commands: vec!["the-command-for-code-action-1".into()],
12301 ..Default::default()
12302 }),
12303 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12304 ..Default::default()
12305 },
12306 ..Default::default()
12307 },
12308 );
12309
12310 let buffer = project
12311 .update(cx, |project, cx| {
12312 project.open_local_buffer(path!("/file.rs"), cx)
12313 })
12314 .await
12315 .unwrap();
12316
12317 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12318 let (editor, cx) = cx.add_window_view(|window, cx| {
12319 build_editor_with_project(project.clone(), buffer, window, cx)
12320 });
12321
12322 cx.executor().start_waiting();
12323
12324 let fake_server = fake_servers.next().await.unwrap();
12325 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12326 move |_params, _| async move {
12327 Ok(Some(vec![lsp::TextEdit::new(
12328 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12329 "applied-formatting\n".to_string(),
12330 )]))
12331 },
12332 );
12333 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12334 move |params, _| async move {
12335 let requested_code_actions = params.context.only.expect("Expected code action request");
12336 assert_eq!(requested_code_actions.len(), 1);
12337
12338 let uri = lsp::Uri::from_file_path(path!("/file.rs")).unwrap();
12339 let code_action = match requested_code_actions[0].as_str() {
12340 "code-action-1" => lsp::CodeAction {
12341 kind: Some("code-action-1".into()),
12342 edit: Some(lsp::WorkspaceEdit::new(
12343 [(
12344 uri,
12345 vec![lsp::TextEdit::new(
12346 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12347 "applied-code-action-1-edit\n".to_string(),
12348 )],
12349 )]
12350 .into_iter()
12351 .collect(),
12352 )),
12353 command: Some(lsp::Command {
12354 command: "the-command-for-code-action-1".into(),
12355 ..Default::default()
12356 }),
12357 ..Default::default()
12358 },
12359 "code-action-2" => lsp::CodeAction {
12360 kind: Some("code-action-2".into()),
12361 edit: Some(lsp::WorkspaceEdit::new(
12362 [(
12363 uri,
12364 vec![lsp::TextEdit::new(
12365 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12366 "applied-code-action-2-edit\n".to_string(),
12367 )],
12368 )]
12369 .into_iter()
12370 .collect(),
12371 )),
12372 ..Default::default()
12373 },
12374 req => panic!("Unexpected code action request: {:?}", req),
12375 };
12376 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12377 code_action,
12378 )]))
12379 },
12380 );
12381
12382 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
12383 move |params, _| async move { Ok(params) }
12384 });
12385
12386 let command_lock = Arc::new(futures::lock::Mutex::new(()));
12387 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
12388 let fake = fake_server.clone();
12389 let lock = command_lock.clone();
12390 move |params, _| {
12391 assert_eq!(params.command, "the-command-for-code-action-1");
12392 let fake = fake.clone();
12393 let lock = lock.clone();
12394 async move {
12395 lock.lock().await;
12396 fake.server
12397 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
12398 label: None,
12399 edit: lsp::WorkspaceEdit {
12400 changes: Some(
12401 [(
12402 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
12403 vec![lsp::TextEdit {
12404 range: lsp::Range::new(
12405 lsp::Position::new(0, 0),
12406 lsp::Position::new(0, 0),
12407 ),
12408 new_text: "applied-code-action-1-command\n".into(),
12409 }],
12410 )]
12411 .into_iter()
12412 .collect(),
12413 ),
12414 ..Default::default()
12415 },
12416 })
12417 .await
12418 .into_response()
12419 .unwrap();
12420 Ok(Some(json!(null)))
12421 }
12422 }
12423 });
12424
12425 cx.executor().start_waiting();
12426 editor
12427 .update_in(cx, |editor, window, cx| {
12428 editor.perform_format(
12429 project.clone(),
12430 FormatTrigger::Manual,
12431 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12432 window,
12433 cx,
12434 )
12435 })
12436 .unwrap()
12437 .await;
12438 editor.update(cx, |editor, cx| {
12439 assert_eq!(
12440 editor.text(cx),
12441 r#"
12442 applied-code-action-2-edit
12443 applied-code-action-1-command
12444 applied-code-action-1-edit
12445 applied-formatting
12446 one
12447 two
12448 three
12449 "#
12450 .unindent()
12451 );
12452 });
12453
12454 editor.update_in(cx, |editor, window, cx| {
12455 editor.undo(&Default::default(), window, cx);
12456 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12457 });
12458
12459 // Perform a manual edit while waiting for an LSP command
12460 // that's being run as part of a formatting code action.
12461 let lock_guard = command_lock.lock().await;
12462 let format = editor
12463 .update_in(cx, |editor, window, cx| {
12464 editor.perform_format(
12465 project.clone(),
12466 FormatTrigger::Manual,
12467 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12468 window,
12469 cx,
12470 )
12471 })
12472 .unwrap();
12473 cx.run_until_parked();
12474 editor.update(cx, |editor, cx| {
12475 assert_eq!(
12476 editor.text(cx),
12477 r#"
12478 applied-code-action-1-edit
12479 applied-formatting
12480 one
12481 two
12482 three
12483 "#
12484 .unindent()
12485 );
12486
12487 editor.buffer.update(cx, |buffer, cx| {
12488 let ix = buffer.len(cx);
12489 buffer.edit([(ix..ix, "edited\n")], None, cx);
12490 });
12491 });
12492
12493 // Allow the LSP command to proceed. Because the buffer was edited,
12494 // the second code action will not be run.
12495 drop(lock_guard);
12496 format.await;
12497 editor.update_in(cx, |editor, window, cx| {
12498 assert_eq!(
12499 editor.text(cx),
12500 r#"
12501 applied-code-action-1-command
12502 applied-code-action-1-edit
12503 applied-formatting
12504 one
12505 two
12506 three
12507 edited
12508 "#
12509 .unindent()
12510 );
12511
12512 // The manual edit is undone first, because it is the last thing the user did
12513 // (even though the command completed afterwards).
12514 editor.undo(&Default::default(), window, cx);
12515 assert_eq!(
12516 editor.text(cx),
12517 r#"
12518 applied-code-action-1-command
12519 applied-code-action-1-edit
12520 applied-formatting
12521 one
12522 two
12523 three
12524 "#
12525 .unindent()
12526 );
12527
12528 // All the formatting (including the command, which completed after the manual edit)
12529 // is undone together.
12530 editor.undo(&Default::default(), window, cx);
12531 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12532 });
12533}
12534
12535#[gpui::test]
12536async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
12537 init_test(cx, |settings| {
12538 settings.defaults.formatter = Some(FormatterList::Vec(vec![Formatter::LanguageServer(
12539 settings::LanguageServerFormatterSpecifier::Current,
12540 )]))
12541 });
12542
12543 let fs = FakeFs::new(cx.executor());
12544 fs.insert_file(path!("/file.ts"), Default::default()).await;
12545
12546 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12547
12548 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12549 language_registry.add(Arc::new(Language::new(
12550 LanguageConfig {
12551 name: "TypeScript".into(),
12552 matcher: LanguageMatcher {
12553 path_suffixes: vec!["ts".to_string()],
12554 ..Default::default()
12555 },
12556 ..LanguageConfig::default()
12557 },
12558 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12559 )));
12560 update_test_language_settings(cx, |settings| {
12561 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
12562 });
12563 let mut fake_servers = language_registry.register_fake_lsp(
12564 "TypeScript",
12565 FakeLspAdapter {
12566 capabilities: lsp::ServerCapabilities {
12567 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12568 ..Default::default()
12569 },
12570 ..Default::default()
12571 },
12572 );
12573
12574 let buffer = project
12575 .update(cx, |project, cx| {
12576 project.open_local_buffer(path!("/file.ts"), cx)
12577 })
12578 .await
12579 .unwrap();
12580
12581 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12582 let (editor, cx) = cx.add_window_view(|window, cx| {
12583 build_editor_with_project(project.clone(), buffer, window, cx)
12584 });
12585 editor.update_in(cx, |editor, window, cx| {
12586 editor.set_text(
12587 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12588 window,
12589 cx,
12590 )
12591 });
12592
12593 cx.executor().start_waiting();
12594 let fake_server = fake_servers.next().await.unwrap();
12595
12596 let format = editor
12597 .update_in(cx, |editor, window, cx| {
12598 editor.perform_code_action_kind(
12599 project.clone(),
12600 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12601 window,
12602 cx,
12603 )
12604 })
12605 .unwrap();
12606 fake_server
12607 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
12608 assert_eq!(
12609 params.text_document.uri,
12610 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12611 );
12612 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12613 lsp::CodeAction {
12614 title: "Organize Imports".to_string(),
12615 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
12616 edit: Some(lsp::WorkspaceEdit {
12617 changes: Some(
12618 [(
12619 params.text_document.uri.clone(),
12620 vec![lsp::TextEdit::new(
12621 lsp::Range::new(
12622 lsp::Position::new(1, 0),
12623 lsp::Position::new(2, 0),
12624 ),
12625 "".to_string(),
12626 )],
12627 )]
12628 .into_iter()
12629 .collect(),
12630 ),
12631 ..Default::default()
12632 }),
12633 ..Default::default()
12634 },
12635 )]))
12636 })
12637 .next()
12638 .await;
12639 cx.executor().start_waiting();
12640 format.await;
12641 assert_eq!(
12642 editor.update(cx, |editor, cx| editor.text(cx)),
12643 "import { a } from 'module';\n\nconst x = a;\n"
12644 );
12645
12646 editor.update_in(cx, |editor, window, cx| {
12647 editor.set_text(
12648 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12649 window,
12650 cx,
12651 )
12652 });
12653 // Ensure we don't lock if code action hangs.
12654 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12655 move |params, _| async move {
12656 assert_eq!(
12657 params.text_document.uri,
12658 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12659 );
12660 futures::future::pending::<()>().await;
12661 unreachable!()
12662 },
12663 );
12664 let format = editor
12665 .update_in(cx, |editor, window, cx| {
12666 editor.perform_code_action_kind(
12667 project,
12668 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12669 window,
12670 cx,
12671 )
12672 })
12673 .unwrap();
12674 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
12675 cx.executor().start_waiting();
12676 format.await;
12677 assert_eq!(
12678 editor.update(cx, |editor, cx| editor.text(cx)),
12679 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
12680 );
12681}
12682
12683#[gpui::test]
12684async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
12685 init_test(cx, |_| {});
12686
12687 let mut cx = EditorLspTestContext::new_rust(
12688 lsp::ServerCapabilities {
12689 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12690 ..Default::default()
12691 },
12692 cx,
12693 )
12694 .await;
12695
12696 cx.set_state(indoc! {"
12697 one.twoˇ
12698 "});
12699
12700 // The format request takes a long time. When it completes, it inserts
12701 // a newline and an indent before the `.`
12702 cx.lsp
12703 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
12704 let executor = cx.background_executor().clone();
12705 async move {
12706 executor.timer(Duration::from_millis(100)).await;
12707 Ok(Some(vec![lsp::TextEdit {
12708 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
12709 new_text: "\n ".into(),
12710 }]))
12711 }
12712 });
12713
12714 // Submit a format request.
12715 let format_1 = cx
12716 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12717 .unwrap();
12718 cx.executor().run_until_parked();
12719
12720 // Submit a second format request.
12721 let format_2 = cx
12722 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12723 .unwrap();
12724 cx.executor().run_until_parked();
12725
12726 // Wait for both format requests to complete
12727 cx.executor().advance_clock(Duration::from_millis(200));
12728 cx.executor().start_waiting();
12729 format_1.await.unwrap();
12730 cx.executor().start_waiting();
12731 format_2.await.unwrap();
12732
12733 // The formatting edits only happens once.
12734 cx.assert_editor_state(indoc! {"
12735 one
12736 .twoˇ
12737 "});
12738}
12739
12740#[gpui::test]
12741async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
12742 init_test(cx, |settings| {
12743 settings.defaults.formatter = Some(FormatterList::default())
12744 });
12745
12746 let mut cx = EditorLspTestContext::new_rust(
12747 lsp::ServerCapabilities {
12748 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12749 ..Default::default()
12750 },
12751 cx,
12752 )
12753 .await;
12754
12755 // Record which buffer changes have been sent to the language server
12756 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
12757 cx.lsp
12758 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
12759 let buffer_changes = buffer_changes.clone();
12760 move |params, _| {
12761 buffer_changes.lock().extend(
12762 params
12763 .content_changes
12764 .into_iter()
12765 .map(|e| (e.range.unwrap(), e.text)),
12766 );
12767 }
12768 });
12769 // Handle formatting requests to the language server.
12770 cx.lsp
12771 .set_request_handler::<lsp::request::Formatting, _, _>({
12772 let buffer_changes = buffer_changes.clone();
12773 move |_, _| {
12774 let buffer_changes = buffer_changes.clone();
12775 // Insert blank lines between each line of the buffer.
12776 async move {
12777 // When formatting is requested, trailing whitespace has already been stripped,
12778 // and the trailing newline has already been added.
12779 assert_eq!(
12780 &buffer_changes.lock()[1..],
12781 &[
12782 (
12783 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
12784 "".into()
12785 ),
12786 (
12787 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
12788 "".into()
12789 ),
12790 (
12791 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
12792 "\n".into()
12793 ),
12794 ]
12795 );
12796
12797 Ok(Some(vec![
12798 lsp::TextEdit {
12799 range: lsp::Range::new(
12800 lsp::Position::new(1, 0),
12801 lsp::Position::new(1, 0),
12802 ),
12803 new_text: "\n".into(),
12804 },
12805 lsp::TextEdit {
12806 range: lsp::Range::new(
12807 lsp::Position::new(2, 0),
12808 lsp::Position::new(2, 0),
12809 ),
12810 new_text: "\n".into(),
12811 },
12812 ]))
12813 }
12814 }
12815 });
12816
12817 // Set up a buffer white some trailing whitespace and no trailing newline.
12818 cx.set_state(
12819 &[
12820 "one ", //
12821 "twoˇ", //
12822 "three ", //
12823 "four", //
12824 ]
12825 .join("\n"),
12826 );
12827 cx.run_until_parked();
12828
12829 // Submit a format request.
12830 let format = cx
12831 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12832 .unwrap();
12833
12834 cx.run_until_parked();
12835 // After formatting the buffer, the trailing whitespace is stripped,
12836 // a newline is appended, and the edits provided by the language server
12837 // have been applied.
12838 format.await.unwrap();
12839
12840 cx.assert_editor_state(
12841 &[
12842 "one", //
12843 "", //
12844 "twoˇ", //
12845 "", //
12846 "three", //
12847 "four", //
12848 "", //
12849 ]
12850 .join("\n"),
12851 );
12852
12853 // Undoing the formatting undoes the trailing whitespace removal, the
12854 // trailing newline, and the LSP edits.
12855 cx.update_buffer(|buffer, cx| buffer.undo(cx));
12856 cx.assert_editor_state(
12857 &[
12858 "one ", //
12859 "twoˇ", //
12860 "three ", //
12861 "four", //
12862 ]
12863 .join("\n"),
12864 );
12865}
12866
12867#[gpui::test]
12868async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
12869 cx: &mut TestAppContext,
12870) {
12871 init_test(cx, |_| {});
12872
12873 cx.update(|cx| {
12874 cx.update_global::<SettingsStore, _>(|settings, cx| {
12875 settings.update_user_settings(cx, |settings| {
12876 settings.editor.auto_signature_help = Some(true);
12877 });
12878 });
12879 });
12880
12881 let mut cx = EditorLspTestContext::new_rust(
12882 lsp::ServerCapabilities {
12883 signature_help_provider: Some(lsp::SignatureHelpOptions {
12884 ..Default::default()
12885 }),
12886 ..Default::default()
12887 },
12888 cx,
12889 )
12890 .await;
12891
12892 let language = Language::new(
12893 LanguageConfig {
12894 name: "Rust".into(),
12895 brackets: BracketPairConfig {
12896 pairs: vec![
12897 BracketPair {
12898 start: "{".to_string(),
12899 end: "}".to_string(),
12900 close: true,
12901 surround: true,
12902 newline: true,
12903 },
12904 BracketPair {
12905 start: "(".to_string(),
12906 end: ")".to_string(),
12907 close: true,
12908 surround: true,
12909 newline: true,
12910 },
12911 BracketPair {
12912 start: "/*".to_string(),
12913 end: " */".to_string(),
12914 close: true,
12915 surround: true,
12916 newline: true,
12917 },
12918 BracketPair {
12919 start: "[".to_string(),
12920 end: "]".to_string(),
12921 close: false,
12922 surround: false,
12923 newline: true,
12924 },
12925 BracketPair {
12926 start: "\"".to_string(),
12927 end: "\"".to_string(),
12928 close: true,
12929 surround: true,
12930 newline: false,
12931 },
12932 BracketPair {
12933 start: "<".to_string(),
12934 end: ">".to_string(),
12935 close: false,
12936 surround: true,
12937 newline: true,
12938 },
12939 ],
12940 ..Default::default()
12941 },
12942 autoclose_before: "})]".to_string(),
12943 ..Default::default()
12944 },
12945 Some(tree_sitter_rust::LANGUAGE.into()),
12946 );
12947 let language = Arc::new(language);
12948
12949 cx.language_registry().add(language.clone());
12950 cx.update_buffer(|buffer, cx| {
12951 buffer.set_language(Some(language), cx);
12952 });
12953
12954 cx.set_state(
12955 &r#"
12956 fn main() {
12957 sampleˇ
12958 }
12959 "#
12960 .unindent(),
12961 );
12962
12963 cx.update_editor(|editor, window, cx| {
12964 editor.handle_input("(", window, cx);
12965 });
12966 cx.assert_editor_state(
12967 &"
12968 fn main() {
12969 sample(ˇ)
12970 }
12971 "
12972 .unindent(),
12973 );
12974
12975 let mocked_response = lsp::SignatureHelp {
12976 signatures: vec![lsp::SignatureInformation {
12977 label: "fn sample(param1: u8, param2: u8)".to_string(),
12978 documentation: None,
12979 parameters: Some(vec![
12980 lsp::ParameterInformation {
12981 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12982 documentation: None,
12983 },
12984 lsp::ParameterInformation {
12985 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12986 documentation: None,
12987 },
12988 ]),
12989 active_parameter: None,
12990 }],
12991 active_signature: Some(0),
12992 active_parameter: Some(0),
12993 };
12994 handle_signature_help_request(&mut cx, mocked_response).await;
12995
12996 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12997 .await;
12998
12999 cx.editor(|editor, _, _| {
13000 let signature_help_state = editor.signature_help_state.popover().cloned();
13001 let signature = signature_help_state.unwrap();
13002 assert_eq!(
13003 signature.signatures[signature.current_signature].label,
13004 "fn sample(param1: u8, param2: u8)"
13005 );
13006 });
13007}
13008
13009#[gpui::test]
13010async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
13011 init_test(cx, |_| {});
13012
13013 cx.update(|cx| {
13014 cx.update_global::<SettingsStore, _>(|settings, cx| {
13015 settings.update_user_settings(cx, |settings| {
13016 settings.editor.auto_signature_help = Some(false);
13017 settings.editor.show_signature_help_after_edits = Some(false);
13018 });
13019 });
13020 });
13021
13022 let mut cx = EditorLspTestContext::new_rust(
13023 lsp::ServerCapabilities {
13024 signature_help_provider: Some(lsp::SignatureHelpOptions {
13025 ..Default::default()
13026 }),
13027 ..Default::default()
13028 },
13029 cx,
13030 )
13031 .await;
13032
13033 let language = Language::new(
13034 LanguageConfig {
13035 name: "Rust".into(),
13036 brackets: BracketPairConfig {
13037 pairs: vec![
13038 BracketPair {
13039 start: "{".to_string(),
13040 end: "}".to_string(),
13041 close: true,
13042 surround: true,
13043 newline: true,
13044 },
13045 BracketPair {
13046 start: "(".to_string(),
13047 end: ")".to_string(),
13048 close: true,
13049 surround: true,
13050 newline: true,
13051 },
13052 BracketPair {
13053 start: "/*".to_string(),
13054 end: " */".to_string(),
13055 close: true,
13056 surround: true,
13057 newline: true,
13058 },
13059 BracketPair {
13060 start: "[".to_string(),
13061 end: "]".to_string(),
13062 close: false,
13063 surround: false,
13064 newline: true,
13065 },
13066 BracketPair {
13067 start: "\"".to_string(),
13068 end: "\"".to_string(),
13069 close: true,
13070 surround: true,
13071 newline: false,
13072 },
13073 BracketPair {
13074 start: "<".to_string(),
13075 end: ">".to_string(),
13076 close: false,
13077 surround: true,
13078 newline: true,
13079 },
13080 ],
13081 ..Default::default()
13082 },
13083 autoclose_before: "})]".to_string(),
13084 ..Default::default()
13085 },
13086 Some(tree_sitter_rust::LANGUAGE.into()),
13087 );
13088 let language = Arc::new(language);
13089
13090 cx.language_registry().add(language.clone());
13091 cx.update_buffer(|buffer, cx| {
13092 buffer.set_language(Some(language), cx);
13093 });
13094
13095 // Ensure that signature_help is not called when no signature help is enabled.
13096 cx.set_state(
13097 &r#"
13098 fn main() {
13099 sampleˇ
13100 }
13101 "#
13102 .unindent(),
13103 );
13104 cx.update_editor(|editor, window, cx| {
13105 editor.handle_input("(", window, cx);
13106 });
13107 cx.assert_editor_state(
13108 &"
13109 fn main() {
13110 sample(ˇ)
13111 }
13112 "
13113 .unindent(),
13114 );
13115 cx.editor(|editor, _, _| {
13116 assert!(editor.signature_help_state.task().is_none());
13117 });
13118
13119 let mocked_response = lsp::SignatureHelp {
13120 signatures: vec![lsp::SignatureInformation {
13121 label: "fn sample(param1: u8, param2: u8)".to_string(),
13122 documentation: None,
13123 parameters: Some(vec![
13124 lsp::ParameterInformation {
13125 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13126 documentation: None,
13127 },
13128 lsp::ParameterInformation {
13129 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13130 documentation: None,
13131 },
13132 ]),
13133 active_parameter: None,
13134 }],
13135 active_signature: Some(0),
13136 active_parameter: Some(0),
13137 };
13138
13139 // Ensure that signature_help is called when enabled afte edits
13140 cx.update(|_, cx| {
13141 cx.update_global::<SettingsStore, _>(|settings, cx| {
13142 settings.update_user_settings(cx, |settings| {
13143 settings.editor.auto_signature_help = Some(false);
13144 settings.editor.show_signature_help_after_edits = Some(true);
13145 });
13146 });
13147 });
13148 cx.set_state(
13149 &r#"
13150 fn main() {
13151 sampleˇ
13152 }
13153 "#
13154 .unindent(),
13155 );
13156 cx.update_editor(|editor, window, cx| {
13157 editor.handle_input("(", window, cx);
13158 });
13159 cx.assert_editor_state(
13160 &"
13161 fn main() {
13162 sample(ˇ)
13163 }
13164 "
13165 .unindent(),
13166 );
13167 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13168 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13169 .await;
13170 cx.update_editor(|editor, _, _| {
13171 let signature_help_state = editor.signature_help_state.popover().cloned();
13172 assert!(signature_help_state.is_some());
13173 let signature = signature_help_state.unwrap();
13174 assert_eq!(
13175 signature.signatures[signature.current_signature].label,
13176 "fn sample(param1: u8, param2: u8)"
13177 );
13178 editor.signature_help_state = SignatureHelpState::default();
13179 });
13180
13181 // Ensure that signature_help is called when auto signature help override is enabled
13182 cx.update(|_, cx| {
13183 cx.update_global::<SettingsStore, _>(|settings, cx| {
13184 settings.update_user_settings(cx, |settings| {
13185 settings.editor.auto_signature_help = Some(true);
13186 settings.editor.show_signature_help_after_edits = Some(false);
13187 });
13188 });
13189 });
13190 cx.set_state(
13191 &r#"
13192 fn main() {
13193 sampleˇ
13194 }
13195 "#
13196 .unindent(),
13197 );
13198 cx.update_editor(|editor, window, cx| {
13199 editor.handle_input("(", window, cx);
13200 });
13201 cx.assert_editor_state(
13202 &"
13203 fn main() {
13204 sample(ˇ)
13205 }
13206 "
13207 .unindent(),
13208 );
13209 handle_signature_help_request(&mut cx, mocked_response).await;
13210 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13211 .await;
13212 cx.editor(|editor, _, _| {
13213 let signature_help_state = editor.signature_help_state.popover().cloned();
13214 assert!(signature_help_state.is_some());
13215 let signature = signature_help_state.unwrap();
13216 assert_eq!(
13217 signature.signatures[signature.current_signature].label,
13218 "fn sample(param1: u8, param2: u8)"
13219 );
13220 });
13221}
13222
13223#[gpui::test]
13224async fn test_signature_help(cx: &mut TestAppContext) {
13225 init_test(cx, |_| {});
13226 cx.update(|cx| {
13227 cx.update_global::<SettingsStore, _>(|settings, cx| {
13228 settings.update_user_settings(cx, |settings| {
13229 settings.editor.auto_signature_help = Some(true);
13230 });
13231 });
13232 });
13233
13234 let mut cx = EditorLspTestContext::new_rust(
13235 lsp::ServerCapabilities {
13236 signature_help_provider: Some(lsp::SignatureHelpOptions {
13237 ..Default::default()
13238 }),
13239 ..Default::default()
13240 },
13241 cx,
13242 )
13243 .await;
13244
13245 // A test that directly calls `show_signature_help`
13246 cx.update_editor(|editor, window, cx| {
13247 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13248 });
13249
13250 let mocked_response = lsp::SignatureHelp {
13251 signatures: vec![lsp::SignatureInformation {
13252 label: "fn sample(param1: u8, param2: u8)".to_string(),
13253 documentation: None,
13254 parameters: Some(vec![
13255 lsp::ParameterInformation {
13256 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13257 documentation: None,
13258 },
13259 lsp::ParameterInformation {
13260 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13261 documentation: None,
13262 },
13263 ]),
13264 active_parameter: None,
13265 }],
13266 active_signature: Some(0),
13267 active_parameter: Some(0),
13268 };
13269 handle_signature_help_request(&mut cx, mocked_response).await;
13270
13271 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13272 .await;
13273
13274 cx.editor(|editor, _, _| {
13275 let signature_help_state = editor.signature_help_state.popover().cloned();
13276 assert!(signature_help_state.is_some());
13277 let signature = signature_help_state.unwrap();
13278 assert_eq!(
13279 signature.signatures[signature.current_signature].label,
13280 "fn sample(param1: u8, param2: u8)"
13281 );
13282 });
13283
13284 // When exiting outside from inside the brackets, `signature_help` is closed.
13285 cx.set_state(indoc! {"
13286 fn main() {
13287 sample(ˇ);
13288 }
13289
13290 fn sample(param1: u8, param2: u8) {}
13291 "});
13292
13293 cx.update_editor(|editor, window, cx| {
13294 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13295 s.select_ranges([0..0])
13296 });
13297 });
13298
13299 let mocked_response = lsp::SignatureHelp {
13300 signatures: Vec::new(),
13301 active_signature: None,
13302 active_parameter: None,
13303 };
13304 handle_signature_help_request(&mut cx, mocked_response).await;
13305
13306 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13307 .await;
13308
13309 cx.editor(|editor, _, _| {
13310 assert!(!editor.signature_help_state.is_shown());
13311 });
13312
13313 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
13314 cx.set_state(indoc! {"
13315 fn main() {
13316 sample(ˇ);
13317 }
13318
13319 fn sample(param1: u8, param2: u8) {}
13320 "});
13321
13322 let mocked_response = lsp::SignatureHelp {
13323 signatures: vec![lsp::SignatureInformation {
13324 label: "fn sample(param1: u8, param2: u8)".to_string(),
13325 documentation: None,
13326 parameters: Some(vec![
13327 lsp::ParameterInformation {
13328 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13329 documentation: None,
13330 },
13331 lsp::ParameterInformation {
13332 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13333 documentation: None,
13334 },
13335 ]),
13336 active_parameter: None,
13337 }],
13338 active_signature: Some(0),
13339 active_parameter: Some(0),
13340 };
13341 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13342 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13343 .await;
13344 cx.editor(|editor, _, _| {
13345 assert!(editor.signature_help_state.is_shown());
13346 });
13347
13348 // Restore the popover with more parameter input
13349 cx.set_state(indoc! {"
13350 fn main() {
13351 sample(param1, param2ˇ);
13352 }
13353
13354 fn sample(param1: u8, param2: u8) {}
13355 "});
13356
13357 let mocked_response = lsp::SignatureHelp {
13358 signatures: vec![lsp::SignatureInformation {
13359 label: "fn sample(param1: u8, param2: u8)".to_string(),
13360 documentation: None,
13361 parameters: Some(vec![
13362 lsp::ParameterInformation {
13363 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13364 documentation: None,
13365 },
13366 lsp::ParameterInformation {
13367 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13368 documentation: None,
13369 },
13370 ]),
13371 active_parameter: None,
13372 }],
13373 active_signature: Some(0),
13374 active_parameter: Some(1),
13375 };
13376 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13377 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13378 .await;
13379
13380 // When selecting a range, the popover is gone.
13381 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
13382 cx.update_editor(|editor, window, cx| {
13383 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13384 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13385 })
13386 });
13387 cx.assert_editor_state(indoc! {"
13388 fn main() {
13389 sample(param1, «ˇparam2»);
13390 }
13391
13392 fn sample(param1: u8, param2: u8) {}
13393 "});
13394 cx.editor(|editor, _, _| {
13395 assert!(!editor.signature_help_state.is_shown());
13396 });
13397
13398 // When unselecting again, the popover is back if within the brackets.
13399 cx.update_editor(|editor, window, cx| {
13400 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13401 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13402 })
13403 });
13404 cx.assert_editor_state(indoc! {"
13405 fn main() {
13406 sample(param1, ˇparam2);
13407 }
13408
13409 fn sample(param1: u8, param2: u8) {}
13410 "});
13411 handle_signature_help_request(&mut cx, mocked_response).await;
13412 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13413 .await;
13414 cx.editor(|editor, _, _| {
13415 assert!(editor.signature_help_state.is_shown());
13416 });
13417
13418 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
13419 cx.update_editor(|editor, window, cx| {
13420 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13421 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
13422 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13423 })
13424 });
13425 cx.assert_editor_state(indoc! {"
13426 fn main() {
13427 sample(param1, ˇparam2);
13428 }
13429
13430 fn sample(param1: u8, param2: u8) {}
13431 "});
13432
13433 let mocked_response = lsp::SignatureHelp {
13434 signatures: vec![lsp::SignatureInformation {
13435 label: "fn sample(param1: u8, param2: u8)".to_string(),
13436 documentation: None,
13437 parameters: Some(vec![
13438 lsp::ParameterInformation {
13439 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13440 documentation: None,
13441 },
13442 lsp::ParameterInformation {
13443 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13444 documentation: None,
13445 },
13446 ]),
13447 active_parameter: None,
13448 }],
13449 active_signature: Some(0),
13450 active_parameter: Some(1),
13451 };
13452 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13453 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13454 .await;
13455 cx.update_editor(|editor, _, cx| {
13456 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
13457 });
13458 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13459 .await;
13460 cx.update_editor(|editor, window, cx| {
13461 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13462 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13463 })
13464 });
13465 cx.assert_editor_state(indoc! {"
13466 fn main() {
13467 sample(param1, «ˇparam2»);
13468 }
13469
13470 fn sample(param1: u8, param2: u8) {}
13471 "});
13472 cx.update_editor(|editor, window, cx| {
13473 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13474 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13475 })
13476 });
13477 cx.assert_editor_state(indoc! {"
13478 fn main() {
13479 sample(param1, ˇparam2);
13480 }
13481
13482 fn sample(param1: u8, param2: u8) {}
13483 "});
13484 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
13485 .await;
13486}
13487
13488#[gpui::test]
13489async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
13490 init_test(cx, |_| {});
13491
13492 let mut cx = EditorLspTestContext::new_rust(
13493 lsp::ServerCapabilities {
13494 signature_help_provider: Some(lsp::SignatureHelpOptions {
13495 ..Default::default()
13496 }),
13497 ..Default::default()
13498 },
13499 cx,
13500 )
13501 .await;
13502
13503 cx.set_state(indoc! {"
13504 fn main() {
13505 overloadedˇ
13506 }
13507 "});
13508
13509 cx.update_editor(|editor, window, cx| {
13510 editor.handle_input("(", window, cx);
13511 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13512 });
13513
13514 // Mock response with 3 signatures
13515 let mocked_response = lsp::SignatureHelp {
13516 signatures: vec![
13517 lsp::SignatureInformation {
13518 label: "fn overloaded(x: i32)".to_string(),
13519 documentation: None,
13520 parameters: Some(vec![lsp::ParameterInformation {
13521 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13522 documentation: None,
13523 }]),
13524 active_parameter: None,
13525 },
13526 lsp::SignatureInformation {
13527 label: "fn overloaded(x: i32, y: i32)".to_string(),
13528 documentation: None,
13529 parameters: Some(vec![
13530 lsp::ParameterInformation {
13531 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13532 documentation: None,
13533 },
13534 lsp::ParameterInformation {
13535 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13536 documentation: None,
13537 },
13538 ]),
13539 active_parameter: None,
13540 },
13541 lsp::SignatureInformation {
13542 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
13543 documentation: None,
13544 parameters: Some(vec![
13545 lsp::ParameterInformation {
13546 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13547 documentation: None,
13548 },
13549 lsp::ParameterInformation {
13550 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13551 documentation: None,
13552 },
13553 lsp::ParameterInformation {
13554 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
13555 documentation: None,
13556 },
13557 ]),
13558 active_parameter: None,
13559 },
13560 ],
13561 active_signature: Some(1),
13562 active_parameter: Some(0),
13563 };
13564 handle_signature_help_request(&mut cx, mocked_response).await;
13565
13566 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13567 .await;
13568
13569 // Verify we have multiple signatures and the right one is selected
13570 cx.editor(|editor, _, _| {
13571 let popover = editor.signature_help_state.popover().cloned().unwrap();
13572 assert_eq!(popover.signatures.len(), 3);
13573 // active_signature was 1, so that should be the current
13574 assert_eq!(popover.current_signature, 1);
13575 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
13576 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
13577 assert_eq!(
13578 popover.signatures[2].label,
13579 "fn overloaded(x: i32, y: i32, z: i32)"
13580 );
13581 });
13582
13583 // Test navigation functionality
13584 cx.update_editor(|editor, window, cx| {
13585 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13586 });
13587
13588 cx.editor(|editor, _, _| {
13589 let popover = editor.signature_help_state.popover().cloned().unwrap();
13590 assert_eq!(popover.current_signature, 2);
13591 });
13592
13593 // Test wrap around
13594 cx.update_editor(|editor, window, cx| {
13595 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13596 });
13597
13598 cx.editor(|editor, _, _| {
13599 let popover = editor.signature_help_state.popover().cloned().unwrap();
13600 assert_eq!(popover.current_signature, 0);
13601 });
13602
13603 // Test previous navigation
13604 cx.update_editor(|editor, window, cx| {
13605 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
13606 });
13607
13608 cx.editor(|editor, _, _| {
13609 let popover = editor.signature_help_state.popover().cloned().unwrap();
13610 assert_eq!(popover.current_signature, 2);
13611 });
13612}
13613
13614#[gpui::test]
13615async fn test_completion_mode(cx: &mut TestAppContext) {
13616 init_test(cx, |_| {});
13617 let mut cx = EditorLspTestContext::new_rust(
13618 lsp::ServerCapabilities {
13619 completion_provider: Some(lsp::CompletionOptions {
13620 resolve_provider: Some(true),
13621 ..Default::default()
13622 }),
13623 ..Default::default()
13624 },
13625 cx,
13626 )
13627 .await;
13628
13629 struct Run {
13630 run_description: &'static str,
13631 initial_state: String,
13632 buffer_marked_text: String,
13633 completion_label: &'static str,
13634 completion_text: &'static str,
13635 expected_with_insert_mode: String,
13636 expected_with_replace_mode: String,
13637 expected_with_replace_subsequence_mode: String,
13638 expected_with_replace_suffix_mode: String,
13639 }
13640
13641 let runs = [
13642 Run {
13643 run_description: "Start of word matches completion text",
13644 initial_state: "before ediˇ after".into(),
13645 buffer_marked_text: "before <edi|> after".into(),
13646 completion_label: "editor",
13647 completion_text: "editor",
13648 expected_with_insert_mode: "before editorˇ after".into(),
13649 expected_with_replace_mode: "before editorˇ after".into(),
13650 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13651 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13652 },
13653 Run {
13654 run_description: "Accept same text at the middle of the word",
13655 initial_state: "before ediˇtor after".into(),
13656 buffer_marked_text: "before <edi|tor> after".into(),
13657 completion_label: "editor",
13658 completion_text: "editor",
13659 expected_with_insert_mode: "before editorˇtor after".into(),
13660 expected_with_replace_mode: "before editorˇ after".into(),
13661 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13662 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13663 },
13664 Run {
13665 run_description: "End of word matches completion text -- cursor at end",
13666 initial_state: "before torˇ after".into(),
13667 buffer_marked_text: "before <tor|> after".into(),
13668 completion_label: "editor",
13669 completion_text: "editor",
13670 expected_with_insert_mode: "before editorˇ after".into(),
13671 expected_with_replace_mode: "before editorˇ after".into(),
13672 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13673 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13674 },
13675 Run {
13676 run_description: "End of word matches completion text -- cursor at start",
13677 initial_state: "before ˇtor after".into(),
13678 buffer_marked_text: "before <|tor> after".into(),
13679 completion_label: "editor",
13680 completion_text: "editor",
13681 expected_with_insert_mode: "before editorˇtor after".into(),
13682 expected_with_replace_mode: "before editorˇ after".into(),
13683 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13684 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13685 },
13686 Run {
13687 run_description: "Prepend text containing whitespace",
13688 initial_state: "pˇfield: bool".into(),
13689 buffer_marked_text: "<p|field>: bool".into(),
13690 completion_label: "pub ",
13691 completion_text: "pub ",
13692 expected_with_insert_mode: "pub ˇfield: bool".into(),
13693 expected_with_replace_mode: "pub ˇ: bool".into(),
13694 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
13695 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
13696 },
13697 Run {
13698 run_description: "Add element to start of list",
13699 initial_state: "[element_ˇelement_2]".into(),
13700 buffer_marked_text: "[<element_|element_2>]".into(),
13701 completion_label: "element_1",
13702 completion_text: "element_1",
13703 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
13704 expected_with_replace_mode: "[element_1ˇ]".into(),
13705 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
13706 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
13707 },
13708 Run {
13709 run_description: "Add element to start of list -- first and second elements are equal",
13710 initial_state: "[elˇelement]".into(),
13711 buffer_marked_text: "[<el|element>]".into(),
13712 completion_label: "element",
13713 completion_text: "element",
13714 expected_with_insert_mode: "[elementˇelement]".into(),
13715 expected_with_replace_mode: "[elementˇ]".into(),
13716 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
13717 expected_with_replace_suffix_mode: "[elementˇ]".into(),
13718 },
13719 Run {
13720 run_description: "Ends with matching suffix",
13721 initial_state: "SubˇError".into(),
13722 buffer_marked_text: "<Sub|Error>".into(),
13723 completion_label: "SubscriptionError",
13724 completion_text: "SubscriptionError",
13725 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
13726 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13727 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13728 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
13729 },
13730 Run {
13731 run_description: "Suffix is a subsequence -- contiguous",
13732 initial_state: "SubˇErr".into(),
13733 buffer_marked_text: "<Sub|Err>".into(),
13734 completion_label: "SubscriptionError",
13735 completion_text: "SubscriptionError",
13736 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
13737 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13738 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13739 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
13740 },
13741 Run {
13742 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
13743 initial_state: "Suˇscrirr".into(),
13744 buffer_marked_text: "<Su|scrirr>".into(),
13745 completion_label: "SubscriptionError",
13746 completion_text: "SubscriptionError",
13747 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
13748 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13749 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13750 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
13751 },
13752 Run {
13753 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
13754 initial_state: "foo(indˇix)".into(),
13755 buffer_marked_text: "foo(<ind|ix>)".into(),
13756 completion_label: "node_index",
13757 completion_text: "node_index",
13758 expected_with_insert_mode: "foo(node_indexˇix)".into(),
13759 expected_with_replace_mode: "foo(node_indexˇ)".into(),
13760 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
13761 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
13762 },
13763 Run {
13764 run_description: "Replace range ends before cursor - should extend to cursor",
13765 initial_state: "before editˇo after".into(),
13766 buffer_marked_text: "before <{ed}>it|o after".into(),
13767 completion_label: "editor",
13768 completion_text: "editor",
13769 expected_with_insert_mode: "before editorˇo after".into(),
13770 expected_with_replace_mode: "before editorˇo after".into(),
13771 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
13772 expected_with_replace_suffix_mode: "before editorˇo after".into(),
13773 },
13774 Run {
13775 run_description: "Uses label for suffix matching",
13776 initial_state: "before ediˇtor after".into(),
13777 buffer_marked_text: "before <edi|tor> after".into(),
13778 completion_label: "editor",
13779 completion_text: "editor()",
13780 expected_with_insert_mode: "before editor()ˇtor after".into(),
13781 expected_with_replace_mode: "before editor()ˇ after".into(),
13782 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
13783 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
13784 },
13785 Run {
13786 run_description: "Case insensitive subsequence and suffix matching",
13787 initial_state: "before EDiˇtoR after".into(),
13788 buffer_marked_text: "before <EDi|toR> after".into(),
13789 completion_label: "editor",
13790 completion_text: "editor",
13791 expected_with_insert_mode: "before editorˇtoR after".into(),
13792 expected_with_replace_mode: "before editorˇ after".into(),
13793 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13794 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13795 },
13796 ];
13797
13798 for run in runs {
13799 let run_variations = [
13800 (LspInsertMode::Insert, run.expected_with_insert_mode),
13801 (LspInsertMode::Replace, run.expected_with_replace_mode),
13802 (
13803 LspInsertMode::ReplaceSubsequence,
13804 run.expected_with_replace_subsequence_mode,
13805 ),
13806 (
13807 LspInsertMode::ReplaceSuffix,
13808 run.expected_with_replace_suffix_mode,
13809 ),
13810 ];
13811
13812 for (lsp_insert_mode, expected_text) in run_variations {
13813 eprintln!(
13814 "run = {:?}, mode = {lsp_insert_mode:.?}",
13815 run.run_description,
13816 );
13817
13818 update_test_language_settings(&mut cx, |settings| {
13819 settings.defaults.completions = Some(CompletionSettingsContent {
13820 lsp_insert_mode: Some(lsp_insert_mode),
13821 words: Some(WordsCompletionMode::Disabled),
13822 words_min_length: Some(0),
13823 ..Default::default()
13824 });
13825 });
13826
13827 cx.set_state(&run.initial_state);
13828 cx.update_editor(|editor, window, cx| {
13829 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13830 });
13831
13832 let counter = Arc::new(AtomicUsize::new(0));
13833 handle_completion_request_with_insert_and_replace(
13834 &mut cx,
13835 &run.buffer_marked_text,
13836 vec![(run.completion_label, run.completion_text)],
13837 counter.clone(),
13838 )
13839 .await;
13840 cx.condition(|editor, _| editor.context_menu_visible())
13841 .await;
13842 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13843
13844 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13845 editor
13846 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13847 .unwrap()
13848 });
13849 cx.assert_editor_state(&expected_text);
13850 handle_resolve_completion_request(&mut cx, None).await;
13851 apply_additional_edits.await.unwrap();
13852 }
13853 }
13854}
13855
13856#[gpui::test]
13857async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
13858 init_test(cx, |_| {});
13859 let mut cx = EditorLspTestContext::new_rust(
13860 lsp::ServerCapabilities {
13861 completion_provider: Some(lsp::CompletionOptions {
13862 resolve_provider: Some(true),
13863 ..Default::default()
13864 }),
13865 ..Default::default()
13866 },
13867 cx,
13868 )
13869 .await;
13870
13871 let initial_state = "SubˇError";
13872 let buffer_marked_text = "<Sub|Error>";
13873 let completion_text = "SubscriptionError";
13874 let expected_with_insert_mode = "SubscriptionErrorˇError";
13875 let expected_with_replace_mode = "SubscriptionErrorˇ";
13876
13877 update_test_language_settings(&mut cx, |settings| {
13878 settings.defaults.completions = Some(CompletionSettingsContent {
13879 words: Some(WordsCompletionMode::Disabled),
13880 words_min_length: Some(0),
13881 // set the opposite here to ensure that the action is overriding the default behavior
13882 lsp_insert_mode: Some(LspInsertMode::Insert),
13883 ..Default::default()
13884 });
13885 });
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
13892 let counter = Arc::new(AtomicUsize::new(0));
13893 handle_completion_request_with_insert_and_replace(
13894 &mut cx,
13895 buffer_marked_text,
13896 vec![(completion_text, completion_text)],
13897 counter.clone(),
13898 )
13899 .await;
13900 cx.condition(|editor, _| editor.context_menu_visible())
13901 .await;
13902 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13903
13904 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13905 editor
13906 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13907 .unwrap()
13908 });
13909 cx.assert_editor_state(expected_with_replace_mode);
13910 handle_resolve_completion_request(&mut cx, None).await;
13911 apply_additional_edits.await.unwrap();
13912
13913 update_test_language_settings(&mut cx, |settings| {
13914 settings.defaults.completions = Some(CompletionSettingsContent {
13915 words: Some(WordsCompletionMode::Disabled),
13916 words_min_length: Some(0),
13917 // set the opposite here to ensure that the action is overriding the default behavior
13918 lsp_insert_mode: Some(LspInsertMode::Replace),
13919 ..Default::default()
13920 });
13921 });
13922
13923 cx.set_state(initial_state);
13924 cx.update_editor(|editor, window, cx| {
13925 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13926 });
13927 handle_completion_request_with_insert_and_replace(
13928 &mut cx,
13929 buffer_marked_text,
13930 vec![(completion_text, completion_text)],
13931 counter.clone(),
13932 )
13933 .await;
13934 cx.condition(|editor, _| editor.context_menu_visible())
13935 .await;
13936 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
13937
13938 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13939 editor
13940 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
13941 .unwrap()
13942 });
13943 cx.assert_editor_state(expected_with_insert_mode);
13944 handle_resolve_completion_request(&mut cx, None).await;
13945 apply_additional_edits.await.unwrap();
13946}
13947
13948#[gpui::test]
13949async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
13950 init_test(cx, |_| {});
13951 let mut cx = EditorLspTestContext::new_rust(
13952 lsp::ServerCapabilities {
13953 completion_provider: Some(lsp::CompletionOptions {
13954 resolve_provider: Some(true),
13955 ..Default::default()
13956 }),
13957 ..Default::default()
13958 },
13959 cx,
13960 )
13961 .await;
13962
13963 // scenario: surrounding text matches completion text
13964 let completion_text = "to_offset";
13965 let initial_state = indoc! {"
13966 1. buf.to_offˇsuffix
13967 2. buf.to_offˇsuf
13968 3. buf.to_offˇfix
13969 4. buf.to_offˇ
13970 5. into_offˇensive
13971 6. ˇsuffix
13972 7. let ˇ //
13973 8. aaˇzz
13974 9. buf.to_off«zzzzzˇ»suffix
13975 10. buf.«ˇzzzzz»suffix
13976 11. to_off«ˇzzzzz»
13977
13978 buf.to_offˇsuffix // newest cursor
13979 "};
13980 let completion_marked_buffer = indoc! {"
13981 1. buf.to_offsuffix
13982 2. buf.to_offsuf
13983 3. buf.to_offfix
13984 4. buf.to_off
13985 5. into_offensive
13986 6. suffix
13987 7. let //
13988 8. aazz
13989 9. buf.to_offzzzzzsuffix
13990 10. buf.zzzzzsuffix
13991 11. to_offzzzzz
13992
13993 buf.<to_off|suffix> // newest cursor
13994 "};
13995 let expected = indoc! {"
13996 1. buf.to_offsetˇ
13997 2. buf.to_offsetˇsuf
13998 3. buf.to_offsetˇfix
13999 4. buf.to_offsetˇ
14000 5. into_offsetˇensive
14001 6. to_offsetˇsuffix
14002 7. let to_offsetˇ //
14003 8. aato_offsetˇzz
14004 9. buf.to_offsetˇ
14005 10. buf.to_offsetˇsuffix
14006 11. to_offsetˇ
14007
14008 buf.to_offsetˇ // newest cursor
14009 "};
14010 cx.set_state(initial_state);
14011 cx.update_editor(|editor, window, cx| {
14012 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14013 });
14014 handle_completion_request_with_insert_and_replace(
14015 &mut cx,
14016 completion_marked_buffer,
14017 vec![(completion_text, completion_text)],
14018 Arc::new(AtomicUsize::new(0)),
14019 )
14020 .await;
14021 cx.condition(|editor, _| editor.context_menu_visible())
14022 .await;
14023 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14024 editor
14025 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14026 .unwrap()
14027 });
14028 cx.assert_editor_state(expected);
14029 handle_resolve_completion_request(&mut cx, None).await;
14030 apply_additional_edits.await.unwrap();
14031
14032 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
14033 let completion_text = "foo_and_bar";
14034 let initial_state = indoc! {"
14035 1. ooanbˇ
14036 2. zooanbˇ
14037 3. ooanbˇz
14038 4. zooanbˇz
14039 5. ooanˇ
14040 6. oanbˇ
14041
14042 ooanbˇ
14043 "};
14044 let completion_marked_buffer = indoc! {"
14045 1. ooanb
14046 2. zooanb
14047 3. ooanbz
14048 4. zooanbz
14049 5. ooan
14050 6. oanb
14051
14052 <ooanb|>
14053 "};
14054 let expected = indoc! {"
14055 1. foo_and_barˇ
14056 2. zfoo_and_barˇ
14057 3. foo_and_barˇz
14058 4. zfoo_and_barˇz
14059 5. ooanfoo_and_barˇ
14060 6. oanbfoo_and_barˇ
14061
14062 foo_and_barˇ
14063 "};
14064 cx.set_state(initial_state);
14065 cx.update_editor(|editor, window, cx| {
14066 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14067 });
14068 handle_completion_request_with_insert_and_replace(
14069 &mut cx,
14070 completion_marked_buffer,
14071 vec![(completion_text, completion_text)],
14072 Arc::new(AtomicUsize::new(0)),
14073 )
14074 .await;
14075 cx.condition(|editor, _| editor.context_menu_visible())
14076 .await;
14077 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14078 editor
14079 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14080 .unwrap()
14081 });
14082 cx.assert_editor_state(expected);
14083 handle_resolve_completion_request(&mut cx, None).await;
14084 apply_additional_edits.await.unwrap();
14085
14086 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
14087 // (expects the same as if it was inserted at the end)
14088 let completion_text = "foo_and_bar";
14089 let initial_state = indoc! {"
14090 1. ooˇanb
14091 2. zooˇanb
14092 3. ooˇanbz
14093 4. zooˇanbz
14094
14095 ooˇanb
14096 "};
14097 let completion_marked_buffer = indoc! {"
14098 1. ooanb
14099 2. zooanb
14100 3. ooanbz
14101 4. zooanbz
14102
14103 <oo|anb>
14104 "};
14105 let expected = indoc! {"
14106 1. foo_and_barˇ
14107 2. zfoo_and_barˇ
14108 3. foo_and_barˇz
14109 4. zfoo_and_barˇz
14110
14111 foo_and_barˇ
14112 "};
14113 cx.set_state(initial_state);
14114 cx.update_editor(|editor, window, cx| {
14115 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14116 });
14117 handle_completion_request_with_insert_and_replace(
14118 &mut cx,
14119 completion_marked_buffer,
14120 vec![(completion_text, completion_text)],
14121 Arc::new(AtomicUsize::new(0)),
14122 )
14123 .await;
14124 cx.condition(|editor, _| editor.context_menu_visible())
14125 .await;
14126 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14127 editor
14128 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14129 .unwrap()
14130 });
14131 cx.assert_editor_state(expected);
14132 handle_resolve_completion_request(&mut cx, None).await;
14133 apply_additional_edits.await.unwrap();
14134}
14135
14136// This used to crash
14137#[gpui::test]
14138async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
14139 init_test(cx, |_| {});
14140
14141 let buffer_text = indoc! {"
14142 fn main() {
14143 10.satu;
14144
14145 //
14146 // separate cursors so they open in different excerpts (manually reproducible)
14147 //
14148
14149 10.satu20;
14150 }
14151 "};
14152 let multibuffer_text_with_selections = indoc! {"
14153 fn main() {
14154 10.satuˇ;
14155
14156 //
14157
14158 //
14159
14160 10.satuˇ20;
14161 }
14162 "};
14163 let expected_multibuffer = indoc! {"
14164 fn main() {
14165 10.saturating_sub()ˇ;
14166
14167 //
14168
14169 //
14170
14171 10.saturating_sub()ˇ;
14172 }
14173 "};
14174
14175 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
14176 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
14177
14178 let fs = FakeFs::new(cx.executor());
14179 fs.insert_tree(
14180 path!("/a"),
14181 json!({
14182 "main.rs": buffer_text,
14183 }),
14184 )
14185 .await;
14186
14187 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14188 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14189 language_registry.add(rust_lang());
14190 let mut fake_servers = language_registry.register_fake_lsp(
14191 "Rust",
14192 FakeLspAdapter {
14193 capabilities: lsp::ServerCapabilities {
14194 completion_provider: Some(lsp::CompletionOptions {
14195 resolve_provider: None,
14196 ..lsp::CompletionOptions::default()
14197 }),
14198 ..lsp::ServerCapabilities::default()
14199 },
14200 ..FakeLspAdapter::default()
14201 },
14202 );
14203 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14204 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14205 let buffer = project
14206 .update(cx, |project, cx| {
14207 project.open_local_buffer(path!("/a/main.rs"), cx)
14208 })
14209 .await
14210 .unwrap();
14211
14212 let multi_buffer = cx.new(|cx| {
14213 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
14214 multi_buffer.push_excerpts(
14215 buffer.clone(),
14216 [ExcerptRange::new(0..first_excerpt_end)],
14217 cx,
14218 );
14219 multi_buffer.push_excerpts(
14220 buffer.clone(),
14221 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
14222 cx,
14223 );
14224 multi_buffer
14225 });
14226
14227 let editor = workspace
14228 .update(cx, |_, window, cx| {
14229 cx.new(|cx| {
14230 Editor::new(
14231 EditorMode::Full {
14232 scale_ui_elements_with_buffer_font_size: false,
14233 show_active_line_background: false,
14234 sizing_behavior: SizingBehavior::Default,
14235 },
14236 multi_buffer.clone(),
14237 Some(project.clone()),
14238 window,
14239 cx,
14240 )
14241 })
14242 })
14243 .unwrap();
14244
14245 let pane = workspace
14246 .update(cx, |workspace, _, _| workspace.active_pane().clone())
14247 .unwrap();
14248 pane.update_in(cx, |pane, window, cx| {
14249 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
14250 });
14251
14252 let fake_server = fake_servers.next().await.unwrap();
14253
14254 editor.update_in(cx, |editor, window, cx| {
14255 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14256 s.select_ranges([
14257 Point::new(1, 11)..Point::new(1, 11),
14258 Point::new(7, 11)..Point::new(7, 11),
14259 ])
14260 });
14261
14262 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
14263 });
14264
14265 editor.update_in(cx, |editor, window, cx| {
14266 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14267 });
14268
14269 fake_server
14270 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14271 let completion_item = lsp::CompletionItem {
14272 label: "saturating_sub()".into(),
14273 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
14274 lsp::InsertReplaceEdit {
14275 new_text: "saturating_sub()".to_owned(),
14276 insert: lsp::Range::new(
14277 lsp::Position::new(7, 7),
14278 lsp::Position::new(7, 11),
14279 ),
14280 replace: lsp::Range::new(
14281 lsp::Position::new(7, 7),
14282 lsp::Position::new(7, 13),
14283 ),
14284 },
14285 )),
14286 ..lsp::CompletionItem::default()
14287 };
14288
14289 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
14290 })
14291 .next()
14292 .await
14293 .unwrap();
14294
14295 cx.condition(&editor, |editor, _| editor.context_menu_visible())
14296 .await;
14297
14298 editor
14299 .update_in(cx, |editor, window, cx| {
14300 editor
14301 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14302 .unwrap()
14303 })
14304 .await
14305 .unwrap();
14306
14307 editor.update(cx, |editor, cx| {
14308 assert_text_with_selections(editor, expected_multibuffer, cx);
14309 })
14310}
14311
14312#[gpui::test]
14313async fn test_completion(cx: &mut TestAppContext) {
14314 init_test(cx, |_| {});
14315
14316 let mut cx = EditorLspTestContext::new_rust(
14317 lsp::ServerCapabilities {
14318 completion_provider: Some(lsp::CompletionOptions {
14319 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14320 resolve_provider: Some(true),
14321 ..Default::default()
14322 }),
14323 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14324 ..Default::default()
14325 },
14326 cx,
14327 )
14328 .await;
14329 let counter = Arc::new(AtomicUsize::new(0));
14330
14331 cx.set_state(indoc! {"
14332 oneˇ
14333 two
14334 three
14335 "});
14336 cx.simulate_keystroke(".");
14337 handle_completion_request(
14338 indoc! {"
14339 one.|<>
14340 two
14341 three
14342 "},
14343 vec!["first_completion", "second_completion"],
14344 true,
14345 counter.clone(),
14346 &mut cx,
14347 )
14348 .await;
14349 cx.condition(|editor, _| editor.context_menu_visible())
14350 .await;
14351 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14352
14353 let _handler = handle_signature_help_request(
14354 &mut cx,
14355 lsp::SignatureHelp {
14356 signatures: vec![lsp::SignatureInformation {
14357 label: "test signature".to_string(),
14358 documentation: None,
14359 parameters: Some(vec![lsp::ParameterInformation {
14360 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
14361 documentation: None,
14362 }]),
14363 active_parameter: None,
14364 }],
14365 active_signature: None,
14366 active_parameter: None,
14367 },
14368 );
14369 cx.update_editor(|editor, window, cx| {
14370 assert!(
14371 !editor.signature_help_state.is_shown(),
14372 "No signature help was called for"
14373 );
14374 editor.show_signature_help(&ShowSignatureHelp, window, cx);
14375 });
14376 cx.run_until_parked();
14377 cx.update_editor(|editor, _, _| {
14378 assert!(
14379 !editor.signature_help_state.is_shown(),
14380 "No signature help should be shown when completions menu is open"
14381 );
14382 });
14383
14384 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14385 editor.context_menu_next(&Default::default(), window, cx);
14386 editor
14387 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14388 .unwrap()
14389 });
14390 cx.assert_editor_state(indoc! {"
14391 one.second_completionˇ
14392 two
14393 three
14394 "});
14395
14396 handle_resolve_completion_request(
14397 &mut cx,
14398 Some(vec![
14399 (
14400 //This overlaps with the primary completion edit which is
14401 //misbehavior from the LSP spec, test that we filter it out
14402 indoc! {"
14403 one.second_ˇcompletion
14404 two
14405 threeˇ
14406 "},
14407 "overlapping additional edit",
14408 ),
14409 (
14410 indoc! {"
14411 one.second_completion
14412 two
14413 threeˇ
14414 "},
14415 "\nadditional edit",
14416 ),
14417 ]),
14418 )
14419 .await;
14420 apply_additional_edits.await.unwrap();
14421 cx.assert_editor_state(indoc! {"
14422 one.second_completionˇ
14423 two
14424 three
14425 additional edit
14426 "});
14427
14428 cx.set_state(indoc! {"
14429 one.second_completion
14430 twoˇ
14431 threeˇ
14432 additional edit
14433 "});
14434 cx.simulate_keystroke(" ");
14435 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14436 cx.simulate_keystroke("s");
14437 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14438
14439 cx.assert_editor_state(indoc! {"
14440 one.second_completion
14441 two sˇ
14442 three sˇ
14443 additional edit
14444 "});
14445 handle_completion_request(
14446 indoc! {"
14447 one.second_completion
14448 two s
14449 three <s|>
14450 additional edit
14451 "},
14452 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14453 true,
14454 counter.clone(),
14455 &mut cx,
14456 )
14457 .await;
14458 cx.condition(|editor, _| editor.context_menu_visible())
14459 .await;
14460 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14461
14462 cx.simulate_keystroke("i");
14463
14464 handle_completion_request(
14465 indoc! {"
14466 one.second_completion
14467 two si
14468 three <si|>
14469 additional edit
14470 "},
14471 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14472 true,
14473 counter.clone(),
14474 &mut cx,
14475 )
14476 .await;
14477 cx.condition(|editor, _| editor.context_menu_visible())
14478 .await;
14479 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14480
14481 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14482 editor
14483 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14484 .unwrap()
14485 });
14486 cx.assert_editor_state(indoc! {"
14487 one.second_completion
14488 two sixth_completionˇ
14489 three sixth_completionˇ
14490 additional edit
14491 "});
14492
14493 apply_additional_edits.await.unwrap();
14494
14495 update_test_language_settings(&mut cx, |settings| {
14496 settings.defaults.show_completions_on_input = Some(false);
14497 });
14498 cx.set_state("editorˇ");
14499 cx.simulate_keystroke(".");
14500 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14501 cx.simulate_keystrokes("c l o");
14502 cx.assert_editor_state("editor.cloˇ");
14503 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14504 cx.update_editor(|editor, window, cx| {
14505 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14506 });
14507 handle_completion_request(
14508 "editor.<clo|>",
14509 vec!["close", "clobber"],
14510 true,
14511 counter.clone(),
14512 &mut cx,
14513 )
14514 .await;
14515 cx.condition(|editor, _| editor.context_menu_visible())
14516 .await;
14517 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
14518
14519 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14520 editor
14521 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14522 .unwrap()
14523 });
14524 cx.assert_editor_state("editor.clobberˇ");
14525 handle_resolve_completion_request(&mut cx, None).await;
14526 apply_additional_edits.await.unwrap();
14527}
14528
14529#[gpui::test]
14530async fn test_completion_reuse(cx: &mut TestAppContext) {
14531 init_test(cx, |_| {});
14532
14533 let mut cx = EditorLspTestContext::new_rust(
14534 lsp::ServerCapabilities {
14535 completion_provider: Some(lsp::CompletionOptions {
14536 trigger_characters: Some(vec![".".to_string()]),
14537 ..Default::default()
14538 }),
14539 ..Default::default()
14540 },
14541 cx,
14542 )
14543 .await;
14544
14545 let counter = Arc::new(AtomicUsize::new(0));
14546 cx.set_state("objˇ");
14547 cx.simulate_keystroke(".");
14548
14549 // Initial completion request returns complete results
14550 let is_incomplete = false;
14551 handle_completion_request(
14552 "obj.|<>",
14553 vec!["a", "ab", "abc"],
14554 is_incomplete,
14555 counter.clone(),
14556 &mut cx,
14557 )
14558 .await;
14559 cx.run_until_parked();
14560 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14561 cx.assert_editor_state("obj.ˇ");
14562 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14563
14564 // Type "a" - filters existing completions
14565 cx.simulate_keystroke("a");
14566 cx.run_until_parked();
14567 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14568 cx.assert_editor_state("obj.aˇ");
14569 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14570
14571 // Type "b" - filters existing completions
14572 cx.simulate_keystroke("b");
14573 cx.run_until_parked();
14574 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14575 cx.assert_editor_state("obj.abˇ");
14576 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14577
14578 // Type "c" - filters existing completions
14579 cx.simulate_keystroke("c");
14580 cx.run_until_parked();
14581 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14582 cx.assert_editor_state("obj.abcˇ");
14583 check_displayed_completions(vec!["abc"], &mut cx);
14584
14585 // Backspace to delete "c" - filters existing completions
14586 cx.update_editor(|editor, window, cx| {
14587 editor.backspace(&Backspace, window, cx);
14588 });
14589 cx.run_until_parked();
14590 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14591 cx.assert_editor_state("obj.abˇ");
14592 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14593
14594 // Moving cursor to the left dismisses menu.
14595 cx.update_editor(|editor, window, cx| {
14596 editor.move_left(&MoveLeft, window, cx);
14597 });
14598 cx.run_until_parked();
14599 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14600 cx.assert_editor_state("obj.aˇb");
14601 cx.update_editor(|editor, _, _| {
14602 assert_eq!(editor.context_menu_visible(), false);
14603 });
14604
14605 // Type "b" - new request
14606 cx.simulate_keystroke("b");
14607 let is_incomplete = false;
14608 handle_completion_request(
14609 "obj.<ab|>a",
14610 vec!["ab", "abc"],
14611 is_incomplete,
14612 counter.clone(),
14613 &mut cx,
14614 )
14615 .await;
14616 cx.run_until_parked();
14617 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14618 cx.assert_editor_state("obj.abˇb");
14619 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14620
14621 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
14622 cx.update_editor(|editor, window, cx| {
14623 editor.backspace(&Backspace, window, cx);
14624 });
14625 let is_incomplete = false;
14626 handle_completion_request(
14627 "obj.<a|>b",
14628 vec!["a", "ab", "abc"],
14629 is_incomplete,
14630 counter.clone(),
14631 &mut cx,
14632 )
14633 .await;
14634 cx.run_until_parked();
14635 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14636 cx.assert_editor_state("obj.aˇb");
14637 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14638
14639 // Backspace to delete "a" - dismisses menu.
14640 cx.update_editor(|editor, window, cx| {
14641 editor.backspace(&Backspace, window, cx);
14642 });
14643 cx.run_until_parked();
14644 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14645 cx.assert_editor_state("obj.ˇb");
14646 cx.update_editor(|editor, _, _| {
14647 assert_eq!(editor.context_menu_visible(), false);
14648 });
14649}
14650
14651#[gpui::test]
14652async fn test_word_completion(cx: &mut TestAppContext) {
14653 let lsp_fetch_timeout_ms = 10;
14654 init_test(cx, |language_settings| {
14655 language_settings.defaults.completions = Some(CompletionSettingsContent {
14656 words_min_length: Some(0),
14657 lsp_fetch_timeout_ms: Some(10),
14658 lsp_insert_mode: Some(LspInsertMode::Insert),
14659 ..Default::default()
14660 });
14661 });
14662
14663 let mut cx = EditorLspTestContext::new_rust(
14664 lsp::ServerCapabilities {
14665 completion_provider: Some(lsp::CompletionOptions {
14666 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14667 ..lsp::CompletionOptions::default()
14668 }),
14669 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14670 ..lsp::ServerCapabilities::default()
14671 },
14672 cx,
14673 )
14674 .await;
14675
14676 let throttle_completions = Arc::new(AtomicBool::new(false));
14677
14678 let lsp_throttle_completions = throttle_completions.clone();
14679 let _completion_requests_handler =
14680 cx.lsp
14681 .server
14682 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
14683 let lsp_throttle_completions = lsp_throttle_completions.clone();
14684 let cx = cx.clone();
14685 async move {
14686 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
14687 cx.background_executor()
14688 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
14689 .await;
14690 }
14691 Ok(Some(lsp::CompletionResponse::Array(vec![
14692 lsp::CompletionItem {
14693 label: "first".into(),
14694 ..lsp::CompletionItem::default()
14695 },
14696 lsp::CompletionItem {
14697 label: "last".into(),
14698 ..lsp::CompletionItem::default()
14699 },
14700 ])))
14701 }
14702 });
14703
14704 cx.set_state(indoc! {"
14705 oneˇ
14706 two
14707 three
14708 "});
14709 cx.simulate_keystroke(".");
14710 cx.executor().run_until_parked();
14711 cx.condition(|editor, _| editor.context_menu_visible())
14712 .await;
14713 cx.update_editor(|editor, window, cx| {
14714 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14715 {
14716 assert_eq!(
14717 completion_menu_entries(menu),
14718 &["first", "last"],
14719 "When LSP server is fast to reply, no fallback word completions are used"
14720 );
14721 } else {
14722 panic!("expected completion menu to be open");
14723 }
14724 editor.cancel(&Cancel, window, cx);
14725 });
14726 cx.executor().run_until_parked();
14727 cx.condition(|editor, _| !editor.context_menu_visible())
14728 .await;
14729
14730 throttle_completions.store(true, atomic::Ordering::Release);
14731 cx.simulate_keystroke(".");
14732 cx.executor()
14733 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
14734 cx.executor().run_until_parked();
14735 cx.condition(|editor, _| editor.context_menu_visible())
14736 .await;
14737 cx.update_editor(|editor, _, _| {
14738 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14739 {
14740 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
14741 "When LSP server is slow, document words can be shown instead, if configured accordingly");
14742 } else {
14743 panic!("expected completion menu to be open");
14744 }
14745 });
14746}
14747
14748#[gpui::test]
14749async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
14750 init_test(cx, |language_settings| {
14751 language_settings.defaults.completions = Some(CompletionSettingsContent {
14752 words: Some(WordsCompletionMode::Enabled),
14753 words_min_length: Some(0),
14754 lsp_insert_mode: Some(LspInsertMode::Insert),
14755 ..Default::default()
14756 });
14757 });
14758
14759 let mut cx = EditorLspTestContext::new_rust(
14760 lsp::ServerCapabilities {
14761 completion_provider: Some(lsp::CompletionOptions {
14762 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14763 ..lsp::CompletionOptions::default()
14764 }),
14765 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14766 ..lsp::ServerCapabilities::default()
14767 },
14768 cx,
14769 )
14770 .await;
14771
14772 let _completion_requests_handler =
14773 cx.lsp
14774 .server
14775 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14776 Ok(Some(lsp::CompletionResponse::Array(vec![
14777 lsp::CompletionItem {
14778 label: "first".into(),
14779 ..lsp::CompletionItem::default()
14780 },
14781 lsp::CompletionItem {
14782 label: "last".into(),
14783 ..lsp::CompletionItem::default()
14784 },
14785 ])))
14786 });
14787
14788 cx.set_state(indoc! {"ˇ
14789 first
14790 last
14791 second
14792 "});
14793 cx.simulate_keystroke(".");
14794 cx.executor().run_until_parked();
14795 cx.condition(|editor, _| editor.context_menu_visible())
14796 .await;
14797 cx.update_editor(|editor, _, _| {
14798 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14799 {
14800 assert_eq!(
14801 completion_menu_entries(menu),
14802 &["first", "last", "second"],
14803 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
14804 );
14805 } else {
14806 panic!("expected completion menu to be open");
14807 }
14808 });
14809}
14810
14811#[gpui::test]
14812async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
14813 init_test(cx, |language_settings| {
14814 language_settings.defaults.completions = Some(CompletionSettingsContent {
14815 words: Some(WordsCompletionMode::Disabled),
14816 words_min_length: Some(0),
14817 lsp_insert_mode: Some(LspInsertMode::Insert),
14818 ..Default::default()
14819 });
14820 });
14821
14822 let mut cx = EditorLspTestContext::new_rust(
14823 lsp::ServerCapabilities {
14824 completion_provider: Some(lsp::CompletionOptions {
14825 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14826 ..lsp::CompletionOptions::default()
14827 }),
14828 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14829 ..lsp::ServerCapabilities::default()
14830 },
14831 cx,
14832 )
14833 .await;
14834
14835 let _completion_requests_handler =
14836 cx.lsp
14837 .server
14838 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14839 panic!("LSP completions should not be queried when dealing with word completions")
14840 });
14841
14842 cx.set_state(indoc! {"ˇ
14843 first
14844 last
14845 second
14846 "});
14847 cx.update_editor(|editor, window, cx| {
14848 editor.show_word_completions(&ShowWordCompletions, window, cx);
14849 });
14850 cx.executor().run_until_parked();
14851 cx.condition(|editor, _| editor.context_menu_visible())
14852 .await;
14853 cx.update_editor(|editor, _, _| {
14854 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14855 {
14856 assert_eq!(
14857 completion_menu_entries(menu),
14858 &["first", "last", "second"],
14859 "`ShowWordCompletions` action should show word completions"
14860 );
14861 } else {
14862 panic!("expected completion menu to be open");
14863 }
14864 });
14865
14866 cx.simulate_keystroke("l");
14867 cx.executor().run_until_parked();
14868 cx.condition(|editor, _| editor.context_menu_visible())
14869 .await;
14870 cx.update_editor(|editor, _, _| {
14871 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14872 {
14873 assert_eq!(
14874 completion_menu_entries(menu),
14875 &["last"],
14876 "After showing word completions, further editing should filter them and not query the LSP"
14877 );
14878 } else {
14879 panic!("expected completion menu to be open");
14880 }
14881 });
14882}
14883
14884#[gpui::test]
14885async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
14886 init_test(cx, |language_settings| {
14887 language_settings.defaults.completions = Some(CompletionSettingsContent {
14888 words_min_length: Some(0),
14889 lsp: Some(false),
14890 lsp_insert_mode: Some(LspInsertMode::Insert),
14891 ..Default::default()
14892 });
14893 });
14894
14895 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14896
14897 cx.set_state(indoc! {"ˇ
14898 0_usize
14899 let
14900 33
14901 4.5f32
14902 "});
14903 cx.update_editor(|editor, window, cx| {
14904 editor.show_completions(&ShowCompletions::default(), window, cx);
14905 });
14906 cx.executor().run_until_parked();
14907 cx.condition(|editor, _| editor.context_menu_visible())
14908 .await;
14909 cx.update_editor(|editor, window, cx| {
14910 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14911 {
14912 assert_eq!(
14913 completion_menu_entries(menu),
14914 &["let"],
14915 "With no digits in the completion query, no digits should be in the word completions"
14916 );
14917 } else {
14918 panic!("expected completion menu to be open");
14919 }
14920 editor.cancel(&Cancel, window, cx);
14921 });
14922
14923 cx.set_state(indoc! {"3ˇ
14924 0_usize
14925 let
14926 3
14927 33.35f32
14928 "});
14929 cx.update_editor(|editor, window, cx| {
14930 editor.show_completions(&ShowCompletions::default(), window, cx);
14931 });
14932 cx.executor().run_until_parked();
14933 cx.condition(|editor, _| editor.context_menu_visible())
14934 .await;
14935 cx.update_editor(|editor, _, _| {
14936 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14937 {
14938 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
14939 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
14940 } else {
14941 panic!("expected completion menu to be open");
14942 }
14943 });
14944}
14945
14946#[gpui::test]
14947async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
14948 init_test(cx, |language_settings| {
14949 language_settings.defaults.completions = Some(CompletionSettingsContent {
14950 words: Some(WordsCompletionMode::Enabled),
14951 words_min_length: Some(3),
14952 lsp_insert_mode: Some(LspInsertMode::Insert),
14953 ..Default::default()
14954 });
14955 });
14956
14957 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14958 cx.set_state(indoc! {"ˇ
14959 wow
14960 wowen
14961 wowser
14962 "});
14963 cx.simulate_keystroke("w");
14964 cx.executor().run_until_parked();
14965 cx.update_editor(|editor, _, _| {
14966 if editor.context_menu.borrow_mut().is_some() {
14967 panic!(
14968 "expected completion menu to be hidden, as words completion threshold is not met"
14969 );
14970 }
14971 });
14972
14973 cx.update_editor(|editor, window, cx| {
14974 editor.show_word_completions(&ShowWordCompletions, window, cx);
14975 });
14976 cx.executor().run_until_parked();
14977 cx.update_editor(|editor, window, cx| {
14978 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14979 {
14980 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");
14981 } else {
14982 panic!("expected completion menu to be open after the word completions are called with an action");
14983 }
14984
14985 editor.cancel(&Cancel, window, cx);
14986 });
14987 cx.update_editor(|editor, _, _| {
14988 if editor.context_menu.borrow_mut().is_some() {
14989 panic!("expected completion menu to be hidden after canceling");
14990 }
14991 });
14992
14993 cx.simulate_keystroke("o");
14994 cx.executor().run_until_parked();
14995 cx.update_editor(|editor, _, _| {
14996 if editor.context_menu.borrow_mut().is_some() {
14997 panic!(
14998 "expected completion menu to be hidden, as words completion threshold is not met still"
14999 );
15000 }
15001 });
15002
15003 cx.simulate_keystroke("w");
15004 cx.executor().run_until_parked();
15005 cx.update_editor(|editor, _, _| {
15006 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15007 {
15008 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
15009 } else {
15010 panic!("expected completion menu to be open after the word completions threshold is met");
15011 }
15012 });
15013}
15014
15015#[gpui::test]
15016async fn test_word_completions_disabled(cx: &mut TestAppContext) {
15017 init_test(cx, |language_settings| {
15018 language_settings.defaults.completions = Some(CompletionSettingsContent {
15019 words: Some(WordsCompletionMode::Enabled),
15020 words_min_length: Some(0),
15021 lsp_insert_mode: Some(LspInsertMode::Insert),
15022 ..Default::default()
15023 });
15024 });
15025
15026 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15027 cx.update_editor(|editor, _, _| {
15028 editor.disable_word_completions();
15029 });
15030 cx.set_state(indoc! {"ˇ
15031 wow
15032 wowen
15033 wowser
15034 "});
15035 cx.simulate_keystroke("w");
15036 cx.executor().run_until_parked();
15037 cx.update_editor(|editor, _, _| {
15038 if editor.context_menu.borrow_mut().is_some() {
15039 panic!(
15040 "expected completion menu to be hidden, as words completion are disabled for this editor"
15041 );
15042 }
15043 });
15044
15045 cx.update_editor(|editor, window, cx| {
15046 editor.show_word_completions(&ShowWordCompletions, window, cx);
15047 });
15048 cx.executor().run_until_parked();
15049 cx.update_editor(|editor, _, _| {
15050 if editor.context_menu.borrow_mut().is_some() {
15051 panic!(
15052 "expected completion menu to be hidden even if called for explicitly, as words completion are disabled for this editor"
15053 );
15054 }
15055 });
15056}
15057
15058fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
15059 let position = || lsp::Position {
15060 line: params.text_document_position.position.line,
15061 character: params.text_document_position.position.character,
15062 };
15063 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15064 range: lsp::Range {
15065 start: position(),
15066 end: position(),
15067 },
15068 new_text: text.to_string(),
15069 }))
15070}
15071
15072#[gpui::test]
15073async fn test_multiline_completion(cx: &mut TestAppContext) {
15074 init_test(cx, |_| {});
15075
15076 let fs = FakeFs::new(cx.executor());
15077 fs.insert_tree(
15078 path!("/a"),
15079 json!({
15080 "main.ts": "a",
15081 }),
15082 )
15083 .await;
15084
15085 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15086 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15087 let typescript_language = Arc::new(Language::new(
15088 LanguageConfig {
15089 name: "TypeScript".into(),
15090 matcher: LanguageMatcher {
15091 path_suffixes: vec!["ts".to_string()],
15092 ..LanguageMatcher::default()
15093 },
15094 line_comments: vec!["// ".into()],
15095 ..LanguageConfig::default()
15096 },
15097 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
15098 ));
15099 language_registry.add(typescript_language.clone());
15100 let mut fake_servers = language_registry.register_fake_lsp(
15101 "TypeScript",
15102 FakeLspAdapter {
15103 capabilities: lsp::ServerCapabilities {
15104 completion_provider: Some(lsp::CompletionOptions {
15105 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15106 ..lsp::CompletionOptions::default()
15107 }),
15108 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15109 ..lsp::ServerCapabilities::default()
15110 },
15111 // Emulate vtsls label generation
15112 label_for_completion: Some(Box::new(|item, _| {
15113 let text = if let Some(description) = item
15114 .label_details
15115 .as_ref()
15116 .and_then(|label_details| label_details.description.as_ref())
15117 {
15118 format!("{} {}", item.label, description)
15119 } else if let Some(detail) = &item.detail {
15120 format!("{} {}", item.label, detail)
15121 } else {
15122 item.label.clone()
15123 };
15124 Some(language::CodeLabel::plain(text, None))
15125 })),
15126 ..FakeLspAdapter::default()
15127 },
15128 );
15129 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15130 let cx = &mut VisualTestContext::from_window(*workspace, cx);
15131 let worktree_id = workspace
15132 .update(cx, |workspace, _window, cx| {
15133 workspace.project().update(cx, |project, cx| {
15134 project.worktrees(cx).next().unwrap().read(cx).id()
15135 })
15136 })
15137 .unwrap();
15138 let _buffer = project
15139 .update(cx, |project, cx| {
15140 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
15141 })
15142 .await
15143 .unwrap();
15144 let editor = workspace
15145 .update(cx, |workspace, window, cx| {
15146 workspace.open_path((worktree_id, rel_path("main.ts")), None, true, window, cx)
15147 })
15148 .unwrap()
15149 .await
15150 .unwrap()
15151 .downcast::<Editor>()
15152 .unwrap();
15153 let fake_server = fake_servers.next().await.unwrap();
15154
15155 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
15156 let multiline_label_2 = "a\nb\nc\n";
15157 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
15158 let multiline_description = "d\ne\nf\n";
15159 let multiline_detail_2 = "g\nh\ni\n";
15160
15161 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
15162 move |params, _| async move {
15163 Ok(Some(lsp::CompletionResponse::Array(vec![
15164 lsp::CompletionItem {
15165 label: multiline_label.to_string(),
15166 text_edit: gen_text_edit(¶ms, "new_text_1"),
15167 ..lsp::CompletionItem::default()
15168 },
15169 lsp::CompletionItem {
15170 label: "single line label 1".to_string(),
15171 detail: Some(multiline_detail.to_string()),
15172 text_edit: gen_text_edit(¶ms, "new_text_2"),
15173 ..lsp::CompletionItem::default()
15174 },
15175 lsp::CompletionItem {
15176 label: "single line label 2".to_string(),
15177 label_details: Some(lsp::CompletionItemLabelDetails {
15178 description: Some(multiline_description.to_string()),
15179 detail: None,
15180 }),
15181 text_edit: gen_text_edit(¶ms, "new_text_2"),
15182 ..lsp::CompletionItem::default()
15183 },
15184 lsp::CompletionItem {
15185 label: multiline_label_2.to_string(),
15186 detail: Some(multiline_detail_2.to_string()),
15187 text_edit: gen_text_edit(¶ms, "new_text_3"),
15188 ..lsp::CompletionItem::default()
15189 },
15190 lsp::CompletionItem {
15191 label: "Label with many spaces and \t but without newlines".to_string(),
15192 detail: Some(
15193 "Details with many spaces and \t but without newlines".to_string(),
15194 ),
15195 text_edit: gen_text_edit(¶ms, "new_text_4"),
15196 ..lsp::CompletionItem::default()
15197 },
15198 ])))
15199 },
15200 );
15201
15202 editor.update_in(cx, |editor, window, cx| {
15203 cx.focus_self(window);
15204 editor.move_to_end(&MoveToEnd, window, cx);
15205 editor.handle_input(".", window, cx);
15206 });
15207 cx.run_until_parked();
15208 completion_handle.next().await.unwrap();
15209
15210 editor.update(cx, |editor, _| {
15211 assert!(editor.context_menu_visible());
15212 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15213 {
15214 let completion_labels = menu
15215 .completions
15216 .borrow()
15217 .iter()
15218 .map(|c| c.label.text.clone())
15219 .collect::<Vec<_>>();
15220 assert_eq!(
15221 completion_labels,
15222 &[
15223 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
15224 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
15225 "single line label 2 d e f ",
15226 "a b c g h i ",
15227 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
15228 ],
15229 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
15230 );
15231
15232 for completion in menu
15233 .completions
15234 .borrow()
15235 .iter() {
15236 assert_eq!(
15237 completion.label.filter_range,
15238 0..completion.label.text.len(),
15239 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
15240 );
15241 }
15242 } else {
15243 panic!("expected completion menu to be open");
15244 }
15245 });
15246}
15247
15248#[gpui::test]
15249async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
15250 init_test(cx, |_| {});
15251 let mut cx = EditorLspTestContext::new_rust(
15252 lsp::ServerCapabilities {
15253 completion_provider: Some(lsp::CompletionOptions {
15254 trigger_characters: Some(vec![".".to_string()]),
15255 ..Default::default()
15256 }),
15257 ..Default::default()
15258 },
15259 cx,
15260 )
15261 .await;
15262 cx.lsp
15263 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15264 Ok(Some(lsp::CompletionResponse::Array(vec![
15265 lsp::CompletionItem {
15266 label: "first".into(),
15267 ..Default::default()
15268 },
15269 lsp::CompletionItem {
15270 label: "last".into(),
15271 ..Default::default()
15272 },
15273 ])))
15274 });
15275 cx.set_state("variableˇ");
15276 cx.simulate_keystroke(".");
15277 cx.executor().run_until_parked();
15278
15279 cx.update_editor(|editor, _, _| {
15280 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15281 {
15282 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
15283 } else {
15284 panic!("expected completion menu to be open");
15285 }
15286 });
15287
15288 cx.update_editor(|editor, window, cx| {
15289 editor.move_page_down(&MovePageDown::default(), window, cx);
15290 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15291 {
15292 assert!(
15293 menu.selected_item == 1,
15294 "expected PageDown to select the last item from the context menu"
15295 );
15296 } else {
15297 panic!("expected completion menu to stay open after PageDown");
15298 }
15299 });
15300
15301 cx.update_editor(|editor, window, cx| {
15302 editor.move_page_up(&MovePageUp::default(), window, cx);
15303 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15304 {
15305 assert!(
15306 menu.selected_item == 0,
15307 "expected PageUp to select the first item from the context menu"
15308 );
15309 } else {
15310 panic!("expected completion menu to stay open after PageUp");
15311 }
15312 });
15313}
15314
15315#[gpui::test]
15316async fn test_as_is_completions(cx: &mut TestAppContext) {
15317 init_test(cx, |_| {});
15318 let mut cx = EditorLspTestContext::new_rust(
15319 lsp::ServerCapabilities {
15320 completion_provider: Some(lsp::CompletionOptions {
15321 ..Default::default()
15322 }),
15323 ..Default::default()
15324 },
15325 cx,
15326 )
15327 .await;
15328 cx.lsp
15329 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15330 Ok(Some(lsp::CompletionResponse::Array(vec![
15331 lsp::CompletionItem {
15332 label: "unsafe".into(),
15333 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15334 range: lsp::Range {
15335 start: lsp::Position {
15336 line: 1,
15337 character: 2,
15338 },
15339 end: lsp::Position {
15340 line: 1,
15341 character: 3,
15342 },
15343 },
15344 new_text: "unsafe".to_string(),
15345 })),
15346 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
15347 ..Default::default()
15348 },
15349 ])))
15350 });
15351 cx.set_state("fn a() {}\n nˇ");
15352 cx.executor().run_until_parked();
15353 cx.update_editor(|editor, window, cx| {
15354 editor.show_completions(
15355 &ShowCompletions {
15356 trigger: Some("\n".into()),
15357 },
15358 window,
15359 cx,
15360 );
15361 });
15362 cx.executor().run_until_parked();
15363
15364 cx.update_editor(|editor, window, cx| {
15365 editor.confirm_completion(&Default::default(), window, cx)
15366 });
15367 cx.executor().run_until_parked();
15368 cx.assert_editor_state("fn a() {}\n unsafeˇ");
15369}
15370
15371#[gpui::test]
15372async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
15373 init_test(cx, |_| {});
15374 let language =
15375 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
15376 let mut cx = EditorLspTestContext::new(
15377 language,
15378 lsp::ServerCapabilities {
15379 completion_provider: Some(lsp::CompletionOptions {
15380 ..lsp::CompletionOptions::default()
15381 }),
15382 ..lsp::ServerCapabilities::default()
15383 },
15384 cx,
15385 )
15386 .await;
15387
15388 cx.set_state(
15389 "#ifndef BAR_H
15390#define BAR_H
15391
15392#include <stdbool.h>
15393
15394int fn_branch(bool do_branch1, bool do_branch2);
15395
15396#endif // BAR_H
15397ˇ",
15398 );
15399 cx.executor().run_until_parked();
15400 cx.update_editor(|editor, window, cx| {
15401 editor.handle_input("#", window, cx);
15402 });
15403 cx.executor().run_until_parked();
15404 cx.update_editor(|editor, window, cx| {
15405 editor.handle_input("i", window, cx);
15406 });
15407 cx.executor().run_until_parked();
15408 cx.update_editor(|editor, window, cx| {
15409 editor.handle_input("n", window, cx);
15410 });
15411 cx.executor().run_until_parked();
15412 cx.assert_editor_state(
15413 "#ifndef BAR_H
15414#define BAR_H
15415
15416#include <stdbool.h>
15417
15418int fn_branch(bool do_branch1, bool do_branch2);
15419
15420#endif // BAR_H
15421#inˇ",
15422 );
15423
15424 cx.lsp
15425 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15426 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15427 is_incomplete: false,
15428 item_defaults: None,
15429 items: vec![lsp::CompletionItem {
15430 kind: Some(lsp::CompletionItemKind::SNIPPET),
15431 label_details: Some(lsp::CompletionItemLabelDetails {
15432 detail: Some("header".to_string()),
15433 description: None,
15434 }),
15435 label: " include".to_string(),
15436 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15437 range: lsp::Range {
15438 start: lsp::Position {
15439 line: 8,
15440 character: 1,
15441 },
15442 end: lsp::Position {
15443 line: 8,
15444 character: 1,
15445 },
15446 },
15447 new_text: "include \"$0\"".to_string(),
15448 })),
15449 sort_text: Some("40b67681include".to_string()),
15450 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15451 filter_text: Some("include".to_string()),
15452 insert_text: Some("include \"$0\"".to_string()),
15453 ..lsp::CompletionItem::default()
15454 }],
15455 })))
15456 });
15457 cx.update_editor(|editor, window, cx| {
15458 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15459 });
15460 cx.executor().run_until_parked();
15461 cx.update_editor(|editor, window, cx| {
15462 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15463 });
15464 cx.executor().run_until_parked();
15465 cx.assert_editor_state(
15466 "#ifndef BAR_H
15467#define BAR_H
15468
15469#include <stdbool.h>
15470
15471int fn_branch(bool do_branch1, bool do_branch2);
15472
15473#endif // BAR_H
15474#include \"ˇ\"",
15475 );
15476
15477 cx.lsp
15478 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15479 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15480 is_incomplete: true,
15481 item_defaults: None,
15482 items: vec![lsp::CompletionItem {
15483 kind: Some(lsp::CompletionItemKind::FILE),
15484 label: "AGL/".to_string(),
15485 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15486 range: lsp::Range {
15487 start: lsp::Position {
15488 line: 8,
15489 character: 10,
15490 },
15491 end: lsp::Position {
15492 line: 8,
15493 character: 11,
15494 },
15495 },
15496 new_text: "AGL/".to_string(),
15497 })),
15498 sort_text: Some("40b67681AGL/".to_string()),
15499 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15500 filter_text: Some("AGL/".to_string()),
15501 insert_text: Some("AGL/".to_string()),
15502 ..lsp::CompletionItem::default()
15503 }],
15504 })))
15505 });
15506 cx.update_editor(|editor, window, cx| {
15507 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15508 });
15509 cx.executor().run_until_parked();
15510 cx.update_editor(|editor, window, cx| {
15511 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15512 });
15513 cx.executor().run_until_parked();
15514 cx.assert_editor_state(
15515 r##"#ifndef BAR_H
15516#define BAR_H
15517
15518#include <stdbool.h>
15519
15520int fn_branch(bool do_branch1, bool do_branch2);
15521
15522#endif // BAR_H
15523#include "AGL/ˇ"##,
15524 );
15525
15526 cx.update_editor(|editor, window, cx| {
15527 editor.handle_input("\"", window, cx);
15528 });
15529 cx.executor().run_until_parked();
15530 cx.assert_editor_state(
15531 r##"#ifndef BAR_H
15532#define BAR_H
15533
15534#include <stdbool.h>
15535
15536int fn_branch(bool do_branch1, bool do_branch2);
15537
15538#endif // BAR_H
15539#include "AGL/"ˇ"##,
15540 );
15541}
15542
15543#[gpui::test]
15544async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
15545 init_test(cx, |_| {});
15546
15547 let mut cx = EditorLspTestContext::new_rust(
15548 lsp::ServerCapabilities {
15549 completion_provider: Some(lsp::CompletionOptions {
15550 trigger_characters: Some(vec![".".to_string()]),
15551 resolve_provider: Some(true),
15552 ..Default::default()
15553 }),
15554 ..Default::default()
15555 },
15556 cx,
15557 )
15558 .await;
15559
15560 cx.set_state("fn main() { let a = 2ˇ; }");
15561 cx.simulate_keystroke(".");
15562 let completion_item = lsp::CompletionItem {
15563 label: "Some".into(),
15564 kind: Some(lsp::CompletionItemKind::SNIPPET),
15565 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15566 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15567 kind: lsp::MarkupKind::Markdown,
15568 value: "```rust\nSome(2)\n```".to_string(),
15569 })),
15570 deprecated: Some(false),
15571 sort_text: Some("Some".to_string()),
15572 filter_text: Some("Some".to_string()),
15573 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15574 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15575 range: lsp::Range {
15576 start: lsp::Position {
15577 line: 0,
15578 character: 22,
15579 },
15580 end: lsp::Position {
15581 line: 0,
15582 character: 22,
15583 },
15584 },
15585 new_text: "Some(2)".to_string(),
15586 })),
15587 additional_text_edits: Some(vec![lsp::TextEdit {
15588 range: lsp::Range {
15589 start: lsp::Position {
15590 line: 0,
15591 character: 20,
15592 },
15593 end: lsp::Position {
15594 line: 0,
15595 character: 22,
15596 },
15597 },
15598 new_text: "".to_string(),
15599 }]),
15600 ..Default::default()
15601 };
15602
15603 let closure_completion_item = completion_item.clone();
15604 let counter = Arc::new(AtomicUsize::new(0));
15605 let counter_clone = counter.clone();
15606 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15607 let task_completion_item = closure_completion_item.clone();
15608 counter_clone.fetch_add(1, atomic::Ordering::Release);
15609 async move {
15610 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15611 is_incomplete: true,
15612 item_defaults: None,
15613 items: vec![task_completion_item],
15614 })))
15615 }
15616 });
15617
15618 cx.condition(|editor, _| editor.context_menu_visible())
15619 .await;
15620 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
15621 assert!(request.next().await.is_some());
15622 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15623
15624 cx.simulate_keystrokes("S o m");
15625 cx.condition(|editor, _| editor.context_menu_visible())
15626 .await;
15627 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
15628 assert!(request.next().await.is_some());
15629 assert!(request.next().await.is_some());
15630 assert!(request.next().await.is_some());
15631 request.close();
15632 assert!(request.next().await.is_none());
15633 assert_eq!(
15634 counter.load(atomic::Ordering::Acquire),
15635 4,
15636 "With the completions menu open, only one LSP request should happen per input"
15637 );
15638}
15639
15640#[gpui::test]
15641async fn test_toggle_comment(cx: &mut TestAppContext) {
15642 init_test(cx, |_| {});
15643 let mut cx = EditorTestContext::new(cx).await;
15644 let language = Arc::new(Language::new(
15645 LanguageConfig {
15646 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15647 ..Default::default()
15648 },
15649 Some(tree_sitter_rust::LANGUAGE.into()),
15650 ));
15651 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15652
15653 // If multiple selections intersect a line, the line is only toggled once.
15654 cx.set_state(indoc! {"
15655 fn a() {
15656 «//b();
15657 ˇ»// «c();
15658 //ˇ» d();
15659 }
15660 "});
15661
15662 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15663
15664 cx.assert_editor_state(indoc! {"
15665 fn a() {
15666 «b();
15667 c();
15668 ˇ» d();
15669 }
15670 "});
15671
15672 // The comment prefix is inserted at the same column for every line in a
15673 // selection.
15674 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15675
15676 cx.assert_editor_state(indoc! {"
15677 fn a() {
15678 // «b();
15679 // c();
15680 ˇ»// d();
15681 }
15682 "});
15683
15684 // If a selection ends at the beginning of a line, that line is not toggled.
15685 cx.set_selections_state(indoc! {"
15686 fn a() {
15687 // b();
15688 «// c();
15689 ˇ» // d();
15690 }
15691 "});
15692
15693 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15694
15695 cx.assert_editor_state(indoc! {"
15696 fn a() {
15697 // b();
15698 «c();
15699 ˇ» // d();
15700 }
15701 "});
15702
15703 // If a selection span a single line and is empty, the line is toggled.
15704 cx.set_state(indoc! {"
15705 fn a() {
15706 a();
15707 b();
15708 ˇ
15709 }
15710 "});
15711
15712 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15713
15714 cx.assert_editor_state(indoc! {"
15715 fn a() {
15716 a();
15717 b();
15718 //•ˇ
15719 }
15720 "});
15721
15722 // If a selection span multiple lines, empty lines are not toggled.
15723 cx.set_state(indoc! {"
15724 fn a() {
15725 «a();
15726
15727 c();ˇ»
15728 }
15729 "});
15730
15731 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15732
15733 cx.assert_editor_state(indoc! {"
15734 fn a() {
15735 // «a();
15736
15737 // c();ˇ»
15738 }
15739 "});
15740
15741 // If a selection includes multiple comment prefixes, all lines are uncommented.
15742 cx.set_state(indoc! {"
15743 fn a() {
15744 «// a();
15745 /// b();
15746 //! c();ˇ»
15747 }
15748 "});
15749
15750 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15751
15752 cx.assert_editor_state(indoc! {"
15753 fn a() {
15754 «a();
15755 b();
15756 c();ˇ»
15757 }
15758 "});
15759}
15760
15761#[gpui::test]
15762async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
15763 init_test(cx, |_| {});
15764 let mut cx = EditorTestContext::new(cx).await;
15765 let language = Arc::new(Language::new(
15766 LanguageConfig {
15767 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15768 ..Default::default()
15769 },
15770 Some(tree_sitter_rust::LANGUAGE.into()),
15771 ));
15772 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15773
15774 let toggle_comments = &ToggleComments {
15775 advance_downwards: false,
15776 ignore_indent: true,
15777 };
15778
15779 // If multiple selections intersect a line, the line is only toggled once.
15780 cx.set_state(indoc! {"
15781 fn a() {
15782 // «b();
15783 // c();
15784 // ˇ» d();
15785 }
15786 "});
15787
15788 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15789
15790 cx.assert_editor_state(indoc! {"
15791 fn a() {
15792 «b();
15793 c();
15794 ˇ» d();
15795 }
15796 "});
15797
15798 // The comment prefix is inserted at the beginning of each line
15799 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15800
15801 cx.assert_editor_state(indoc! {"
15802 fn a() {
15803 // «b();
15804 // c();
15805 // ˇ» d();
15806 }
15807 "});
15808
15809 // If a selection ends at the beginning of a line, that line is not toggled.
15810 cx.set_selections_state(indoc! {"
15811 fn a() {
15812 // b();
15813 // «c();
15814 ˇ»// d();
15815 }
15816 "});
15817
15818 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15819
15820 cx.assert_editor_state(indoc! {"
15821 fn a() {
15822 // b();
15823 «c();
15824 ˇ»// d();
15825 }
15826 "});
15827
15828 // If a selection span a single line and is empty, the line is toggled.
15829 cx.set_state(indoc! {"
15830 fn a() {
15831 a();
15832 b();
15833 ˇ
15834 }
15835 "});
15836
15837 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15838
15839 cx.assert_editor_state(indoc! {"
15840 fn a() {
15841 a();
15842 b();
15843 //ˇ
15844 }
15845 "});
15846
15847 // If a selection span multiple lines, empty lines are not toggled.
15848 cx.set_state(indoc! {"
15849 fn a() {
15850 «a();
15851
15852 c();ˇ»
15853 }
15854 "});
15855
15856 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15857
15858 cx.assert_editor_state(indoc! {"
15859 fn a() {
15860 // «a();
15861
15862 // c();ˇ»
15863 }
15864 "});
15865
15866 // If a selection includes multiple comment prefixes, all lines are uncommented.
15867 cx.set_state(indoc! {"
15868 fn a() {
15869 // «a();
15870 /// b();
15871 //! c();ˇ»
15872 }
15873 "});
15874
15875 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15876
15877 cx.assert_editor_state(indoc! {"
15878 fn a() {
15879 «a();
15880 b();
15881 c();ˇ»
15882 }
15883 "});
15884}
15885
15886#[gpui::test]
15887async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
15888 init_test(cx, |_| {});
15889
15890 let language = Arc::new(Language::new(
15891 LanguageConfig {
15892 line_comments: vec!["// ".into()],
15893 ..Default::default()
15894 },
15895 Some(tree_sitter_rust::LANGUAGE.into()),
15896 ));
15897
15898 let mut cx = EditorTestContext::new(cx).await;
15899
15900 cx.language_registry().add(language.clone());
15901 cx.update_buffer(|buffer, cx| {
15902 buffer.set_language(Some(language), cx);
15903 });
15904
15905 let toggle_comments = &ToggleComments {
15906 advance_downwards: true,
15907 ignore_indent: false,
15908 };
15909
15910 // Single cursor on one line -> advance
15911 // Cursor moves horizontally 3 characters as well on non-blank line
15912 cx.set_state(indoc!(
15913 "fn a() {
15914 ˇdog();
15915 cat();
15916 }"
15917 ));
15918 cx.update_editor(|editor, window, cx| {
15919 editor.toggle_comments(toggle_comments, window, cx);
15920 });
15921 cx.assert_editor_state(indoc!(
15922 "fn a() {
15923 // dog();
15924 catˇ();
15925 }"
15926 ));
15927
15928 // Single selection on one line -> don't advance
15929 cx.set_state(indoc!(
15930 "fn a() {
15931 «dog()ˇ»;
15932 cat();
15933 }"
15934 ));
15935 cx.update_editor(|editor, window, cx| {
15936 editor.toggle_comments(toggle_comments, window, cx);
15937 });
15938 cx.assert_editor_state(indoc!(
15939 "fn a() {
15940 // «dog()ˇ»;
15941 cat();
15942 }"
15943 ));
15944
15945 // Multiple cursors on one line -> advance
15946 cx.set_state(indoc!(
15947 "fn a() {
15948 ˇdˇog();
15949 cat();
15950 }"
15951 ));
15952 cx.update_editor(|editor, window, cx| {
15953 editor.toggle_comments(toggle_comments, window, cx);
15954 });
15955 cx.assert_editor_state(indoc!(
15956 "fn a() {
15957 // dog();
15958 catˇ(ˇ);
15959 }"
15960 ));
15961
15962 // Multiple cursors on one line, with selection -> don't advance
15963 cx.set_state(indoc!(
15964 "fn a() {
15965 ˇdˇog«()ˇ»;
15966 cat();
15967 }"
15968 ));
15969 cx.update_editor(|editor, window, cx| {
15970 editor.toggle_comments(toggle_comments, window, cx);
15971 });
15972 cx.assert_editor_state(indoc!(
15973 "fn a() {
15974 // ˇdˇog«()ˇ»;
15975 cat();
15976 }"
15977 ));
15978
15979 // Single cursor on one line -> advance
15980 // Cursor moves to column 0 on blank line
15981 cx.set_state(indoc!(
15982 "fn a() {
15983 ˇdog();
15984
15985 cat();
15986 }"
15987 ));
15988 cx.update_editor(|editor, window, cx| {
15989 editor.toggle_comments(toggle_comments, window, cx);
15990 });
15991 cx.assert_editor_state(indoc!(
15992 "fn a() {
15993 // dog();
15994 ˇ
15995 cat();
15996 }"
15997 ));
15998
15999 // Single cursor on one line -> advance
16000 // Cursor starts and ends at column 0
16001 cx.set_state(indoc!(
16002 "fn a() {
16003 ˇ dog();
16004 cat();
16005 }"
16006 ));
16007 cx.update_editor(|editor, window, cx| {
16008 editor.toggle_comments(toggle_comments, window, cx);
16009 });
16010 cx.assert_editor_state(indoc!(
16011 "fn a() {
16012 // dog();
16013 ˇ cat();
16014 }"
16015 ));
16016}
16017
16018#[gpui::test]
16019async fn test_toggle_block_comment(cx: &mut TestAppContext) {
16020 init_test(cx, |_| {});
16021
16022 let mut cx = EditorTestContext::new(cx).await;
16023
16024 let html_language = Arc::new(
16025 Language::new(
16026 LanguageConfig {
16027 name: "HTML".into(),
16028 block_comment: Some(BlockCommentConfig {
16029 start: "<!-- ".into(),
16030 prefix: "".into(),
16031 end: " -->".into(),
16032 tab_size: 0,
16033 }),
16034 ..Default::default()
16035 },
16036 Some(tree_sitter_html::LANGUAGE.into()),
16037 )
16038 .with_injection_query(
16039 r#"
16040 (script_element
16041 (raw_text) @injection.content
16042 (#set! injection.language "javascript"))
16043 "#,
16044 )
16045 .unwrap(),
16046 );
16047
16048 let javascript_language = Arc::new(Language::new(
16049 LanguageConfig {
16050 name: "JavaScript".into(),
16051 line_comments: vec!["// ".into()],
16052 ..Default::default()
16053 },
16054 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
16055 ));
16056
16057 cx.language_registry().add(html_language.clone());
16058 cx.language_registry().add(javascript_language);
16059 cx.update_buffer(|buffer, cx| {
16060 buffer.set_language(Some(html_language), cx);
16061 });
16062
16063 // Toggle comments for empty selections
16064 cx.set_state(
16065 &r#"
16066 <p>A</p>ˇ
16067 <p>B</p>ˇ
16068 <p>C</p>ˇ
16069 "#
16070 .unindent(),
16071 );
16072 cx.update_editor(|editor, window, cx| {
16073 editor.toggle_comments(&ToggleComments::default(), window, cx)
16074 });
16075 cx.assert_editor_state(
16076 &r#"
16077 <!-- <p>A</p>ˇ -->
16078 <!-- <p>B</p>ˇ -->
16079 <!-- <p>C</p>ˇ -->
16080 "#
16081 .unindent(),
16082 );
16083 cx.update_editor(|editor, window, cx| {
16084 editor.toggle_comments(&ToggleComments::default(), window, cx)
16085 });
16086 cx.assert_editor_state(
16087 &r#"
16088 <p>A</p>ˇ
16089 <p>B</p>ˇ
16090 <p>C</p>ˇ
16091 "#
16092 .unindent(),
16093 );
16094
16095 // Toggle comments for mixture of empty and non-empty selections, where
16096 // multiple selections occupy a given line.
16097 cx.set_state(
16098 &r#"
16099 <p>A«</p>
16100 <p>ˇ»B</p>ˇ
16101 <p>C«</p>
16102 <p>ˇ»D</p>ˇ
16103 "#
16104 .unindent(),
16105 );
16106
16107 cx.update_editor(|editor, window, cx| {
16108 editor.toggle_comments(&ToggleComments::default(), window, cx)
16109 });
16110 cx.assert_editor_state(
16111 &r#"
16112 <!-- <p>A«</p>
16113 <p>ˇ»B</p>ˇ -->
16114 <!-- <p>C«</p>
16115 <p>ˇ»D</p>ˇ -->
16116 "#
16117 .unindent(),
16118 );
16119 cx.update_editor(|editor, window, cx| {
16120 editor.toggle_comments(&ToggleComments::default(), window, cx)
16121 });
16122 cx.assert_editor_state(
16123 &r#"
16124 <p>A«</p>
16125 <p>ˇ»B</p>ˇ
16126 <p>C«</p>
16127 <p>ˇ»D</p>ˇ
16128 "#
16129 .unindent(),
16130 );
16131
16132 // Toggle comments when different languages are active for different
16133 // selections.
16134 cx.set_state(
16135 &r#"
16136 ˇ<script>
16137 ˇvar x = new Y();
16138 ˇ</script>
16139 "#
16140 .unindent(),
16141 );
16142 cx.executor().run_until_parked();
16143 cx.update_editor(|editor, window, cx| {
16144 editor.toggle_comments(&ToggleComments::default(), window, cx)
16145 });
16146 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
16147 // Uncommenting and commenting from this position brings in even more wrong artifacts.
16148 cx.assert_editor_state(
16149 &r#"
16150 <!-- ˇ<script> -->
16151 // ˇvar x = new Y();
16152 <!-- ˇ</script> -->
16153 "#
16154 .unindent(),
16155 );
16156}
16157
16158#[gpui::test]
16159fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
16160 init_test(cx, |_| {});
16161
16162 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16163 let multibuffer = cx.new(|cx| {
16164 let mut multibuffer = MultiBuffer::new(ReadWrite);
16165 multibuffer.push_excerpts(
16166 buffer.clone(),
16167 [
16168 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
16169 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
16170 ],
16171 cx,
16172 );
16173 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
16174 multibuffer
16175 });
16176
16177 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
16178 editor.update_in(cx, |editor, window, cx| {
16179 assert_eq!(editor.text(cx), "aaaa\nbbbb");
16180 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16181 s.select_ranges([
16182 Point::new(0, 0)..Point::new(0, 0),
16183 Point::new(1, 0)..Point::new(1, 0),
16184 ])
16185 });
16186
16187 editor.handle_input("X", window, cx);
16188 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
16189 assert_eq!(
16190 editor.selections.ranges(&editor.display_snapshot(cx)),
16191 [
16192 Point::new(0, 1)..Point::new(0, 1),
16193 Point::new(1, 1)..Point::new(1, 1),
16194 ]
16195 );
16196
16197 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
16198 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16199 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
16200 });
16201 editor.backspace(&Default::default(), window, cx);
16202 assert_eq!(editor.text(cx), "Xa\nbbb");
16203 assert_eq!(
16204 editor.selections.ranges(&editor.display_snapshot(cx)),
16205 [Point::new(1, 0)..Point::new(1, 0)]
16206 );
16207
16208 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16209 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
16210 });
16211 editor.backspace(&Default::default(), window, cx);
16212 assert_eq!(editor.text(cx), "X\nbb");
16213 assert_eq!(
16214 editor.selections.ranges(&editor.display_snapshot(cx)),
16215 [Point::new(0, 1)..Point::new(0, 1)]
16216 );
16217 });
16218}
16219
16220#[gpui::test]
16221fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
16222 init_test(cx, |_| {});
16223
16224 let markers = vec![('[', ']').into(), ('(', ')').into()];
16225 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
16226 indoc! {"
16227 [aaaa
16228 (bbbb]
16229 cccc)",
16230 },
16231 markers.clone(),
16232 );
16233 let excerpt_ranges = markers.into_iter().map(|marker| {
16234 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
16235 ExcerptRange::new(context)
16236 });
16237 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
16238 let multibuffer = cx.new(|cx| {
16239 let mut multibuffer = MultiBuffer::new(ReadWrite);
16240 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
16241 multibuffer
16242 });
16243
16244 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
16245 editor.update_in(cx, |editor, window, cx| {
16246 let (expected_text, selection_ranges) = marked_text_ranges(
16247 indoc! {"
16248 aaaa
16249 bˇbbb
16250 bˇbbˇb
16251 cccc"
16252 },
16253 true,
16254 );
16255 assert_eq!(editor.text(cx), expected_text);
16256 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16257 s.select_ranges(selection_ranges)
16258 });
16259
16260 editor.handle_input("X", window, cx);
16261
16262 let (expected_text, expected_selections) = marked_text_ranges(
16263 indoc! {"
16264 aaaa
16265 bXˇbbXb
16266 bXˇbbXˇb
16267 cccc"
16268 },
16269 false,
16270 );
16271 assert_eq!(editor.text(cx), expected_text);
16272 assert_eq!(
16273 editor.selections.ranges(&editor.display_snapshot(cx)),
16274 expected_selections
16275 );
16276
16277 editor.newline(&Newline, window, cx);
16278 let (expected_text, expected_selections) = marked_text_ranges(
16279 indoc! {"
16280 aaaa
16281 bX
16282 ˇbbX
16283 b
16284 bX
16285 ˇbbX
16286 ˇb
16287 cccc"
16288 },
16289 false,
16290 );
16291 assert_eq!(editor.text(cx), expected_text);
16292 assert_eq!(
16293 editor.selections.ranges(&editor.display_snapshot(cx)),
16294 expected_selections
16295 );
16296 });
16297}
16298
16299#[gpui::test]
16300fn test_refresh_selections(cx: &mut TestAppContext) {
16301 init_test(cx, |_| {});
16302
16303 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16304 let mut excerpt1_id = None;
16305 let multibuffer = cx.new(|cx| {
16306 let mut multibuffer = MultiBuffer::new(ReadWrite);
16307 excerpt1_id = multibuffer
16308 .push_excerpts(
16309 buffer.clone(),
16310 [
16311 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
16312 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
16313 ],
16314 cx,
16315 )
16316 .into_iter()
16317 .next();
16318 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16319 multibuffer
16320 });
16321
16322 let editor = cx.add_window(|window, cx| {
16323 let mut editor = build_editor(multibuffer.clone(), window, cx);
16324 let snapshot = editor.snapshot(window, cx);
16325 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16326 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
16327 });
16328 editor.begin_selection(
16329 Point::new(2, 1).to_display_point(&snapshot),
16330 true,
16331 1,
16332 window,
16333 cx,
16334 );
16335 assert_eq!(
16336 editor.selections.ranges(&editor.display_snapshot(cx)),
16337 [
16338 Point::new(1, 3)..Point::new(1, 3),
16339 Point::new(2, 1)..Point::new(2, 1),
16340 ]
16341 );
16342 editor
16343 });
16344
16345 // Refreshing selections is a no-op when excerpts haven't changed.
16346 _ = editor.update(cx, |editor, window, cx| {
16347 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16348 assert_eq!(
16349 editor.selections.ranges(&editor.display_snapshot(cx)),
16350 [
16351 Point::new(1, 3)..Point::new(1, 3),
16352 Point::new(2, 1)..Point::new(2, 1),
16353 ]
16354 );
16355 });
16356
16357 multibuffer.update(cx, |multibuffer, cx| {
16358 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16359 });
16360 _ = editor.update(cx, |editor, window, cx| {
16361 // Removing an excerpt causes the first selection to become degenerate.
16362 assert_eq!(
16363 editor.selections.ranges(&editor.display_snapshot(cx)),
16364 [
16365 Point::new(0, 0)..Point::new(0, 0),
16366 Point::new(0, 1)..Point::new(0, 1)
16367 ]
16368 );
16369
16370 // Refreshing selections will relocate the first selection to the original buffer
16371 // location.
16372 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16373 assert_eq!(
16374 editor.selections.ranges(&editor.display_snapshot(cx)),
16375 [
16376 Point::new(0, 1)..Point::new(0, 1),
16377 Point::new(0, 3)..Point::new(0, 3)
16378 ]
16379 );
16380 assert!(editor.selections.pending_anchor().is_some());
16381 });
16382}
16383
16384#[gpui::test]
16385fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
16386 init_test(cx, |_| {});
16387
16388 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16389 let mut excerpt1_id = None;
16390 let multibuffer = cx.new(|cx| {
16391 let mut multibuffer = MultiBuffer::new(ReadWrite);
16392 excerpt1_id = multibuffer
16393 .push_excerpts(
16394 buffer.clone(),
16395 [
16396 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
16397 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
16398 ],
16399 cx,
16400 )
16401 .into_iter()
16402 .next();
16403 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16404 multibuffer
16405 });
16406
16407 let editor = cx.add_window(|window, cx| {
16408 let mut editor = build_editor(multibuffer.clone(), window, cx);
16409 let snapshot = editor.snapshot(window, cx);
16410 editor.begin_selection(
16411 Point::new(1, 3).to_display_point(&snapshot),
16412 false,
16413 1,
16414 window,
16415 cx,
16416 );
16417 assert_eq!(
16418 editor.selections.ranges(&editor.display_snapshot(cx)),
16419 [Point::new(1, 3)..Point::new(1, 3)]
16420 );
16421 editor
16422 });
16423
16424 multibuffer.update(cx, |multibuffer, cx| {
16425 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16426 });
16427 _ = editor.update(cx, |editor, window, cx| {
16428 assert_eq!(
16429 editor.selections.ranges(&editor.display_snapshot(cx)),
16430 [Point::new(0, 0)..Point::new(0, 0)]
16431 );
16432
16433 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
16434 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16435 assert_eq!(
16436 editor.selections.ranges(&editor.display_snapshot(cx)),
16437 [Point::new(0, 3)..Point::new(0, 3)]
16438 );
16439 assert!(editor.selections.pending_anchor().is_some());
16440 });
16441}
16442
16443#[gpui::test]
16444async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
16445 init_test(cx, |_| {});
16446
16447 let language = Arc::new(
16448 Language::new(
16449 LanguageConfig {
16450 brackets: BracketPairConfig {
16451 pairs: vec![
16452 BracketPair {
16453 start: "{".to_string(),
16454 end: "}".to_string(),
16455 close: true,
16456 surround: true,
16457 newline: true,
16458 },
16459 BracketPair {
16460 start: "/* ".to_string(),
16461 end: " */".to_string(),
16462 close: true,
16463 surround: true,
16464 newline: true,
16465 },
16466 ],
16467 ..Default::default()
16468 },
16469 ..Default::default()
16470 },
16471 Some(tree_sitter_rust::LANGUAGE.into()),
16472 )
16473 .with_indents_query("")
16474 .unwrap(),
16475 );
16476
16477 let text = concat!(
16478 "{ }\n", //
16479 " x\n", //
16480 " /* */\n", //
16481 "x\n", //
16482 "{{} }\n", //
16483 );
16484
16485 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
16486 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16487 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16488 editor
16489 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
16490 .await;
16491
16492 editor.update_in(cx, |editor, window, cx| {
16493 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16494 s.select_display_ranges([
16495 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
16496 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
16497 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
16498 ])
16499 });
16500 editor.newline(&Newline, window, cx);
16501
16502 assert_eq!(
16503 editor.buffer().read(cx).read(cx).text(),
16504 concat!(
16505 "{ \n", // Suppress rustfmt
16506 "\n", //
16507 "}\n", //
16508 " x\n", //
16509 " /* \n", //
16510 " \n", //
16511 " */\n", //
16512 "x\n", //
16513 "{{} \n", //
16514 "}\n", //
16515 )
16516 );
16517 });
16518}
16519
16520#[gpui::test]
16521fn test_highlighted_ranges(cx: &mut TestAppContext) {
16522 init_test(cx, |_| {});
16523
16524 let editor = cx.add_window(|window, cx| {
16525 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
16526 build_editor(buffer, window, cx)
16527 });
16528
16529 _ = editor.update(cx, |editor, window, cx| {
16530 struct Type1;
16531 struct Type2;
16532
16533 let buffer = editor.buffer.read(cx).snapshot(cx);
16534
16535 let anchor_range =
16536 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
16537
16538 editor.highlight_background::<Type1>(
16539 &[
16540 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
16541 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
16542 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
16543 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
16544 ],
16545 |_| Hsla::red(),
16546 cx,
16547 );
16548 editor.highlight_background::<Type2>(
16549 &[
16550 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
16551 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
16552 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
16553 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
16554 ],
16555 |_| Hsla::green(),
16556 cx,
16557 );
16558
16559 let snapshot = editor.snapshot(window, cx);
16560 let highlighted_ranges = editor.sorted_background_highlights_in_range(
16561 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
16562 &snapshot,
16563 cx.theme(),
16564 );
16565 assert_eq!(
16566 highlighted_ranges,
16567 &[
16568 (
16569 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
16570 Hsla::green(),
16571 ),
16572 (
16573 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
16574 Hsla::red(),
16575 ),
16576 (
16577 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
16578 Hsla::green(),
16579 ),
16580 (
16581 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16582 Hsla::red(),
16583 ),
16584 ]
16585 );
16586 assert_eq!(
16587 editor.sorted_background_highlights_in_range(
16588 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
16589 &snapshot,
16590 cx.theme(),
16591 ),
16592 &[(
16593 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16594 Hsla::red(),
16595 )]
16596 );
16597 });
16598}
16599
16600#[gpui::test]
16601async fn test_following(cx: &mut TestAppContext) {
16602 init_test(cx, |_| {});
16603
16604 let fs = FakeFs::new(cx.executor());
16605 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16606
16607 let buffer = project.update(cx, |project, cx| {
16608 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, false, cx);
16609 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
16610 });
16611 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
16612 let follower = cx.update(|cx| {
16613 cx.open_window(
16614 WindowOptions {
16615 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
16616 gpui::Point::new(px(0.), px(0.)),
16617 gpui::Point::new(px(10.), px(80.)),
16618 ))),
16619 ..Default::default()
16620 },
16621 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
16622 )
16623 .unwrap()
16624 });
16625
16626 let is_still_following = Rc::new(RefCell::new(true));
16627 let follower_edit_event_count = Rc::new(RefCell::new(0));
16628 let pending_update = Rc::new(RefCell::new(None));
16629 let leader_entity = leader.root(cx).unwrap();
16630 let follower_entity = follower.root(cx).unwrap();
16631 _ = follower.update(cx, {
16632 let update = pending_update.clone();
16633 let is_still_following = is_still_following.clone();
16634 let follower_edit_event_count = follower_edit_event_count.clone();
16635 |_, window, cx| {
16636 cx.subscribe_in(
16637 &leader_entity,
16638 window,
16639 move |_, leader, event, window, cx| {
16640 leader.read(cx).add_event_to_update_proto(
16641 event,
16642 &mut update.borrow_mut(),
16643 window,
16644 cx,
16645 );
16646 },
16647 )
16648 .detach();
16649
16650 cx.subscribe_in(
16651 &follower_entity,
16652 window,
16653 move |_, _, event: &EditorEvent, _window, _cx| {
16654 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
16655 *is_still_following.borrow_mut() = false;
16656 }
16657
16658 if let EditorEvent::BufferEdited = event {
16659 *follower_edit_event_count.borrow_mut() += 1;
16660 }
16661 },
16662 )
16663 .detach();
16664 }
16665 });
16666
16667 // Update the selections only
16668 _ = leader.update(cx, |leader, window, cx| {
16669 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16670 s.select_ranges([1..1])
16671 });
16672 });
16673 follower
16674 .update(cx, |follower, window, cx| {
16675 follower.apply_update_proto(
16676 &project,
16677 pending_update.borrow_mut().take().unwrap(),
16678 window,
16679 cx,
16680 )
16681 })
16682 .unwrap()
16683 .await
16684 .unwrap();
16685 _ = follower.update(cx, |follower, _, cx| {
16686 assert_eq!(
16687 follower.selections.ranges(&follower.display_snapshot(cx)),
16688 vec![1..1]
16689 );
16690 });
16691 assert!(*is_still_following.borrow());
16692 assert_eq!(*follower_edit_event_count.borrow(), 0);
16693
16694 // Update the scroll position only
16695 _ = leader.update(cx, |leader, window, cx| {
16696 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16697 });
16698 follower
16699 .update(cx, |follower, window, cx| {
16700 follower.apply_update_proto(
16701 &project,
16702 pending_update.borrow_mut().take().unwrap(),
16703 window,
16704 cx,
16705 )
16706 })
16707 .unwrap()
16708 .await
16709 .unwrap();
16710 assert_eq!(
16711 follower
16712 .update(cx, |follower, _, cx| follower.scroll_position(cx))
16713 .unwrap(),
16714 gpui::Point::new(1.5, 3.5)
16715 );
16716 assert!(*is_still_following.borrow());
16717 assert_eq!(*follower_edit_event_count.borrow(), 0);
16718
16719 // Update the selections and scroll position. The follower's scroll position is updated
16720 // via autoscroll, not via the leader's exact scroll position.
16721 _ = leader.update(cx, |leader, window, cx| {
16722 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16723 s.select_ranges([0..0])
16724 });
16725 leader.request_autoscroll(Autoscroll::newest(), cx);
16726 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16727 });
16728 follower
16729 .update(cx, |follower, window, cx| {
16730 follower.apply_update_proto(
16731 &project,
16732 pending_update.borrow_mut().take().unwrap(),
16733 window,
16734 cx,
16735 )
16736 })
16737 .unwrap()
16738 .await
16739 .unwrap();
16740 _ = follower.update(cx, |follower, _, cx| {
16741 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
16742 assert_eq!(
16743 follower.selections.ranges(&follower.display_snapshot(cx)),
16744 vec![0..0]
16745 );
16746 });
16747 assert!(*is_still_following.borrow());
16748
16749 // Creating a pending selection that precedes another selection
16750 _ = leader.update(cx, |leader, window, cx| {
16751 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16752 s.select_ranges([1..1])
16753 });
16754 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
16755 });
16756 follower
16757 .update(cx, |follower, window, cx| {
16758 follower.apply_update_proto(
16759 &project,
16760 pending_update.borrow_mut().take().unwrap(),
16761 window,
16762 cx,
16763 )
16764 })
16765 .unwrap()
16766 .await
16767 .unwrap();
16768 _ = follower.update(cx, |follower, _, cx| {
16769 assert_eq!(
16770 follower.selections.ranges(&follower.display_snapshot(cx)),
16771 vec![0..0, 1..1]
16772 );
16773 });
16774 assert!(*is_still_following.borrow());
16775
16776 // Extend the pending selection so that it surrounds another selection
16777 _ = leader.update(cx, |leader, window, cx| {
16778 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
16779 });
16780 follower
16781 .update(cx, |follower, window, cx| {
16782 follower.apply_update_proto(
16783 &project,
16784 pending_update.borrow_mut().take().unwrap(),
16785 window,
16786 cx,
16787 )
16788 })
16789 .unwrap()
16790 .await
16791 .unwrap();
16792 _ = follower.update(cx, |follower, _, cx| {
16793 assert_eq!(
16794 follower.selections.ranges(&follower.display_snapshot(cx)),
16795 vec![0..2]
16796 );
16797 });
16798
16799 // Scrolling locally breaks the follow
16800 _ = follower.update(cx, |follower, window, cx| {
16801 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
16802 follower.set_scroll_anchor(
16803 ScrollAnchor {
16804 anchor: top_anchor,
16805 offset: gpui::Point::new(0.0, 0.5),
16806 },
16807 window,
16808 cx,
16809 );
16810 });
16811 assert!(!(*is_still_following.borrow()));
16812}
16813
16814#[gpui::test]
16815async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
16816 init_test(cx, |_| {});
16817
16818 let fs = FakeFs::new(cx.executor());
16819 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16820 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16821 let pane = workspace
16822 .update(cx, |workspace, _, _| workspace.active_pane().clone())
16823 .unwrap();
16824
16825 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16826
16827 let leader = pane.update_in(cx, |_, window, cx| {
16828 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
16829 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
16830 });
16831
16832 // Start following the editor when it has no excerpts.
16833 let mut state_message =
16834 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16835 let workspace_entity = workspace.root(cx).unwrap();
16836 let follower_1 = cx
16837 .update_window(*workspace.deref(), |_, window, cx| {
16838 Editor::from_state_proto(
16839 workspace_entity,
16840 ViewId {
16841 creator: CollaboratorId::PeerId(PeerId::default()),
16842 id: 0,
16843 },
16844 &mut state_message,
16845 window,
16846 cx,
16847 )
16848 })
16849 .unwrap()
16850 .unwrap()
16851 .await
16852 .unwrap();
16853
16854 let update_message = Rc::new(RefCell::new(None));
16855 follower_1.update_in(cx, {
16856 let update = update_message.clone();
16857 |_, window, cx| {
16858 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
16859 leader.read(cx).add_event_to_update_proto(
16860 event,
16861 &mut update.borrow_mut(),
16862 window,
16863 cx,
16864 );
16865 })
16866 .detach();
16867 }
16868 });
16869
16870 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
16871 (
16872 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, false, cx),
16873 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, false, cx),
16874 )
16875 });
16876
16877 // Insert some excerpts.
16878 leader.update(cx, |leader, cx| {
16879 leader.buffer.update(cx, |multibuffer, cx| {
16880 multibuffer.set_excerpts_for_path(
16881 PathKey::with_sort_prefix(1, rel_path("b.txt").into_arc()),
16882 buffer_1.clone(),
16883 vec![
16884 Point::row_range(0..3),
16885 Point::row_range(1..6),
16886 Point::row_range(12..15),
16887 ],
16888 0,
16889 cx,
16890 );
16891 multibuffer.set_excerpts_for_path(
16892 PathKey::with_sort_prefix(1, rel_path("a.txt").into_arc()),
16893 buffer_2.clone(),
16894 vec![Point::row_range(0..6), Point::row_range(8..12)],
16895 0,
16896 cx,
16897 );
16898 });
16899 });
16900
16901 // Apply the update of adding the excerpts.
16902 follower_1
16903 .update_in(cx, |follower, window, cx| {
16904 follower.apply_update_proto(
16905 &project,
16906 update_message.borrow().clone().unwrap(),
16907 window,
16908 cx,
16909 )
16910 })
16911 .await
16912 .unwrap();
16913 assert_eq!(
16914 follower_1.update(cx, |editor, cx| editor.text(cx)),
16915 leader.update(cx, |editor, cx| editor.text(cx))
16916 );
16917 update_message.borrow_mut().take();
16918
16919 // Start following separately after it already has excerpts.
16920 let mut state_message =
16921 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16922 let workspace_entity = workspace.root(cx).unwrap();
16923 let follower_2 = cx
16924 .update_window(*workspace.deref(), |_, window, cx| {
16925 Editor::from_state_proto(
16926 workspace_entity,
16927 ViewId {
16928 creator: CollaboratorId::PeerId(PeerId::default()),
16929 id: 0,
16930 },
16931 &mut state_message,
16932 window,
16933 cx,
16934 )
16935 })
16936 .unwrap()
16937 .unwrap()
16938 .await
16939 .unwrap();
16940 assert_eq!(
16941 follower_2.update(cx, |editor, cx| editor.text(cx)),
16942 leader.update(cx, |editor, cx| editor.text(cx))
16943 );
16944
16945 // Remove some excerpts.
16946 leader.update(cx, |leader, cx| {
16947 leader.buffer.update(cx, |multibuffer, cx| {
16948 let excerpt_ids = multibuffer.excerpt_ids();
16949 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
16950 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
16951 });
16952 });
16953
16954 // Apply the update of removing the excerpts.
16955 follower_1
16956 .update_in(cx, |follower, window, cx| {
16957 follower.apply_update_proto(
16958 &project,
16959 update_message.borrow().clone().unwrap(),
16960 window,
16961 cx,
16962 )
16963 })
16964 .await
16965 .unwrap();
16966 follower_2
16967 .update_in(cx, |follower, window, cx| {
16968 follower.apply_update_proto(
16969 &project,
16970 update_message.borrow().clone().unwrap(),
16971 window,
16972 cx,
16973 )
16974 })
16975 .await
16976 .unwrap();
16977 update_message.borrow_mut().take();
16978 assert_eq!(
16979 follower_1.update(cx, |editor, cx| editor.text(cx)),
16980 leader.update(cx, |editor, cx| editor.text(cx))
16981 );
16982}
16983
16984#[gpui::test]
16985async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16986 init_test(cx, |_| {});
16987
16988 let mut cx = EditorTestContext::new(cx).await;
16989 let lsp_store =
16990 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
16991
16992 cx.set_state(indoc! {"
16993 ˇfn func(abc def: i32) -> u32 {
16994 }
16995 "});
16996
16997 cx.update(|_, cx| {
16998 lsp_store.update(cx, |lsp_store, cx| {
16999 lsp_store
17000 .update_diagnostics(
17001 LanguageServerId(0),
17002 lsp::PublishDiagnosticsParams {
17003 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
17004 version: None,
17005 diagnostics: vec![
17006 lsp::Diagnostic {
17007 range: lsp::Range::new(
17008 lsp::Position::new(0, 11),
17009 lsp::Position::new(0, 12),
17010 ),
17011 severity: Some(lsp::DiagnosticSeverity::ERROR),
17012 ..Default::default()
17013 },
17014 lsp::Diagnostic {
17015 range: lsp::Range::new(
17016 lsp::Position::new(0, 12),
17017 lsp::Position::new(0, 15),
17018 ),
17019 severity: Some(lsp::DiagnosticSeverity::ERROR),
17020 ..Default::default()
17021 },
17022 lsp::Diagnostic {
17023 range: lsp::Range::new(
17024 lsp::Position::new(0, 25),
17025 lsp::Position::new(0, 28),
17026 ),
17027 severity: Some(lsp::DiagnosticSeverity::ERROR),
17028 ..Default::default()
17029 },
17030 ],
17031 },
17032 None,
17033 DiagnosticSourceKind::Pushed,
17034 &[],
17035 cx,
17036 )
17037 .unwrap()
17038 });
17039 });
17040
17041 executor.run_until_parked();
17042
17043 cx.update_editor(|editor, window, cx| {
17044 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17045 });
17046
17047 cx.assert_editor_state(indoc! {"
17048 fn func(abc def: i32) -> ˇu32 {
17049 }
17050 "});
17051
17052 cx.update_editor(|editor, window, cx| {
17053 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17054 });
17055
17056 cx.assert_editor_state(indoc! {"
17057 fn func(abc ˇdef: i32) -> u32 {
17058 }
17059 "});
17060
17061 cx.update_editor(|editor, window, cx| {
17062 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17063 });
17064
17065 cx.assert_editor_state(indoc! {"
17066 fn func(abcˇ def: i32) -> u32 {
17067 }
17068 "});
17069
17070 cx.update_editor(|editor, window, cx| {
17071 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17072 });
17073
17074 cx.assert_editor_state(indoc! {"
17075 fn func(abc def: i32) -> ˇu32 {
17076 }
17077 "});
17078}
17079
17080#[gpui::test]
17081async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17082 init_test(cx, |_| {});
17083
17084 let mut cx = EditorTestContext::new(cx).await;
17085
17086 let diff_base = r#"
17087 use some::mod;
17088
17089 const A: u32 = 42;
17090
17091 fn main() {
17092 println!("hello");
17093
17094 println!("world");
17095 }
17096 "#
17097 .unindent();
17098
17099 // Edits are modified, removed, modified, added
17100 cx.set_state(
17101 &r#"
17102 use some::modified;
17103
17104 ˇ
17105 fn main() {
17106 println!("hello there");
17107
17108 println!("around the");
17109 println!("world");
17110 }
17111 "#
17112 .unindent(),
17113 );
17114
17115 cx.set_head_text(&diff_base);
17116 executor.run_until_parked();
17117
17118 cx.update_editor(|editor, window, cx| {
17119 //Wrap around the bottom of the buffer
17120 for _ in 0..3 {
17121 editor.go_to_next_hunk(&GoToHunk, window, cx);
17122 }
17123 });
17124
17125 cx.assert_editor_state(
17126 &r#"
17127 ˇuse some::modified;
17128
17129
17130 fn main() {
17131 println!("hello there");
17132
17133 println!("around the");
17134 println!("world");
17135 }
17136 "#
17137 .unindent(),
17138 );
17139
17140 cx.update_editor(|editor, window, cx| {
17141 //Wrap around the top of the buffer
17142 for _ in 0..2 {
17143 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17144 }
17145 });
17146
17147 cx.assert_editor_state(
17148 &r#"
17149 use some::modified;
17150
17151
17152 fn main() {
17153 ˇ println!("hello there");
17154
17155 println!("around the");
17156 println!("world");
17157 }
17158 "#
17159 .unindent(),
17160 );
17161
17162 cx.update_editor(|editor, window, cx| {
17163 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17164 });
17165
17166 cx.assert_editor_state(
17167 &r#"
17168 use some::modified;
17169
17170 ˇ
17171 fn main() {
17172 println!("hello there");
17173
17174 println!("around the");
17175 println!("world");
17176 }
17177 "#
17178 .unindent(),
17179 );
17180
17181 cx.update_editor(|editor, window, cx| {
17182 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17183 });
17184
17185 cx.assert_editor_state(
17186 &r#"
17187 ˇuse some::modified;
17188
17189
17190 fn main() {
17191 println!("hello there");
17192
17193 println!("around the");
17194 println!("world");
17195 }
17196 "#
17197 .unindent(),
17198 );
17199
17200 cx.update_editor(|editor, window, cx| {
17201 for _ in 0..2 {
17202 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17203 }
17204 });
17205
17206 cx.assert_editor_state(
17207 &r#"
17208 use some::modified;
17209
17210
17211 fn main() {
17212 ˇ println!("hello there");
17213
17214 println!("around the");
17215 println!("world");
17216 }
17217 "#
17218 .unindent(),
17219 );
17220
17221 cx.update_editor(|editor, window, cx| {
17222 editor.fold(&Fold, window, cx);
17223 });
17224
17225 cx.update_editor(|editor, window, cx| {
17226 editor.go_to_next_hunk(&GoToHunk, window, cx);
17227 });
17228
17229 cx.assert_editor_state(
17230 &r#"
17231 ˇuse some::modified;
17232
17233
17234 fn main() {
17235 println!("hello there");
17236
17237 println!("around the");
17238 println!("world");
17239 }
17240 "#
17241 .unindent(),
17242 );
17243}
17244
17245#[test]
17246fn test_split_words() {
17247 fn split(text: &str) -> Vec<&str> {
17248 split_words(text).collect()
17249 }
17250
17251 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
17252 assert_eq!(split("hello_world"), &["hello_", "world"]);
17253 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
17254 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
17255 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
17256 assert_eq!(split("helloworld"), &["helloworld"]);
17257
17258 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
17259}
17260
17261#[gpui::test]
17262async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
17263 init_test(cx, |_| {});
17264
17265 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
17266 let mut assert = |before, after| {
17267 let _state_context = cx.set_state(before);
17268 cx.run_until_parked();
17269 cx.update_editor(|editor, window, cx| {
17270 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
17271 });
17272 cx.run_until_parked();
17273 cx.assert_editor_state(after);
17274 };
17275
17276 // Outside bracket jumps to outside of matching bracket
17277 assert("console.logˇ(var);", "console.log(var)ˇ;");
17278 assert("console.log(var)ˇ;", "console.logˇ(var);");
17279
17280 // Inside bracket jumps to inside of matching bracket
17281 assert("console.log(ˇvar);", "console.log(varˇ);");
17282 assert("console.log(varˇ);", "console.log(ˇvar);");
17283
17284 // When outside a bracket and inside, favor jumping to the inside bracket
17285 assert(
17286 "console.log('foo', [1, 2, 3]ˇ);",
17287 "console.log(ˇ'foo', [1, 2, 3]);",
17288 );
17289 assert(
17290 "console.log(ˇ'foo', [1, 2, 3]);",
17291 "console.log('foo', [1, 2, 3]ˇ);",
17292 );
17293
17294 // Bias forward if two options are equally likely
17295 assert(
17296 "let result = curried_fun()ˇ();",
17297 "let result = curried_fun()()ˇ;",
17298 );
17299
17300 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
17301 assert(
17302 indoc! {"
17303 function test() {
17304 console.log('test')ˇ
17305 }"},
17306 indoc! {"
17307 function test() {
17308 console.logˇ('test')
17309 }"},
17310 );
17311}
17312
17313#[gpui::test]
17314async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
17315 init_test(cx, |_| {});
17316
17317 let fs = FakeFs::new(cx.executor());
17318 fs.insert_tree(
17319 path!("/a"),
17320 json!({
17321 "main.rs": "fn main() { let a = 5; }",
17322 "other.rs": "// Test file",
17323 }),
17324 )
17325 .await;
17326 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17327
17328 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17329 language_registry.add(Arc::new(Language::new(
17330 LanguageConfig {
17331 name: "Rust".into(),
17332 matcher: LanguageMatcher {
17333 path_suffixes: vec!["rs".to_string()],
17334 ..Default::default()
17335 },
17336 brackets: BracketPairConfig {
17337 pairs: vec![BracketPair {
17338 start: "{".to_string(),
17339 end: "}".to_string(),
17340 close: true,
17341 surround: true,
17342 newline: true,
17343 }],
17344 disabled_scopes_by_bracket_ix: Vec::new(),
17345 },
17346 ..Default::default()
17347 },
17348 Some(tree_sitter_rust::LANGUAGE.into()),
17349 )));
17350 let mut fake_servers = language_registry.register_fake_lsp(
17351 "Rust",
17352 FakeLspAdapter {
17353 capabilities: lsp::ServerCapabilities {
17354 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
17355 first_trigger_character: "{".to_string(),
17356 more_trigger_character: None,
17357 }),
17358 ..Default::default()
17359 },
17360 ..Default::default()
17361 },
17362 );
17363
17364 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17365
17366 let cx = &mut VisualTestContext::from_window(*workspace, cx);
17367
17368 let worktree_id = workspace
17369 .update(cx, |workspace, _, cx| {
17370 workspace.project().update(cx, |project, cx| {
17371 project.worktrees(cx).next().unwrap().read(cx).id()
17372 })
17373 })
17374 .unwrap();
17375
17376 let buffer = project
17377 .update(cx, |project, cx| {
17378 project.open_local_buffer(path!("/a/main.rs"), cx)
17379 })
17380 .await
17381 .unwrap();
17382 let editor_handle = workspace
17383 .update(cx, |workspace, window, cx| {
17384 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
17385 })
17386 .unwrap()
17387 .await
17388 .unwrap()
17389 .downcast::<Editor>()
17390 .unwrap();
17391
17392 cx.executor().start_waiting();
17393 let fake_server = fake_servers.next().await.unwrap();
17394
17395 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
17396 |params, _| async move {
17397 assert_eq!(
17398 params.text_document_position.text_document.uri,
17399 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
17400 );
17401 assert_eq!(
17402 params.text_document_position.position,
17403 lsp::Position::new(0, 21),
17404 );
17405
17406 Ok(Some(vec![lsp::TextEdit {
17407 new_text: "]".to_string(),
17408 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17409 }]))
17410 },
17411 );
17412
17413 editor_handle.update_in(cx, |editor, window, cx| {
17414 window.focus(&editor.focus_handle(cx));
17415 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17416 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
17417 });
17418 editor.handle_input("{", window, cx);
17419 });
17420
17421 cx.executor().run_until_parked();
17422
17423 buffer.update(cx, |buffer, _| {
17424 assert_eq!(
17425 buffer.text(),
17426 "fn main() { let a = {5}; }",
17427 "No extra braces from on type formatting should appear in the buffer"
17428 )
17429 });
17430}
17431
17432#[gpui::test(iterations = 20, seeds(31))]
17433async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
17434 init_test(cx, |_| {});
17435
17436 let mut cx = EditorLspTestContext::new_rust(
17437 lsp::ServerCapabilities {
17438 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
17439 first_trigger_character: ".".to_string(),
17440 more_trigger_character: None,
17441 }),
17442 ..Default::default()
17443 },
17444 cx,
17445 )
17446 .await;
17447
17448 cx.update_buffer(|buffer, _| {
17449 // This causes autoindent to be async.
17450 buffer.set_sync_parse_timeout(Duration::ZERO)
17451 });
17452
17453 cx.set_state("fn c() {\n d()ˇ\n}\n");
17454 cx.simulate_keystroke("\n");
17455 cx.run_until_parked();
17456
17457 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
17458 let mut request =
17459 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
17460 let buffer_cloned = buffer_cloned.clone();
17461 async move {
17462 buffer_cloned.update(&mut cx, |buffer, _| {
17463 assert_eq!(
17464 buffer.text(),
17465 "fn c() {\n d()\n .\n}\n",
17466 "OnTypeFormatting should triggered after autoindent applied"
17467 )
17468 })?;
17469
17470 Ok(Some(vec![]))
17471 }
17472 });
17473
17474 cx.simulate_keystroke(".");
17475 cx.run_until_parked();
17476
17477 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
17478 assert!(request.next().await.is_some());
17479 request.close();
17480 assert!(request.next().await.is_none());
17481}
17482
17483#[gpui::test]
17484async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
17485 init_test(cx, |_| {});
17486
17487 let fs = FakeFs::new(cx.executor());
17488 fs.insert_tree(
17489 path!("/a"),
17490 json!({
17491 "main.rs": "fn main() { let a = 5; }",
17492 "other.rs": "// Test file",
17493 }),
17494 )
17495 .await;
17496
17497 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17498
17499 let server_restarts = Arc::new(AtomicUsize::new(0));
17500 let closure_restarts = Arc::clone(&server_restarts);
17501 let language_server_name = "test language server";
17502 let language_name: LanguageName = "Rust".into();
17503
17504 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17505 language_registry.add(Arc::new(Language::new(
17506 LanguageConfig {
17507 name: language_name.clone(),
17508 matcher: LanguageMatcher {
17509 path_suffixes: vec!["rs".to_string()],
17510 ..Default::default()
17511 },
17512 ..Default::default()
17513 },
17514 Some(tree_sitter_rust::LANGUAGE.into()),
17515 )));
17516 let mut fake_servers = language_registry.register_fake_lsp(
17517 "Rust",
17518 FakeLspAdapter {
17519 name: language_server_name,
17520 initialization_options: Some(json!({
17521 "testOptionValue": true
17522 })),
17523 initializer: Some(Box::new(move |fake_server| {
17524 let task_restarts = Arc::clone(&closure_restarts);
17525 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
17526 task_restarts.fetch_add(1, atomic::Ordering::Release);
17527 futures::future::ready(Ok(()))
17528 });
17529 })),
17530 ..Default::default()
17531 },
17532 );
17533
17534 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17535 let _buffer = project
17536 .update(cx, |project, cx| {
17537 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
17538 })
17539 .await
17540 .unwrap();
17541 let _fake_server = fake_servers.next().await.unwrap();
17542 update_test_language_settings(cx, |language_settings| {
17543 language_settings.languages.0.insert(
17544 language_name.clone().0,
17545 LanguageSettingsContent {
17546 tab_size: NonZeroU32::new(8),
17547 ..Default::default()
17548 },
17549 );
17550 });
17551 cx.executor().run_until_parked();
17552 assert_eq!(
17553 server_restarts.load(atomic::Ordering::Acquire),
17554 0,
17555 "Should not restart LSP server on an unrelated change"
17556 );
17557
17558 update_test_project_settings(cx, |project_settings| {
17559 project_settings.lsp.insert(
17560 "Some other server name".into(),
17561 LspSettings {
17562 binary: None,
17563 settings: None,
17564 initialization_options: Some(json!({
17565 "some other init value": false
17566 })),
17567 enable_lsp_tasks: false,
17568 fetch: None,
17569 },
17570 );
17571 });
17572 cx.executor().run_until_parked();
17573 assert_eq!(
17574 server_restarts.load(atomic::Ordering::Acquire),
17575 0,
17576 "Should not restart LSP server on an unrelated LSP settings change"
17577 );
17578
17579 update_test_project_settings(cx, |project_settings| {
17580 project_settings.lsp.insert(
17581 language_server_name.into(),
17582 LspSettings {
17583 binary: None,
17584 settings: None,
17585 initialization_options: Some(json!({
17586 "anotherInitValue": false
17587 })),
17588 enable_lsp_tasks: false,
17589 fetch: None,
17590 },
17591 );
17592 });
17593 cx.executor().run_until_parked();
17594 assert_eq!(
17595 server_restarts.load(atomic::Ordering::Acquire),
17596 1,
17597 "Should restart LSP server on a related LSP settings change"
17598 );
17599
17600 update_test_project_settings(cx, |project_settings| {
17601 project_settings.lsp.insert(
17602 language_server_name.into(),
17603 LspSettings {
17604 binary: None,
17605 settings: None,
17606 initialization_options: Some(json!({
17607 "anotherInitValue": false
17608 })),
17609 enable_lsp_tasks: false,
17610 fetch: None,
17611 },
17612 );
17613 });
17614 cx.executor().run_until_parked();
17615 assert_eq!(
17616 server_restarts.load(atomic::Ordering::Acquire),
17617 1,
17618 "Should not restart LSP server on a related LSP settings change that is the same"
17619 );
17620
17621 update_test_project_settings(cx, |project_settings| {
17622 project_settings.lsp.insert(
17623 language_server_name.into(),
17624 LspSettings {
17625 binary: None,
17626 settings: None,
17627 initialization_options: None,
17628 enable_lsp_tasks: false,
17629 fetch: None,
17630 },
17631 );
17632 });
17633 cx.executor().run_until_parked();
17634 assert_eq!(
17635 server_restarts.load(atomic::Ordering::Acquire),
17636 2,
17637 "Should restart LSP server on another related LSP settings change"
17638 );
17639}
17640
17641#[gpui::test]
17642async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
17643 init_test(cx, |_| {});
17644
17645 let mut cx = EditorLspTestContext::new_rust(
17646 lsp::ServerCapabilities {
17647 completion_provider: Some(lsp::CompletionOptions {
17648 trigger_characters: Some(vec![".".to_string()]),
17649 resolve_provider: Some(true),
17650 ..Default::default()
17651 }),
17652 ..Default::default()
17653 },
17654 cx,
17655 )
17656 .await;
17657
17658 cx.set_state("fn main() { let a = 2ˇ; }");
17659 cx.simulate_keystroke(".");
17660 let completion_item = lsp::CompletionItem {
17661 label: "some".into(),
17662 kind: Some(lsp::CompletionItemKind::SNIPPET),
17663 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
17664 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
17665 kind: lsp::MarkupKind::Markdown,
17666 value: "```rust\nSome(2)\n```".to_string(),
17667 })),
17668 deprecated: Some(false),
17669 sort_text: Some("fffffff2".to_string()),
17670 filter_text: Some("some".to_string()),
17671 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
17672 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17673 range: lsp::Range {
17674 start: lsp::Position {
17675 line: 0,
17676 character: 22,
17677 },
17678 end: lsp::Position {
17679 line: 0,
17680 character: 22,
17681 },
17682 },
17683 new_text: "Some(2)".to_string(),
17684 })),
17685 additional_text_edits: Some(vec![lsp::TextEdit {
17686 range: lsp::Range {
17687 start: lsp::Position {
17688 line: 0,
17689 character: 20,
17690 },
17691 end: lsp::Position {
17692 line: 0,
17693 character: 22,
17694 },
17695 },
17696 new_text: "".to_string(),
17697 }]),
17698 ..Default::default()
17699 };
17700
17701 let closure_completion_item = completion_item.clone();
17702 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17703 let task_completion_item = closure_completion_item.clone();
17704 async move {
17705 Ok(Some(lsp::CompletionResponse::Array(vec![
17706 task_completion_item,
17707 ])))
17708 }
17709 });
17710
17711 request.next().await;
17712
17713 cx.condition(|editor, _| editor.context_menu_visible())
17714 .await;
17715 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
17716 editor
17717 .confirm_completion(&ConfirmCompletion::default(), window, cx)
17718 .unwrap()
17719 });
17720 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
17721
17722 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
17723 let task_completion_item = completion_item.clone();
17724 async move { Ok(task_completion_item) }
17725 })
17726 .next()
17727 .await
17728 .unwrap();
17729 apply_additional_edits.await.unwrap();
17730 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
17731}
17732
17733#[gpui::test]
17734async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
17735 init_test(cx, |_| {});
17736
17737 let mut cx = EditorLspTestContext::new_rust(
17738 lsp::ServerCapabilities {
17739 completion_provider: Some(lsp::CompletionOptions {
17740 trigger_characters: Some(vec![".".to_string()]),
17741 resolve_provider: Some(true),
17742 ..Default::default()
17743 }),
17744 ..Default::default()
17745 },
17746 cx,
17747 )
17748 .await;
17749
17750 cx.set_state("fn main() { let a = 2ˇ; }");
17751 cx.simulate_keystroke(".");
17752
17753 let item1 = lsp::CompletionItem {
17754 label: "method id()".to_string(),
17755 filter_text: Some("id".to_string()),
17756 detail: None,
17757 documentation: None,
17758 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17759 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17760 new_text: ".id".to_string(),
17761 })),
17762 ..lsp::CompletionItem::default()
17763 };
17764
17765 let item2 = lsp::CompletionItem {
17766 label: "other".to_string(),
17767 filter_text: Some("other".to_string()),
17768 detail: None,
17769 documentation: None,
17770 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17771 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17772 new_text: ".other".to_string(),
17773 })),
17774 ..lsp::CompletionItem::default()
17775 };
17776
17777 let item1 = item1.clone();
17778 cx.set_request_handler::<lsp::request::Completion, _, _>({
17779 let item1 = item1.clone();
17780 move |_, _, _| {
17781 let item1 = item1.clone();
17782 let item2 = item2.clone();
17783 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
17784 }
17785 })
17786 .next()
17787 .await;
17788
17789 cx.condition(|editor, _| editor.context_menu_visible())
17790 .await;
17791 cx.update_editor(|editor, _, _| {
17792 let context_menu = editor.context_menu.borrow_mut();
17793 let context_menu = context_menu
17794 .as_ref()
17795 .expect("Should have the context menu deployed");
17796 match context_menu {
17797 CodeContextMenu::Completions(completions_menu) => {
17798 let completions = completions_menu.completions.borrow_mut();
17799 assert_eq!(
17800 completions
17801 .iter()
17802 .map(|completion| &completion.label.text)
17803 .collect::<Vec<_>>(),
17804 vec!["method id()", "other"]
17805 )
17806 }
17807 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17808 }
17809 });
17810
17811 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
17812 let item1 = item1.clone();
17813 move |_, item_to_resolve, _| {
17814 let item1 = item1.clone();
17815 async move {
17816 if item1 == item_to_resolve {
17817 Ok(lsp::CompletionItem {
17818 label: "method id()".to_string(),
17819 filter_text: Some("id".to_string()),
17820 detail: Some("Now resolved!".to_string()),
17821 documentation: Some(lsp::Documentation::String("Docs".to_string())),
17822 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17823 range: lsp::Range::new(
17824 lsp::Position::new(0, 22),
17825 lsp::Position::new(0, 22),
17826 ),
17827 new_text: ".id".to_string(),
17828 })),
17829 ..lsp::CompletionItem::default()
17830 })
17831 } else {
17832 Ok(item_to_resolve)
17833 }
17834 }
17835 }
17836 })
17837 .next()
17838 .await
17839 .unwrap();
17840 cx.run_until_parked();
17841
17842 cx.update_editor(|editor, window, cx| {
17843 editor.context_menu_next(&Default::default(), window, cx);
17844 });
17845
17846 cx.update_editor(|editor, _, _| {
17847 let context_menu = editor.context_menu.borrow_mut();
17848 let context_menu = context_menu
17849 .as_ref()
17850 .expect("Should have the context menu deployed");
17851 match context_menu {
17852 CodeContextMenu::Completions(completions_menu) => {
17853 let completions = completions_menu.completions.borrow_mut();
17854 assert_eq!(
17855 completions
17856 .iter()
17857 .map(|completion| &completion.label.text)
17858 .collect::<Vec<_>>(),
17859 vec!["method id() Now resolved!", "other"],
17860 "Should update first completion label, but not second as the filter text did not match."
17861 );
17862 }
17863 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17864 }
17865 });
17866}
17867
17868#[gpui::test]
17869async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
17870 init_test(cx, |_| {});
17871 let mut cx = EditorLspTestContext::new_rust(
17872 lsp::ServerCapabilities {
17873 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
17874 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
17875 completion_provider: Some(lsp::CompletionOptions {
17876 resolve_provider: Some(true),
17877 ..Default::default()
17878 }),
17879 ..Default::default()
17880 },
17881 cx,
17882 )
17883 .await;
17884 cx.set_state(indoc! {"
17885 struct TestStruct {
17886 field: i32
17887 }
17888
17889 fn mainˇ() {
17890 let unused_var = 42;
17891 let test_struct = TestStruct { field: 42 };
17892 }
17893 "});
17894 let symbol_range = cx.lsp_range(indoc! {"
17895 struct TestStruct {
17896 field: i32
17897 }
17898
17899 «fn main»() {
17900 let unused_var = 42;
17901 let test_struct = TestStruct { field: 42 };
17902 }
17903 "});
17904 let mut hover_requests =
17905 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
17906 Ok(Some(lsp::Hover {
17907 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
17908 kind: lsp::MarkupKind::Markdown,
17909 value: "Function documentation".to_string(),
17910 }),
17911 range: Some(symbol_range),
17912 }))
17913 });
17914
17915 // Case 1: Test that code action menu hide hover popover
17916 cx.dispatch_action(Hover);
17917 hover_requests.next().await;
17918 cx.condition(|editor, _| editor.hover_state.visible()).await;
17919 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
17920 move |_, _, _| async move {
17921 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
17922 lsp::CodeAction {
17923 title: "Remove unused variable".to_string(),
17924 kind: Some(CodeActionKind::QUICKFIX),
17925 edit: Some(lsp::WorkspaceEdit {
17926 changes: Some(
17927 [(
17928 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
17929 vec![lsp::TextEdit {
17930 range: lsp::Range::new(
17931 lsp::Position::new(5, 4),
17932 lsp::Position::new(5, 27),
17933 ),
17934 new_text: "".to_string(),
17935 }],
17936 )]
17937 .into_iter()
17938 .collect(),
17939 ),
17940 ..Default::default()
17941 }),
17942 ..Default::default()
17943 },
17944 )]))
17945 },
17946 );
17947 cx.update_editor(|editor, window, cx| {
17948 editor.toggle_code_actions(
17949 &ToggleCodeActions {
17950 deployed_from: None,
17951 quick_launch: false,
17952 },
17953 window,
17954 cx,
17955 );
17956 });
17957 code_action_requests.next().await;
17958 cx.run_until_parked();
17959 cx.condition(|editor, _| editor.context_menu_visible())
17960 .await;
17961 cx.update_editor(|editor, _, _| {
17962 assert!(
17963 !editor.hover_state.visible(),
17964 "Hover popover should be hidden when code action menu is shown"
17965 );
17966 // Hide code actions
17967 editor.context_menu.take();
17968 });
17969
17970 // Case 2: Test that code completions hide hover popover
17971 cx.dispatch_action(Hover);
17972 hover_requests.next().await;
17973 cx.condition(|editor, _| editor.hover_state.visible()).await;
17974 let counter = Arc::new(AtomicUsize::new(0));
17975 let mut completion_requests =
17976 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17977 let counter = counter.clone();
17978 async move {
17979 counter.fetch_add(1, atomic::Ordering::Release);
17980 Ok(Some(lsp::CompletionResponse::Array(vec![
17981 lsp::CompletionItem {
17982 label: "main".into(),
17983 kind: Some(lsp::CompletionItemKind::FUNCTION),
17984 detail: Some("() -> ()".to_string()),
17985 ..Default::default()
17986 },
17987 lsp::CompletionItem {
17988 label: "TestStruct".into(),
17989 kind: Some(lsp::CompletionItemKind::STRUCT),
17990 detail: Some("struct TestStruct".to_string()),
17991 ..Default::default()
17992 },
17993 ])))
17994 }
17995 });
17996 cx.update_editor(|editor, window, cx| {
17997 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
17998 });
17999 completion_requests.next().await;
18000 cx.condition(|editor, _| editor.context_menu_visible())
18001 .await;
18002 cx.update_editor(|editor, _, _| {
18003 assert!(
18004 !editor.hover_state.visible(),
18005 "Hover popover should be hidden when completion menu is shown"
18006 );
18007 });
18008}
18009
18010#[gpui::test]
18011async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
18012 init_test(cx, |_| {});
18013
18014 let mut cx = EditorLspTestContext::new_rust(
18015 lsp::ServerCapabilities {
18016 completion_provider: Some(lsp::CompletionOptions {
18017 trigger_characters: Some(vec![".".to_string()]),
18018 resolve_provider: Some(true),
18019 ..Default::default()
18020 }),
18021 ..Default::default()
18022 },
18023 cx,
18024 )
18025 .await;
18026
18027 cx.set_state("fn main() { let a = 2ˇ; }");
18028 cx.simulate_keystroke(".");
18029
18030 let unresolved_item_1 = lsp::CompletionItem {
18031 label: "id".to_string(),
18032 filter_text: Some("id".to_string()),
18033 detail: None,
18034 documentation: None,
18035 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18036 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18037 new_text: ".id".to_string(),
18038 })),
18039 ..lsp::CompletionItem::default()
18040 };
18041 let resolved_item_1 = lsp::CompletionItem {
18042 additional_text_edits: Some(vec![lsp::TextEdit {
18043 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
18044 new_text: "!!".to_string(),
18045 }]),
18046 ..unresolved_item_1.clone()
18047 };
18048 let unresolved_item_2 = lsp::CompletionItem {
18049 label: "other".to_string(),
18050 filter_text: Some("other".to_string()),
18051 detail: None,
18052 documentation: None,
18053 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18054 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18055 new_text: ".other".to_string(),
18056 })),
18057 ..lsp::CompletionItem::default()
18058 };
18059 let resolved_item_2 = lsp::CompletionItem {
18060 additional_text_edits: Some(vec![lsp::TextEdit {
18061 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
18062 new_text: "??".to_string(),
18063 }]),
18064 ..unresolved_item_2.clone()
18065 };
18066
18067 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
18068 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
18069 cx.lsp
18070 .server
18071 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
18072 let unresolved_item_1 = unresolved_item_1.clone();
18073 let resolved_item_1 = resolved_item_1.clone();
18074 let unresolved_item_2 = unresolved_item_2.clone();
18075 let resolved_item_2 = resolved_item_2.clone();
18076 let resolve_requests_1 = resolve_requests_1.clone();
18077 let resolve_requests_2 = resolve_requests_2.clone();
18078 move |unresolved_request, _| {
18079 let unresolved_item_1 = unresolved_item_1.clone();
18080 let resolved_item_1 = resolved_item_1.clone();
18081 let unresolved_item_2 = unresolved_item_2.clone();
18082 let resolved_item_2 = resolved_item_2.clone();
18083 let resolve_requests_1 = resolve_requests_1.clone();
18084 let resolve_requests_2 = resolve_requests_2.clone();
18085 async move {
18086 if unresolved_request == unresolved_item_1 {
18087 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
18088 Ok(resolved_item_1.clone())
18089 } else if unresolved_request == unresolved_item_2 {
18090 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
18091 Ok(resolved_item_2.clone())
18092 } else {
18093 panic!("Unexpected completion item {unresolved_request:?}")
18094 }
18095 }
18096 }
18097 })
18098 .detach();
18099
18100 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18101 let unresolved_item_1 = unresolved_item_1.clone();
18102 let unresolved_item_2 = unresolved_item_2.clone();
18103 async move {
18104 Ok(Some(lsp::CompletionResponse::Array(vec![
18105 unresolved_item_1,
18106 unresolved_item_2,
18107 ])))
18108 }
18109 })
18110 .next()
18111 .await;
18112
18113 cx.condition(|editor, _| editor.context_menu_visible())
18114 .await;
18115 cx.update_editor(|editor, _, _| {
18116 let context_menu = editor.context_menu.borrow_mut();
18117 let context_menu = context_menu
18118 .as_ref()
18119 .expect("Should have the context menu deployed");
18120 match context_menu {
18121 CodeContextMenu::Completions(completions_menu) => {
18122 let completions = completions_menu.completions.borrow_mut();
18123 assert_eq!(
18124 completions
18125 .iter()
18126 .map(|completion| &completion.label.text)
18127 .collect::<Vec<_>>(),
18128 vec!["id", "other"]
18129 )
18130 }
18131 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
18132 }
18133 });
18134 cx.run_until_parked();
18135
18136 cx.update_editor(|editor, window, cx| {
18137 editor.context_menu_next(&ContextMenuNext, window, cx);
18138 });
18139 cx.run_until_parked();
18140 cx.update_editor(|editor, window, cx| {
18141 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
18142 });
18143 cx.run_until_parked();
18144 cx.update_editor(|editor, window, cx| {
18145 editor.context_menu_next(&ContextMenuNext, window, cx);
18146 });
18147 cx.run_until_parked();
18148 cx.update_editor(|editor, window, cx| {
18149 editor
18150 .compose_completion(&ComposeCompletion::default(), window, cx)
18151 .expect("No task returned")
18152 })
18153 .await
18154 .expect("Completion failed");
18155 cx.run_until_parked();
18156
18157 cx.update_editor(|editor, _, cx| {
18158 assert_eq!(
18159 resolve_requests_1.load(atomic::Ordering::Acquire),
18160 1,
18161 "Should always resolve once despite multiple selections"
18162 );
18163 assert_eq!(
18164 resolve_requests_2.load(atomic::Ordering::Acquire),
18165 1,
18166 "Should always resolve once after multiple selections and applying the completion"
18167 );
18168 assert_eq!(
18169 editor.text(cx),
18170 "fn main() { let a = ??.other; }",
18171 "Should use resolved data when applying the completion"
18172 );
18173 });
18174}
18175
18176#[gpui::test]
18177async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
18178 init_test(cx, |_| {});
18179
18180 let item_0 = lsp::CompletionItem {
18181 label: "abs".into(),
18182 insert_text: Some("abs".into()),
18183 data: Some(json!({ "very": "special"})),
18184 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
18185 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
18186 lsp::InsertReplaceEdit {
18187 new_text: "abs".to_string(),
18188 insert: lsp::Range::default(),
18189 replace: lsp::Range::default(),
18190 },
18191 )),
18192 ..lsp::CompletionItem::default()
18193 };
18194 let items = iter::once(item_0.clone())
18195 .chain((11..51).map(|i| lsp::CompletionItem {
18196 label: format!("item_{}", i),
18197 insert_text: Some(format!("item_{}", i)),
18198 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
18199 ..lsp::CompletionItem::default()
18200 }))
18201 .collect::<Vec<_>>();
18202
18203 let default_commit_characters = vec!["?".to_string()];
18204 let default_data = json!({ "default": "data"});
18205 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
18206 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
18207 let default_edit_range = lsp::Range {
18208 start: lsp::Position {
18209 line: 0,
18210 character: 5,
18211 },
18212 end: lsp::Position {
18213 line: 0,
18214 character: 5,
18215 },
18216 };
18217
18218 let mut cx = EditorLspTestContext::new_rust(
18219 lsp::ServerCapabilities {
18220 completion_provider: Some(lsp::CompletionOptions {
18221 trigger_characters: Some(vec![".".to_string()]),
18222 resolve_provider: Some(true),
18223 ..Default::default()
18224 }),
18225 ..Default::default()
18226 },
18227 cx,
18228 )
18229 .await;
18230
18231 cx.set_state("fn main() { let a = 2ˇ; }");
18232 cx.simulate_keystroke(".");
18233
18234 let completion_data = default_data.clone();
18235 let completion_characters = default_commit_characters.clone();
18236 let completion_items = items.clone();
18237 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18238 let default_data = completion_data.clone();
18239 let default_commit_characters = completion_characters.clone();
18240 let items = completion_items.clone();
18241 async move {
18242 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
18243 items,
18244 item_defaults: Some(lsp::CompletionListItemDefaults {
18245 data: Some(default_data.clone()),
18246 commit_characters: Some(default_commit_characters.clone()),
18247 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
18248 default_edit_range,
18249 )),
18250 insert_text_format: Some(default_insert_text_format),
18251 insert_text_mode: Some(default_insert_text_mode),
18252 }),
18253 ..lsp::CompletionList::default()
18254 })))
18255 }
18256 })
18257 .next()
18258 .await;
18259
18260 let resolved_items = Arc::new(Mutex::new(Vec::new()));
18261 cx.lsp
18262 .server
18263 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
18264 let closure_resolved_items = resolved_items.clone();
18265 move |item_to_resolve, _| {
18266 let closure_resolved_items = closure_resolved_items.clone();
18267 async move {
18268 closure_resolved_items.lock().push(item_to_resolve.clone());
18269 Ok(item_to_resolve)
18270 }
18271 }
18272 })
18273 .detach();
18274
18275 cx.condition(|editor, _| editor.context_menu_visible())
18276 .await;
18277 cx.run_until_parked();
18278 cx.update_editor(|editor, _, _| {
18279 let menu = editor.context_menu.borrow_mut();
18280 match menu.as_ref().expect("should have the completions menu") {
18281 CodeContextMenu::Completions(completions_menu) => {
18282 assert_eq!(
18283 completions_menu
18284 .entries
18285 .borrow()
18286 .iter()
18287 .map(|mat| mat.string.clone())
18288 .collect::<Vec<String>>(),
18289 items
18290 .iter()
18291 .map(|completion| completion.label.clone())
18292 .collect::<Vec<String>>()
18293 );
18294 }
18295 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
18296 }
18297 });
18298 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
18299 // with 4 from the end.
18300 assert_eq!(
18301 *resolved_items.lock(),
18302 [&items[0..16], &items[items.len() - 4..items.len()]]
18303 .concat()
18304 .iter()
18305 .cloned()
18306 .map(|mut item| {
18307 if item.data.is_none() {
18308 item.data = Some(default_data.clone());
18309 }
18310 item
18311 })
18312 .collect::<Vec<lsp::CompletionItem>>(),
18313 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
18314 );
18315 resolved_items.lock().clear();
18316
18317 cx.update_editor(|editor, window, cx| {
18318 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
18319 });
18320 cx.run_until_parked();
18321 // Completions that have already been resolved are skipped.
18322 assert_eq!(
18323 *resolved_items.lock(),
18324 items[items.len() - 17..items.len() - 4]
18325 .iter()
18326 .cloned()
18327 .map(|mut item| {
18328 if item.data.is_none() {
18329 item.data = Some(default_data.clone());
18330 }
18331 item
18332 })
18333 .collect::<Vec<lsp::CompletionItem>>()
18334 );
18335 resolved_items.lock().clear();
18336}
18337
18338#[gpui::test]
18339async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
18340 init_test(cx, |_| {});
18341
18342 let mut cx = EditorLspTestContext::new(
18343 Language::new(
18344 LanguageConfig {
18345 matcher: LanguageMatcher {
18346 path_suffixes: vec!["jsx".into()],
18347 ..Default::default()
18348 },
18349 overrides: [(
18350 "element".into(),
18351 LanguageConfigOverride {
18352 completion_query_characters: Override::Set(['-'].into_iter().collect()),
18353 ..Default::default()
18354 },
18355 )]
18356 .into_iter()
18357 .collect(),
18358 ..Default::default()
18359 },
18360 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
18361 )
18362 .with_override_query("(jsx_self_closing_element) @element")
18363 .unwrap(),
18364 lsp::ServerCapabilities {
18365 completion_provider: Some(lsp::CompletionOptions {
18366 trigger_characters: Some(vec![":".to_string()]),
18367 ..Default::default()
18368 }),
18369 ..Default::default()
18370 },
18371 cx,
18372 )
18373 .await;
18374
18375 cx.lsp
18376 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
18377 Ok(Some(lsp::CompletionResponse::Array(vec![
18378 lsp::CompletionItem {
18379 label: "bg-blue".into(),
18380 ..Default::default()
18381 },
18382 lsp::CompletionItem {
18383 label: "bg-red".into(),
18384 ..Default::default()
18385 },
18386 lsp::CompletionItem {
18387 label: "bg-yellow".into(),
18388 ..Default::default()
18389 },
18390 ])))
18391 });
18392
18393 cx.set_state(r#"<p class="bgˇ" />"#);
18394
18395 // Trigger completion when typing a dash, because the dash is an extra
18396 // word character in the 'element' scope, which contains the cursor.
18397 cx.simulate_keystroke("-");
18398 cx.executor().run_until_parked();
18399 cx.update_editor(|editor, _, _| {
18400 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18401 {
18402 assert_eq!(
18403 completion_menu_entries(menu),
18404 &["bg-blue", "bg-red", "bg-yellow"]
18405 );
18406 } else {
18407 panic!("expected completion menu to be open");
18408 }
18409 });
18410
18411 cx.simulate_keystroke("l");
18412 cx.executor().run_until_parked();
18413 cx.update_editor(|editor, _, _| {
18414 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18415 {
18416 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
18417 } else {
18418 panic!("expected completion menu to be open");
18419 }
18420 });
18421
18422 // When filtering completions, consider the character after the '-' to
18423 // be the start of a subword.
18424 cx.set_state(r#"<p class="yelˇ" />"#);
18425 cx.simulate_keystroke("l");
18426 cx.executor().run_until_parked();
18427 cx.update_editor(|editor, _, _| {
18428 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18429 {
18430 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
18431 } else {
18432 panic!("expected completion menu to be open");
18433 }
18434 });
18435}
18436
18437fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
18438 let entries = menu.entries.borrow();
18439 entries.iter().map(|mat| mat.string.clone()).collect()
18440}
18441
18442#[gpui::test]
18443async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
18444 init_test(cx, |settings| {
18445 settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
18446 });
18447
18448 let fs = FakeFs::new(cx.executor());
18449 fs.insert_file(path!("/file.ts"), Default::default()).await;
18450
18451 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
18452 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18453
18454 language_registry.add(Arc::new(Language::new(
18455 LanguageConfig {
18456 name: "TypeScript".into(),
18457 matcher: LanguageMatcher {
18458 path_suffixes: vec!["ts".to_string()],
18459 ..Default::default()
18460 },
18461 ..Default::default()
18462 },
18463 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
18464 )));
18465 update_test_language_settings(cx, |settings| {
18466 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
18467 });
18468
18469 let test_plugin = "test_plugin";
18470 let _ = language_registry.register_fake_lsp(
18471 "TypeScript",
18472 FakeLspAdapter {
18473 prettier_plugins: vec![test_plugin],
18474 ..Default::default()
18475 },
18476 );
18477
18478 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
18479 let buffer = project
18480 .update(cx, |project, cx| {
18481 project.open_local_buffer(path!("/file.ts"), cx)
18482 })
18483 .await
18484 .unwrap();
18485
18486 let buffer_text = "one\ntwo\nthree\n";
18487 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
18488 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
18489 editor.update_in(cx, |editor, window, cx| {
18490 editor.set_text(buffer_text, window, cx)
18491 });
18492
18493 editor
18494 .update_in(cx, |editor, window, cx| {
18495 editor.perform_format(
18496 project.clone(),
18497 FormatTrigger::Manual,
18498 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18499 window,
18500 cx,
18501 )
18502 })
18503 .unwrap()
18504 .await;
18505 assert_eq!(
18506 editor.update(cx, |editor, cx| editor.text(cx)),
18507 buffer_text.to_string() + prettier_format_suffix,
18508 "Test prettier formatting was not applied to the original buffer text",
18509 );
18510
18511 update_test_language_settings(cx, |settings| {
18512 settings.defaults.formatter = Some(FormatterList::default())
18513 });
18514 let format = editor.update_in(cx, |editor, window, cx| {
18515 editor.perform_format(
18516 project.clone(),
18517 FormatTrigger::Manual,
18518 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18519 window,
18520 cx,
18521 )
18522 });
18523 format.await.unwrap();
18524 assert_eq!(
18525 editor.update(cx, |editor, cx| editor.text(cx)),
18526 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
18527 "Autoformatting (via test prettier) was not applied to the original buffer text",
18528 );
18529}
18530
18531#[gpui::test]
18532async fn test_addition_reverts(cx: &mut TestAppContext) {
18533 init_test(cx, |_| {});
18534 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18535 let base_text = indoc! {r#"
18536 struct Row;
18537 struct Row1;
18538 struct Row2;
18539
18540 struct Row4;
18541 struct Row5;
18542 struct Row6;
18543
18544 struct Row8;
18545 struct Row9;
18546 struct Row10;"#};
18547
18548 // When addition hunks are not adjacent to carets, no hunk revert is performed
18549 assert_hunk_revert(
18550 indoc! {r#"struct Row;
18551 struct Row1;
18552 struct Row1.1;
18553 struct Row1.2;
18554 struct Row2;ˇ
18555
18556 struct Row4;
18557 struct Row5;
18558 struct Row6;
18559
18560 struct Row8;
18561 ˇstruct Row9;
18562 struct Row9.1;
18563 struct Row9.2;
18564 struct Row9.3;
18565 struct Row10;"#},
18566 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18567 indoc! {r#"struct Row;
18568 struct Row1;
18569 struct Row1.1;
18570 struct Row1.2;
18571 struct Row2;ˇ
18572
18573 struct Row4;
18574 struct Row5;
18575 struct Row6;
18576
18577 struct Row8;
18578 ˇstruct Row9;
18579 struct Row9.1;
18580 struct Row9.2;
18581 struct Row9.3;
18582 struct Row10;"#},
18583 base_text,
18584 &mut cx,
18585 );
18586 // Same for selections
18587 assert_hunk_revert(
18588 indoc! {r#"struct Row;
18589 struct Row1;
18590 struct Row2;
18591 struct Row2.1;
18592 struct Row2.2;
18593 «ˇ
18594 struct Row4;
18595 struct» Row5;
18596 «struct Row6;
18597 ˇ»
18598 struct Row9.1;
18599 struct Row9.2;
18600 struct Row9.3;
18601 struct Row8;
18602 struct Row9;
18603 struct Row10;"#},
18604 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18605 indoc! {r#"struct Row;
18606 struct Row1;
18607 struct Row2;
18608 struct Row2.1;
18609 struct Row2.2;
18610 «ˇ
18611 struct Row4;
18612 struct» Row5;
18613 «struct Row6;
18614 ˇ»
18615 struct Row9.1;
18616 struct Row9.2;
18617 struct Row9.3;
18618 struct Row8;
18619 struct Row9;
18620 struct Row10;"#},
18621 base_text,
18622 &mut cx,
18623 );
18624
18625 // When carets and selections intersect the addition hunks, those are reverted.
18626 // Adjacent carets got merged.
18627 assert_hunk_revert(
18628 indoc! {r#"struct Row;
18629 ˇ// something on the top
18630 struct Row1;
18631 struct Row2;
18632 struct Roˇw3.1;
18633 struct Row2.2;
18634 struct Row2.3;ˇ
18635
18636 struct Row4;
18637 struct ˇRow5.1;
18638 struct Row5.2;
18639 struct «Rowˇ»5.3;
18640 struct Row5;
18641 struct Row6;
18642 ˇ
18643 struct Row9.1;
18644 struct «Rowˇ»9.2;
18645 struct «ˇRow»9.3;
18646 struct Row8;
18647 struct Row9;
18648 «ˇ// something on bottom»
18649 struct Row10;"#},
18650 vec![
18651 DiffHunkStatusKind::Added,
18652 DiffHunkStatusKind::Added,
18653 DiffHunkStatusKind::Added,
18654 DiffHunkStatusKind::Added,
18655 DiffHunkStatusKind::Added,
18656 ],
18657 indoc! {r#"struct Row;
18658 ˇstruct Row1;
18659 struct Row2;
18660 ˇ
18661 struct Row4;
18662 ˇstruct Row5;
18663 struct Row6;
18664 ˇ
18665 ˇstruct Row8;
18666 struct Row9;
18667 ˇstruct Row10;"#},
18668 base_text,
18669 &mut cx,
18670 );
18671}
18672
18673#[gpui::test]
18674async fn test_modification_reverts(cx: &mut TestAppContext) {
18675 init_test(cx, |_| {});
18676 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18677 let base_text = indoc! {r#"
18678 struct Row;
18679 struct Row1;
18680 struct Row2;
18681
18682 struct Row4;
18683 struct Row5;
18684 struct Row6;
18685
18686 struct Row8;
18687 struct Row9;
18688 struct Row10;"#};
18689
18690 // Modification hunks behave the same as the addition ones.
18691 assert_hunk_revert(
18692 indoc! {r#"struct Row;
18693 struct Row1;
18694 struct Row33;
18695 ˇ
18696 struct Row4;
18697 struct Row5;
18698 struct Row6;
18699 ˇ
18700 struct Row99;
18701 struct Row9;
18702 struct Row10;"#},
18703 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18704 indoc! {r#"struct Row;
18705 struct Row1;
18706 struct Row33;
18707 ˇ
18708 struct Row4;
18709 struct Row5;
18710 struct Row6;
18711 ˇ
18712 struct Row99;
18713 struct Row9;
18714 struct Row10;"#},
18715 base_text,
18716 &mut cx,
18717 );
18718 assert_hunk_revert(
18719 indoc! {r#"struct Row;
18720 struct Row1;
18721 struct Row33;
18722 «ˇ
18723 struct Row4;
18724 struct» Row5;
18725 «struct Row6;
18726 ˇ»
18727 struct Row99;
18728 struct Row9;
18729 struct Row10;"#},
18730 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18731 indoc! {r#"struct Row;
18732 struct Row1;
18733 struct Row33;
18734 «ˇ
18735 struct Row4;
18736 struct» Row5;
18737 «struct Row6;
18738 ˇ»
18739 struct Row99;
18740 struct Row9;
18741 struct Row10;"#},
18742 base_text,
18743 &mut cx,
18744 );
18745
18746 assert_hunk_revert(
18747 indoc! {r#"ˇstruct Row1.1;
18748 struct Row1;
18749 «ˇstr»uct Row22;
18750
18751 struct ˇRow44;
18752 struct Row5;
18753 struct «Rˇ»ow66;ˇ
18754
18755 «struˇ»ct Row88;
18756 struct Row9;
18757 struct Row1011;ˇ"#},
18758 vec![
18759 DiffHunkStatusKind::Modified,
18760 DiffHunkStatusKind::Modified,
18761 DiffHunkStatusKind::Modified,
18762 DiffHunkStatusKind::Modified,
18763 DiffHunkStatusKind::Modified,
18764 DiffHunkStatusKind::Modified,
18765 ],
18766 indoc! {r#"struct Row;
18767 ˇstruct Row1;
18768 struct Row2;
18769 ˇ
18770 struct Row4;
18771 ˇstruct Row5;
18772 struct Row6;
18773 ˇ
18774 struct Row8;
18775 ˇstruct Row9;
18776 struct Row10;ˇ"#},
18777 base_text,
18778 &mut cx,
18779 );
18780}
18781
18782#[gpui::test]
18783async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
18784 init_test(cx, |_| {});
18785 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18786 let base_text = indoc! {r#"
18787 one
18788
18789 two
18790 three
18791 "#};
18792
18793 cx.set_head_text(base_text);
18794 cx.set_state("\nˇ\n");
18795 cx.executor().run_until_parked();
18796 cx.update_editor(|editor, _window, cx| {
18797 editor.expand_selected_diff_hunks(cx);
18798 });
18799 cx.executor().run_until_parked();
18800 cx.update_editor(|editor, window, cx| {
18801 editor.backspace(&Default::default(), window, cx);
18802 });
18803 cx.run_until_parked();
18804 cx.assert_state_with_diff(
18805 indoc! {r#"
18806
18807 - two
18808 - threeˇ
18809 +
18810 "#}
18811 .to_string(),
18812 );
18813}
18814
18815#[gpui::test]
18816async fn test_deletion_reverts(cx: &mut TestAppContext) {
18817 init_test(cx, |_| {});
18818 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18819 let base_text = indoc! {r#"struct Row;
18820struct Row1;
18821struct Row2;
18822
18823struct Row4;
18824struct Row5;
18825struct Row6;
18826
18827struct Row8;
18828struct Row9;
18829struct Row10;"#};
18830
18831 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
18832 assert_hunk_revert(
18833 indoc! {r#"struct Row;
18834 struct Row2;
18835
18836 ˇstruct Row4;
18837 struct Row5;
18838 struct Row6;
18839 ˇ
18840 struct Row8;
18841 struct Row10;"#},
18842 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18843 indoc! {r#"struct Row;
18844 struct Row2;
18845
18846 ˇstruct Row4;
18847 struct Row5;
18848 struct Row6;
18849 ˇ
18850 struct Row8;
18851 struct Row10;"#},
18852 base_text,
18853 &mut cx,
18854 );
18855 assert_hunk_revert(
18856 indoc! {r#"struct Row;
18857 struct Row2;
18858
18859 «ˇstruct Row4;
18860 struct» Row5;
18861 «struct Row6;
18862 ˇ»
18863 struct Row8;
18864 struct Row10;"#},
18865 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18866 indoc! {r#"struct Row;
18867 struct Row2;
18868
18869 «ˇstruct Row4;
18870 struct» Row5;
18871 «struct Row6;
18872 ˇ»
18873 struct Row8;
18874 struct Row10;"#},
18875 base_text,
18876 &mut cx,
18877 );
18878
18879 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
18880 assert_hunk_revert(
18881 indoc! {r#"struct Row;
18882 ˇstruct Row2;
18883
18884 struct Row4;
18885 struct Row5;
18886 struct Row6;
18887
18888 struct Row8;ˇ
18889 struct Row10;"#},
18890 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18891 indoc! {r#"struct Row;
18892 struct Row1;
18893 ˇstruct Row2;
18894
18895 struct Row4;
18896 struct Row5;
18897 struct Row6;
18898
18899 struct Row8;ˇ
18900 struct Row9;
18901 struct Row10;"#},
18902 base_text,
18903 &mut cx,
18904 );
18905 assert_hunk_revert(
18906 indoc! {r#"struct Row;
18907 struct Row2«ˇ;
18908 struct Row4;
18909 struct» Row5;
18910 «struct Row6;
18911
18912 struct Row8;ˇ»
18913 struct Row10;"#},
18914 vec![
18915 DiffHunkStatusKind::Deleted,
18916 DiffHunkStatusKind::Deleted,
18917 DiffHunkStatusKind::Deleted,
18918 ],
18919 indoc! {r#"struct Row;
18920 struct Row1;
18921 struct Row2«ˇ;
18922
18923 struct Row4;
18924 struct» Row5;
18925 «struct Row6;
18926
18927 struct Row8;ˇ»
18928 struct Row9;
18929 struct Row10;"#},
18930 base_text,
18931 &mut cx,
18932 );
18933}
18934
18935#[gpui::test]
18936async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
18937 init_test(cx, |_| {});
18938
18939 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
18940 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
18941 let base_text_3 =
18942 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
18943
18944 let text_1 = edit_first_char_of_every_line(base_text_1);
18945 let text_2 = edit_first_char_of_every_line(base_text_2);
18946 let text_3 = edit_first_char_of_every_line(base_text_3);
18947
18948 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
18949 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
18950 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
18951
18952 let multibuffer = cx.new(|cx| {
18953 let mut multibuffer = MultiBuffer::new(ReadWrite);
18954 multibuffer.push_excerpts(
18955 buffer_1.clone(),
18956 [
18957 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18958 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18959 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18960 ],
18961 cx,
18962 );
18963 multibuffer.push_excerpts(
18964 buffer_2.clone(),
18965 [
18966 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18967 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18968 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18969 ],
18970 cx,
18971 );
18972 multibuffer.push_excerpts(
18973 buffer_3.clone(),
18974 [
18975 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18976 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18977 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18978 ],
18979 cx,
18980 );
18981 multibuffer
18982 });
18983
18984 let fs = FakeFs::new(cx.executor());
18985 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
18986 let (editor, cx) = cx
18987 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
18988 editor.update_in(cx, |editor, _window, cx| {
18989 for (buffer, diff_base) in [
18990 (buffer_1.clone(), base_text_1),
18991 (buffer_2.clone(), base_text_2),
18992 (buffer_3.clone(), base_text_3),
18993 ] {
18994 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
18995 editor
18996 .buffer
18997 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
18998 }
18999 });
19000 cx.executor().run_until_parked();
19001
19002 editor.update_in(cx, |editor, window, cx| {
19003 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}");
19004 editor.select_all(&SelectAll, window, cx);
19005 editor.git_restore(&Default::default(), window, cx);
19006 });
19007 cx.executor().run_until_parked();
19008
19009 // When all ranges are selected, all buffer hunks are reverted.
19010 editor.update(cx, |editor, cx| {
19011 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");
19012 });
19013 buffer_1.update(cx, |buffer, _| {
19014 assert_eq!(buffer.text(), base_text_1);
19015 });
19016 buffer_2.update(cx, |buffer, _| {
19017 assert_eq!(buffer.text(), base_text_2);
19018 });
19019 buffer_3.update(cx, |buffer, _| {
19020 assert_eq!(buffer.text(), base_text_3);
19021 });
19022
19023 editor.update_in(cx, |editor, window, cx| {
19024 editor.undo(&Default::default(), window, cx);
19025 });
19026
19027 editor.update_in(cx, |editor, window, cx| {
19028 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19029 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
19030 });
19031 editor.git_restore(&Default::default(), window, cx);
19032 });
19033
19034 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
19035 // but not affect buffer_2 and its related excerpts.
19036 editor.update(cx, |editor, cx| {
19037 assert_eq!(
19038 editor.text(cx),
19039 "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}"
19040 );
19041 });
19042 buffer_1.update(cx, |buffer, _| {
19043 assert_eq!(buffer.text(), base_text_1);
19044 });
19045 buffer_2.update(cx, |buffer, _| {
19046 assert_eq!(
19047 buffer.text(),
19048 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
19049 );
19050 });
19051 buffer_3.update(cx, |buffer, _| {
19052 assert_eq!(
19053 buffer.text(),
19054 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
19055 );
19056 });
19057
19058 fn edit_first_char_of_every_line(text: &str) -> String {
19059 text.split('\n')
19060 .map(|line| format!("X{}", &line[1..]))
19061 .collect::<Vec<_>>()
19062 .join("\n")
19063 }
19064}
19065
19066#[gpui::test]
19067async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
19068 init_test(cx, |_| {});
19069
19070 let cols = 4;
19071 let rows = 10;
19072 let sample_text_1 = sample_text(rows, cols, 'a');
19073 assert_eq!(
19074 sample_text_1,
19075 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
19076 );
19077 let sample_text_2 = sample_text(rows, cols, 'l');
19078 assert_eq!(
19079 sample_text_2,
19080 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
19081 );
19082 let sample_text_3 = sample_text(rows, cols, 'v');
19083 assert_eq!(
19084 sample_text_3,
19085 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
19086 );
19087
19088 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
19089 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
19090 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
19091
19092 let multi_buffer = cx.new(|cx| {
19093 let mut multibuffer = MultiBuffer::new(ReadWrite);
19094 multibuffer.push_excerpts(
19095 buffer_1.clone(),
19096 [
19097 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19098 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19099 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19100 ],
19101 cx,
19102 );
19103 multibuffer.push_excerpts(
19104 buffer_2.clone(),
19105 [
19106 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19107 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19108 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19109 ],
19110 cx,
19111 );
19112 multibuffer.push_excerpts(
19113 buffer_3.clone(),
19114 [
19115 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19116 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19117 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19118 ],
19119 cx,
19120 );
19121 multibuffer
19122 });
19123
19124 let fs = FakeFs::new(cx.executor());
19125 fs.insert_tree(
19126 "/a",
19127 json!({
19128 "main.rs": sample_text_1,
19129 "other.rs": sample_text_2,
19130 "lib.rs": sample_text_3,
19131 }),
19132 )
19133 .await;
19134 let project = Project::test(fs, ["/a".as_ref()], cx).await;
19135 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19136 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19137 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19138 Editor::new(
19139 EditorMode::full(),
19140 multi_buffer,
19141 Some(project.clone()),
19142 window,
19143 cx,
19144 )
19145 });
19146 let multibuffer_item_id = workspace
19147 .update(cx, |workspace, window, cx| {
19148 assert!(
19149 workspace.active_item(cx).is_none(),
19150 "active item should be None before the first item is added"
19151 );
19152 workspace.add_item_to_active_pane(
19153 Box::new(multi_buffer_editor.clone()),
19154 None,
19155 true,
19156 window,
19157 cx,
19158 );
19159 let active_item = workspace
19160 .active_item(cx)
19161 .expect("should have an active item after adding the multi buffer");
19162 assert_eq!(
19163 active_item.buffer_kind(cx),
19164 ItemBufferKind::Multibuffer,
19165 "A multi buffer was expected to active after adding"
19166 );
19167 active_item.item_id()
19168 })
19169 .unwrap();
19170 cx.executor().run_until_parked();
19171
19172 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19173 editor.change_selections(
19174 SelectionEffects::scroll(Autoscroll::Next),
19175 window,
19176 cx,
19177 |s| s.select_ranges(Some(1..2)),
19178 );
19179 editor.open_excerpts(&OpenExcerpts, window, cx);
19180 });
19181 cx.executor().run_until_parked();
19182 let first_item_id = workspace
19183 .update(cx, |workspace, window, cx| {
19184 let active_item = workspace
19185 .active_item(cx)
19186 .expect("should have an active item after navigating into the 1st buffer");
19187 let first_item_id = active_item.item_id();
19188 assert_ne!(
19189 first_item_id, multibuffer_item_id,
19190 "Should navigate into the 1st buffer and activate it"
19191 );
19192 assert_eq!(
19193 active_item.buffer_kind(cx),
19194 ItemBufferKind::Singleton,
19195 "New active item should be a singleton buffer"
19196 );
19197 assert_eq!(
19198 active_item
19199 .act_as::<Editor>(cx)
19200 .expect("should have navigated into an editor for the 1st buffer")
19201 .read(cx)
19202 .text(cx),
19203 sample_text_1
19204 );
19205
19206 workspace
19207 .go_back(workspace.active_pane().downgrade(), window, cx)
19208 .detach_and_log_err(cx);
19209
19210 first_item_id
19211 })
19212 .unwrap();
19213 cx.executor().run_until_parked();
19214 workspace
19215 .update(cx, |workspace, _, cx| {
19216 let active_item = workspace
19217 .active_item(cx)
19218 .expect("should have an active item after navigating back");
19219 assert_eq!(
19220 active_item.item_id(),
19221 multibuffer_item_id,
19222 "Should navigate back to the multi buffer"
19223 );
19224 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19225 })
19226 .unwrap();
19227
19228 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19229 editor.change_selections(
19230 SelectionEffects::scroll(Autoscroll::Next),
19231 window,
19232 cx,
19233 |s| s.select_ranges(Some(39..40)),
19234 );
19235 editor.open_excerpts(&OpenExcerpts, window, cx);
19236 });
19237 cx.executor().run_until_parked();
19238 let second_item_id = workspace
19239 .update(cx, |workspace, window, cx| {
19240 let active_item = workspace
19241 .active_item(cx)
19242 .expect("should have an active item after navigating into the 2nd buffer");
19243 let second_item_id = active_item.item_id();
19244 assert_ne!(
19245 second_item_id, multibuffer_item_id,
19246 "Should navigate away from the multibuffer"
19247 );
19248 assert_ne!(
19249 second_item_id, first_item_id,
19250 "Should navigate into the 2nd buffer and activate it"
19251 );
19252 assert_eq!(
19253 active_item.buffer_kind(cx),
19254 ItemBufferKind::Singleton,
19255 "New active item should be a singleton buffer"
19256 );
19257 assert_eq!(
19258 active_item
19259 .act_as::<Editor>(cx)
19260 .expect("should have navigated into an editor")
19261 .read(cx)
19262 .text(cx),
19263 sample_text_2
19264 );
19265
19266 workspace
19267 .go_back(workspace.active_pane().downgrade(), window, cx)
19268 .detach_and_log_err(cx);
19269
19270 second_item_id
19271 })
19272 .unwrap();
19273 cx.executor().run_until_parked();
19274 workspace
19275 .update(cx, |workspace, _, cx| {
19276 let active_item = workspace
19277 .active_item(cx)
19278 .expect("should have an active item after navigating back from the 2nd buffer");
19279 assert_eq!(
19280 active_item.item_id(),
19281 multibuffer_item_id,
19282 "Should navigate back from the 2nd buffer to the multi buffer"
19283 );
19284 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19285 })
19286 .unwrap();
19287
19288 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19289 editor.change_selections(
19290 SelectionEffects::scroll(Autoscroll::Next),
19291 window,
19292 cx,
19293 |s| s.select_ranges(Some(70..70)),
19294 );
19295 editor.open_excerpts(&OpenExcerpts, window, cx);
19296 });
19297 cx.executor().run_until_parked();
19298 workspace
19299 .update(cx, |workspace, window, cx| {
19300 let active_item = workspace
19301 .active_item(cx)
19302 .expect("should have an active item after navigating into the 3rd buffer");
19303 let third_item_id = active_item.item_id();
19304 assert_ne!(
19305 third_item_id, multibuffer_item_id,
19306 "Should navigate into the 3rd buffer and activate it"
19307 );
19308 assert_ne!(third_item_id, first_item_id);
19309 assert_ne!(third_item_id, second_item_id);
19310 assert_eq!(
19311 active_item.buffer_kind(cx),
19312 ItemBufferKind::Singleton,
19313 "New active item should be a singleton buffer"
19314 );
19315 assert_eq!(
19316 active_item
19317 .act_as::<Editor>(cx)
19318 .expect("should have navigated into an editor")
19319 .read(cx)
19320 .text(cx),
19321 sample_text_3
19322 );
19323
19324 workspace
19325 .go_back(workspace.active_pane().downgrade(), window, cx)
19326 .detach_and_log_err(cx);
19327 })
19328 .unwrap();
19329 cx.executor().run_until_parked();
19330 workspace
19331 .update(cx, |workspace, _, cx| {
19332 let active_item = workspace
19333 .active_item(cx)
19334 .expect("should have an active item after navigating back from the 3rd buffer");
19335 assert_eq!(
19336 active_item.item_id(),
19337 multibuffer_item_id,
19338 "Should navigate back from the 3rd buffer to the multi buffer"
19339 );
19340 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19341 })
19342 .unwrap();
19343}
19344
19345#[gpui::test]
19346async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19347 init_test(cx, |_| {});
19348
19349 let mut cx = EditorTestContext::new(cx).await;
19350
19351 let diff_base = r#"
19352 use some::mod;
19353
19354 const A: u32 = 42;
19355
19356 fn main() {
19357 println!("hello");
19358
19359 println!("world");
19360 }
19361 "#
19362 .unindent();
19363
19364 cx.set_state(
19365 &r#"
19366 use some::modified;
19367
19368 ˇ
19369 fn main() {
19370 println!("hello there");
19371
19372 println!("around the");
19373 println!("world");
19374 }
19375 "#
19376 .unindent(),
19377 );
19378
19379 cx.set_head_text(&diff_base);
19380 executor.run_until_parked();
19381
19382 cx.update_editor(|editor, window, cx| {
19383 editor.go_to_next_hunk(&GoToHunk, window, cx);
19384 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19385 });
19386 executor.run_until_parked();
19387 cx.assert_state_with_diff(
19388 r#"
19389 use some::modified;
19390
19391
19392 fn main() {
19393 - println!("hello");
19394 + ˇ println!("hello there");
19395
19396 println!("around the");
19397 println!("world");
19398 }
19399 "#
19400 .unindent(),
19401 );
19402
19403 cx.update_editor(|editor, window, cx| {
19404 for _ in 0..2 {
19405 editor.go_to_next_hunk(&GoToHunk, window, cx);
19406 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19407 }
19408 });
19409 executor.run_until_parked();
19410 cx.assert_state_with_diff(
19411 r#"
19412 - use some::mod;
19413 + ˇuse some::modified;
19414
19415
19416 fn main() {
19417 - println!("hello");
19418 + println!("hello there");
19419
19420 + println!("around the");
19421 println!("world");
19422 }
19423 "#
19424 .unindent(),
19425 );
19426
19427 cx.update_editor(|editor, window, cx| {
19428 editor.go_to_next_hunk(&GoToHunk, window, cx);
19429 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19430 });
19431 executor.run_until_parked();
19432 cx.assert_state_with_diff(
19433 r#"
19434 - use some::mod;
19435 + use some::modified;
19436
19437 - const A: u32 = 42;
19438 ˇ
19439 fn main() {
19440 - println!("hello");
19441 + println!("hello there");
19442
19443 + println!("around the");
19444 println!("world");
19445 }
19446 "#
19447 .unindent(),
19448 );
19449
19450 cx.update_editor(|editor, window, cx| {
19451 editor.cancel(&Cancel, window, cx);
19452 });
19453
19454 cx.assert_state_with_diff(
19455 r#"
19456 use some::modified;
19457
19458 ˇ
19459 fn main() {
19460 println!("hello there");
19461
19462 println!("around the");
19463 println!("world");
19464 }
19465 "#
19466 .unindent(),
19467 );
19468}
19469
19470#[gpui::test]
19471async fn test_diff_base_change_with_expanded_diff_hunks(
19472 executor: BackgroundExecutor,
19473 cx: &mut TestAppContext,
19474) {
19475 init_test(cx, |_| {});
19476
19477 let mut cx = EditorTestContext::new(cx).await;
19478
19479 let diff_base = r#"
19480 use some::mod1;
19481 use some::mod2;
19482
19483 const A: u32 = 42;
19484 const B: u32 = 42;
19485 const C: u32 = 42;
19486
19487 fn main() {
19488 println!("hello");
19489
19490 println!("world");
19491 }
19492 "#
19493 .unindent();
19494
19495 cx.set_state(
19496 &r#"
19497 use some::mod2;
19498
19499 const A: u32 = 42;
19500 const C: u32 = 42;
19501
19502 fn main(ˇ) {
19503 //println!("hello");
19504
19505 println!("world");
19506 //
19507 //
19508 }
19509 "#
19510 .unindent(),
19511 );
19512
19513 cx.set_head_text(&diff_base);
19514 executor.run_until_parked();
19515
19516 cx.update_editor(|editor, window, cx| {
19517 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19518 });
19519 executor.run_until_parked();
19520 cx.assert_state_with_diff(
19521 r#"
19522 - use some::mod1;
19523 use some::mod2;
19524
19525 const A: u32 = 42;
19526 - const B: u32 = 42;
19527 const C: u32 = 42;
19528
19529 fn main(ˇ) {
19530 - println!("hello");
19531 + //println!("hello");
19532
19533 println!("world");
19534 + //
19535 + //
19536 }
19537 "#
19538 .unindent(),
19539 );
19540
19541 cx.set_head_text("new diff base!");
19542 executor.run_until_parked();
19543 cx.assert_state_with_diff(
19544 r#"
19545 - new diff base!
19546 + use some::mod2;
19547 +
19548 + const A: u32 = 42;
19549 + const C: u32 = 42;
19550 +
19551 + fn main(ˇ) {
19552 + //println!("hello");
19553 +
19554 + println!("world");
19555 + //
19556 + //
19557 + }
19558 "#
19559 .unindent(),
19560 );
19561}
19562
19563#[gpui::test]
19564async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
19565 init_test(cx, |_| {});
19566
19567 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19568 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19569 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19570 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19571 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
19572 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
19573
19574 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
19575 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
19576 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
19577
19578 let multi_buffer = cx.new(|cx| {
19579 let mut multibuffer = MultiBuffer::new(ReadWrite);
19580 multibuffer.push_excerpts(
19581 buffer_1.clone(),
19582 [
19583 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19584 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19585 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19586 ],
19587 cx,
19588 );
19589 multibuffer.push_excerpts(
19590 buffer_2.clone(),
19591 [
19592 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19593 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19594 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19595 ],
19596 cx,
19597 );
19598 multibuffer.push_excerpts(
19599 buffer_3.clone(),
19600 [
19601 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19602 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19603 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19604 ],
19605 cx,
19606 );
19607 multibuffer
19608 });
19609
19610 let editor =
19611 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19612 editor
19613 .update(cx, |editor, _window, cx| {
19614 for (buffer, diff_base) in [
19615 (buffer_1.clone(), file_1_old),
19616 (buffer_2.clone(), file_2_old),
19617 (buffer_3.clone(), file_3_old),
19618 ] {
19619 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
19620 editor
19621 .buffer
19622 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
19623 }
19624 })
19625 .unwrap();
19626
19627 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19628 cx.run_until_parked();
19629
19630 cx.assert_editor_state(
19631 &"
19632 ˇaaa
19633 ccc
19634 ddd
19635
19636 ggg
19637 hhh
19638
19639
19640 lll
19641 mmm
19642 NNN
19643
19644 qqq
19645 rrr
19646
19647 uuu
19648 111
19649 222
19650 333
19651
19652 666
19653 777
19654
19655 000
19656 !!!"
19657 .unindent(),
19658 );
19659
19660 cx.update_editor(|editor, window, cx| {
19661 editor.select_all(&SelectAll, window, cx);
19662 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19663 });
19664 cx.executor().run_until_parked();
19665
19666 cx.assert_state_with_diff(
19667 "
19668 «aaa
19669 - bbb
19670 ccc
19671 ddd
19672
19673 ggg
19674 hhh
19675
19676
19677 lll
19678 mmm
19679 - nnn
19680 + NNN
19681
19682 qqq
19683 rrr
19684
19685 uuu
19686 111
19687 222
19688 333
19689
19690 + 666
19691 777
19692
19693 000
19694 !!!ˇ»"
19695 .unindent(),
19696 );
19697}
19698
19699#[gpui::test]
19700async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
19701 init_test(cx, |_| {});
19702
19703 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
19704 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
19705
19706 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
19707 let multi_buffer = cx.new(|cx| {
19708 let mut multibuffer = MultiBuffer::new(ReadWrite);
19709 multibuffer.push_excerpts(
19710 buffer.clone(),
19711 [
19712 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
19713 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
19714 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
19715 ],
19716 cx,
19717 );
19718 multibuffer
19719 });
19720
19721 let editor =
19722 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19723 editor
19724 .update(cx, |editor, _window, cx| {
19725 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
19726 editor
19727 .buffer
19728 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
19729 })
19730 .unwrap();
19731
19732 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19733 cx.run_until_parked();
19734
19735 cx.update_editor(|editor, window, cx| {
19736 editor.expand_all_diff_hunks(&Default::default(), window, cx)
19737 });
19738 cx.executor().run_until_parked();
19739
19740 // When the start of a hunk coincides with the start of its excerpt,
19741 // the hunk is expanded. When the start of a hunk is earlier than
19742 // the start of its excerpt, the hunk is not expanded.
19743 cx.assert_state_with_diff(
19744 "
19745 ˇaaa
19746 - bbb
19747 + BBB
19748
19749 - ddd
19750 - eee
19751 + DDD
19752 + EEE
19753 fff
19754
19755 iii
19756 "
19757 .unindent(),
19758 );
19759}
19760
19761#[gpui::test]
19762async fn test_edits_around_expanded_insertion_hunks(
19763 executor: BackgroundExecutor,
19764 cx: &mut TestAppContext,
19765) {
19766 init_test(cx, |_| {});
19767
19768 let mut cx = EditorTestContext::new(cx).await;
19769
19770 let diff_base = r#"
19771 use some::mod1;
19772 use some::mod2;
19773
19774 const A: u32 = 42;
19775
19776 fn main() {
19777 println!("hello");
19778
19779 println!("world");
19780 }
19781 "#
19782 .unindent();
19783 executor.run_until_parked();
19784 cx.set_state(
19785 &r#"
19786 use some::mod1;
19787 use some::mod2;
19788
19789 const A: u32 = 42;
19790 const B: u32 = 42;
19791 const C: u32 = 42;
19792 ˇ
19793
19794 fn main() {
19795 println!("hello");
19796
19797 println!("world");
19798 }
19799 "#
19800 .unindent(),
19801 );
19802
19803 cx.set_head_text(&diff_base);
19804 executor.run_until_parked();
19805
19806 cx.update_editor(|editor, window, cx| {
19807 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19808 });
19809 executor.run_until_parked();
19810
19811 cx.assert_state_with_diff(
19812 r#"
19813 use some::mod1;
19814 use some::mod2;
19815
19816 const A: u32 = 42;
19817 + const B: u32 = 42;
19818 + const C: u32 = 42;
19819 + ˇ
19820
19821 fn main() {
19822 println!("hello");
19823
19824 println!("world");
19825 }
19826 "#
19827 .unindent(),
19828 );
19829
19830 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
19831 executor.run_until_parked();
19832
19833 cx.assert_state_with_diff(
19834 r#"
19835 use some::mod1;
19836 use some::mod2;
19837
19838 const A: u32 = 42;
19839 + const B: u32 = 42;
19840 + const C: u32 = 42;
19841 + const D: u32 = 42;
19842 + ˇ
19843
19844 fn main() {
19845 println!("hello");
19846
19847 println!("world");
19848 }
19849 "#
19850 .unindent(),
19851 );
19852
19853 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
19854 executor.run_until_parked();
19855
19856 cx.assert_state_with_diff(
19857 r#"
19858 use some::mod1;
19859 use some::mod2;
19860
19861 const A: u32 = 42;
19862 + const B: u32 = 42;
19863 + const C: u32 = 42;
19864 + const D: u32 = 42;
19865 + const E: u32 = 42;
19866 + ˇ
19867
19868 fn main() {
19869 println!("hello");
19870
19871 println!("world");
19872 }
19873 "#
19874 .unindent(),
19875 );
19876
19877 cx.update_editor(|editor, window, cx| {
19878 editor.delete_line(&DeleteLine, window, cx);
19879 });
19880 executor.run_until_parked();
19881
19882 cx.assert_state_with_diff(
19883 r#"
19884 use some::mod1;
19885 use some::mod2;
19886
19887 const A: u32 = 42;
19888 + const B: u32 = 42;
19889 + const C: u32 = 42;
19890 + const D: u32 = 42;
19891 + const E: u32 = 42;
19892 ˇ
19893 fn main() {
19894 println!("hello");
19895
19896 println!("world");
19897 }
19898 "#
19899 .unindent(),
19900 );
19901
19902 cx.update_editor(|editor, window, cx| {
19903 editor.move_up(&MoveUp, window, cx);
19904 editor.delete_line(&DeleteLine, window, cx);
19905 editor.move_up(&MoveUp, window, cx);
19906 editor.delete_line(&DeleteLine, window, cx);
19907 editor.move_up(&MoveUp, window, cx);
19908 editor.delete_line(&DeleteLine, window, cx);
19909 });
19910 executor.run_until_parked();
19911 cx.assert_state_with_diff(
19912 r#"
19913 use some::mod1;
19914 use some::mod2;
19915
19916 const A: u32 = 42;
19917 + const B: u32 = 42;
19918 ˇ
19919 fn main() {
19920 println!("hello");
19921
19922 println!("world");
19923 }
19924 "#
19925 .unindent(),
19926 );
19927
19928 cx.update_editor(|editor, window, cx| {
19929 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
19930 editor.delete_line(&DeleteLine, window, cx);
19931 });
19932 executor.run_until_parked();
19933 cx.assert_state_with_diff(
19934 r#"
19935 ˇ
19936 fn main() {
19937 println!("hello");
19938
19939 println!("world");
19940 }
19941 "#
19942 .unindent(),
19943 );
19944}
19945
19946#[gpui::test]
19947async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
19948 init_test(cx, |_| {});
19949
19950 let mut cx = EditorTestContext::new(cx).await;
19951 cx.set_head_text(indoc! { "
19952 one
19953 two
19954 three
19955 four
19956 five
19957 "
19958 });
19959 cx.set_state(indoc! { "
19960 one
19961 ˇthree
19962 five
19963 "});
19964 cx.run_until_parked();
19965 cx.update_editor(|editor, window, cx| {
19966 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19967 });
19968 cx.assert_state_with_diff(
19969 indoc! { "
19970 one
19971 - two
19972 ˇthree
19973 - four
19974 five
19975 "}
19976 .to_string(),
19977 );
19978 cx.update_editor(|editor, window, cx| {
19979 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19980 });
19981
19982 cx.assert_state_with_diff(
19983 indoc! { "
19984 one
19985 ˇthree
19986 five
19987 "}
19988 .to_string(),
19989 );
19990
19991 cx.set_state(indoc! { "
19992 one
19993 ˇTWO
19994 three
19995 four
19996 five
19997 "});
19998 cx.run_until_parked();
19999 cx.update_editor(|editor, window, cx| {
20000 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20001 });
20002
20003 cx.assert_state_with_diff(
20004 indoc! { "
20005 one
20006 - two
20007 + ˇTWO
20008 three
20009 four
20010 five
20011 "}
20012 .to_string(),
20013 );
20014 cx.update_editor(|editor, window, cx| {
20015 editor.move_up(&Default::default(), window, cx);
20016 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20017 });
20018 cx.assert_state_with_diff(
20019 indoc! { "
20020 one
20021 ˇTWO
20022 three
20023 four
20024 five
20025 "}
20026 .to_string(),
20027 );
20028}
20029
20030#[gpui::test]
20031async fn test_edits_around_expanded_deletion_hunks(
20032 executor: BackgroundExecutor,
20033 cx: &mut TestAppContext,
20034) {
20035 init_test(cx, |_| {});
20036
20037 let mut cx = EditorTestContext::new(cx).await;
20038
20039 let diff_base = r#"
20040 use some::mod1;
20041 use some::mod2;
20042
20043 const A: u32 = 42;
20044 const B: u32 = 42;
20045 const C: u32 = 42;
20046
20047
20048 fn main() {
20049 println!("hello");
20050
20051 println!("world");
20052 }
20053 "#
20054 .unindent();
20055 executor.run_until_parked();
20056 cx.set_state(
20057 &r#"
20058 use some::mod1;
20059 use some::mod2;
20060
20061 ˇconst B: u32 = 42;
20062 const C: u32 = 42;
20063
20064
20065 fn main() {
20066 println!("hello");
20067
20068 println!("world");
20069 }
20070 "#
20071 .unindent(),
20072 );
20073
20074 cx.set_head_text(&diff_base);
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 use some::mod1;
20085 use some::mod2;
20086
20087 - const A: u32 = 42;
20088 ˇconst B: u32 = 42;
20089 const C: u32 = 42;
20090
20091
20092 fn main() {
20093 println!("hello");
20094
20095 println!("world");
20096 }
20097 "#
20098 .unindent(),
20099 );
20100
20101 cx.update_editor(|editor, window, cx| {
20102 editor.delete_line(&DeleteLine, window, cx);
20103 });
20104 executor.run_until_parked();
20105 cx.assert_state_with_diff(
20106 r#"
20107 use some::mod1;
20108 use some::mod2;
20109
20110 - const A: u32 = 42;
20111 - const B: u32 = 42;
20112 ˇconst C: u32 = 42;
20113
20114
20115 fn main() {
20116 println!("hello");
20117
20118 println!("world");
20119 }
20120 "#
20121 .unindent(),
20122 );
20123
20124 cx.update_editor(|editor, window, cx| {
20125 editor.delete_line(&DeleteLine, window, cx);
20126 });
20127 executor.run_until_parked();
20128 cx.assert_state_with_diff(
20129 r#"
20130 use some::mod1;
20131 use some::mod2;
20132
20133 - const A: u32 = 42;
20134 - const B: u32 = 42;
20135 - const C: u32 = 42;
20136 ˇ
20137
20138 fn main() {
20139 println!("hello");
20140
20141 println!("world");
20142 }
20143 "#
20144 .unindent(),
20145 );
20146
20147 cx.update_editor(|editor, window, cx| {
20148 editor.handle_input("replacement", window, cx);
20149 });
20150 executor.run_until_parked();
20151 cx.assert_state_with_diff(
20152 r#"
20153 use some::mod1;
20154 use some::mod2;
20155
20156 - const A: u32 = 42;
20157 - const B: u32 = 42;
20158 - const C: u32 = 42;
20159 -
20160 + replacementˇ
20161
20162 fn main() {
20163 println!("hello");
20164
20165 println!("world");
20166 }
20167 "#
20168 .unindent(),
20169 );
20170}
20171
20172#[gpui::test]
20173async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20174 init_test(cx, |_| {});
20175
20176 let mut cx = EditorTestContext::new(cx).await;
20177
20178 let base_text = r#"
20179 one
20180 two
20181 three
20182 four
20183 five
20184 "#
20185 .unindent();
20186 executor.run_until_parked();
20187 cx.set_state(
20188 &r#"
20189 one
20190 two
20191 fˇour
20192 five
20193 "#
20194 .unindent(),
20195 );
20196
20197 cx.set_head_text(&base_text);
20198 executor.run_until_parked();
20199
20200 cx.update_editor(|editor, window, cx| {
20201 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20202 });
20203 executor.run_until_parked();
20204
20205 cx.assert_state_with_diff(
20206 r#"
20207 one
20208 two
20209 - three
20210 fˇour
20211 five
20212 "#
20213 .unindent(),
20214 );
20215
20216 cx.update_editor(|editor, window, cx| {
20217 editor.backspace(&Backspace, window, cx);
20218 editor.backspace(&Backspace, window, cx);
20219 });
20220 executor.run_until_parked();
20221 cx.assert_state_with_diff(
20222 r#"
20223 one
20224 two
20225 - threeˇ
20226 - four
20227 + our
20228 five
20229 "#
20230 .unindent(),
20231 );
20232}
20233
20234#[gpui::test]
20235async fn test_edit_after_expanded_modification_hunk(
20236 executor: BackgroundExecutor,
20237 cx: &mut TestAppContext,
20238) {
20239 init_test(cx, |_| {});
20240
20241 let mut cx = EditorTestContext::new(cx).await;
20242
20243 let diff_base = r#"
20244 use some::mod1;
20245 use some::mod2;
20246
20247 const A: u32 = 42;
20248 const B: u32 = 42;
20249 const C: u32 = 42;
20250 const D: u32 = 42;
20251
20252
20253 fn main() {
20254 println!("hello");
20255
20256 println!("world");
20257 }"#
20258 .unindent();
20259
20260 cx.set_state(
20261 &r#"
20262 use some::mod1;
20263 use some::mod2;
20264
20265 const A: u32 = 42;
20266 const B: u32 = 42;
20267 const C: u32 = 43ˇ
20268 const D: u32 = 42;
20269
20270
20271 fn main() {
20272 println!("hello");
20273
20274 println!("world");
20275 }"#
20276 .unindent(),
20277 );
20278
20279 cx.set_head_text(&diff_base);
20280 executor.run_until_parked();
20281 cx.update_editor(|editor, window, cx| {
20282 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20283 });
20284 executor.run_until_parked();
20285
20286 cx.assert_state_with_diff(
20287 r#"
20288 use some::mod1;
20289 use some::mod2;
20290
20291 const A: u32 = 42;
20292 const B: u32 = 42;
20293 - const C: u32 = 42;
20294 + const C: u32 = 43ˇ
20295 const D: u32 = 42;
20296
20297
20298 fn main() {
20299 println!("hello");
20300
20301 println!("world");
20302 }"#
20303 .unindent(),
20304 );
20305
20306 cx.update_editor(|editor, window, cx| {
20307 editor.handle_input("\nnew_line\n", window, cx);
20308 });
20309 executor.run_until_parked();
20310
20311 cx.assert_state_with_diff(
20312 r#"
20313 use some::mod1;
20314 use some::mod2;
20315
20316 const A: u32 = 42;
20317 const B: u32 = 42;
20318 - const C: u32 = 42;
20319 + const C: u32 = 43
20320 + new_line
20321 + ˇ
20322 const D: u32 = 42;
20323
20324
20325 fn main() {
20326 println!("hello");
20327
20328 println!("world");
20329 }"#
20330 .unindent(),
20331 );
20332}
20333
20334#[gpui::test]
20335async fn test_stage_and_unstage_added_file_hunk(
20336 executor: BackgroundExecutor,
20337 cx: &mut TestAppContext,
20338) {
20339 init_test(cx, |_| {});
20340
20341 let mut cx = EditorTestContext::new(cx).await;
20342 cx.update_editor(|editor, _, cx| {
20343 editor.set_expand_all_diff_hunks(cx);
20344 });
20345
20346 let working_copy = r#"
20347 ˇfn main() {
20348 println!("hello, world!");
20349 }
20350 "#
20351 .unindent();
20352
20353 cx.set_state(&working_copy);
20354 executor.run_until_parked();
20355
20356 cx.assert_state_with_diff(
20357 r#"
20358 + ˇfn main() {
20359 + println!("hello, world!");
20360 + }
20361 "#
20362 .unindent(),
20363 );
20364 cx.assert_index_text(None);
20365
20366 cx.update_editor(|editor, window, cx| {
20367 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20368 });
20369 executor.run_until_parked();
20370 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
20371 cx.assert_state_with_diff(
20372 r#"
20373 + ˇfn main() {
20374 + println!("hello, world!");
20375 + }
20376 "#
20377 .unindent(),
20378 );
20379
20380 cx.update_editor(|editor, window, cx| {
20381 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20382 });
20383 executor.run_until_parked();
20384 cx.assert_index_text(None);
20385}
20386
20387async fn setup_indent_guides_editor(
20388 text: &str,
20389 cx: &mut TestAppContext,
20390) -> (BufferId, EditorTestContext) {
20391 init_test(cx, |_| {});
20392
20393 let mut cx = EditorTestContext::new(cx).await;
20394
20395 let buffer_id = cx.update_editor(|editor, window, cx| {
20396 editor.set_text(text, window, cx);
20397 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
20398
20399 buffer_ids[0]
20400 });
20401
20402 (buffer_id, cx)
20403}
20404
20405fn assert_indent_guides(
20406 range: Range<u32>,
20407 expected: Vec<IndentGuide>,
20408 active_indices: Option<Vec<usize>>,
20409 cx: &mut EditorTestContext,
20410) {
20411 let indent_guides = cx.update_editor(|editor, window, cx| {
20412 let snapshot = editor.snapshot(window, cx).display_snapshot;
20413 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
20414 editor,
20415 MultiBufferRow(range.start)..MultiBufferRow(range.end),
20416 true,
20417 &snapshot,
20418 cx,
20419 );
20420
20421 indent_guides.sort_by(|a, b| {
20422 a.depth.cmp(&b.depth).then(
20423 a.start_row
20424 .cmp(&b.start_row)
20425 .then(a.end_row.cmp(&b.end_row)),
20426 )
20427 });
20428 indent_guides
20429 });
20430
20431 if let Some(expected) = active_indices {
20432 let active_indices = cx.update_editor(|editor, window, cx| {
20433 let snapshot = editor.snapshot(window, cx).display_snapshot;
20434 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
20435 });
20436
20437 assert_eq!(
20438 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
20439 expected,
20440 "Active indent guide indices do not match"
20441 );
20442 }
20443
20444 assert_eq!(indent_guides, expected, "Indent guides do not match");
20445}
20446
20447fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
20448 IndentGuide {
20449 buffer_id,
20450 start_row: MultiBufferRow(start_row),
20451 end_row: MultiBufferRow(end_row),
20452 depth,
20453 tab_size: 4,
20454 settings: IndentGuideSettings {
20455 enabled: true,
20456 line_width: 1,
20457 active_line_width: 1,
20458 coloring: IndentGuideColoring::default(),
20459 background_coloring: IndentGuideBackgroundColoring::default(),
20460 },
20461 }
20462}
20463
20464#[gpui::test]
20465async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
20466 let (buffer_id, mut cx) = setup_indent_guides_editor(
20467 &"
20468 fn main() {
20469 let a = 1;
20470 }"
20471 .unindent(),
20472 cx,
20473 )
20474 .await;
20475
20476 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20477}
20478
20479#[gpui::test]
20480async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
20481 let (buffer_id, mut cx) = setup_indent_guides_editor(
20482 &"
20483 fn main() {
20484 let a = 1;
20485 let b = 2;
20486 }"
20487 .unindent(),
20488 cx,
20489 )
20490 .await;
20491
20492 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
20493}
20494
20495#[gpui::test]
20496async fn test_indent_guide_nested(cx: &mut TestAppContext) {
20497 let (buffer_id, mut cx) = setup_indent_guides_editor(
20498 &"
20499 fn main() {
20500 let a = 1;
20501 if a == 3 {
20502 let b = 2;
20503 } else {
20504 let c = 3;
20505 }
20506 }"
20507 .unindent(),
20508 cx,
20509 )
20510 .await;
20511
20512 assert_indent_guides(
20513 0..8,
20514 vec![
20515 indent_guide(buffer_id, 1, 6, 0),
20516 indent_guide(buffer_id, 3, 3, 1),
20517 indent_guide(buffer_id, 5, 5, 1),
20518 ],
20519 None,
20520 &mut cx,
20521 );
20522}
20523
20524#[gpui::test]
20525async fn test_indent_guide_tab(cx: &mut TestAppContext) {
20526 let (buffer_id, mut cx) = setup_indent_guides_editor(
20527 &"
20528 fn main() {
20529 let a = 1;
20530 let b = 2;
20531 let c = 3;
20532 }"
20533 .unindent(),
20534 cx,
20535 )
20536 .await;
20537
20538 assert_indent_guides(
20539 0..5,
20540 vec![
20541 indent_guide(buffer_id, 1, 3, 0),
20542 indent_guide(buffer_id, 2, 2, 1),
20543 ],
20544 None,
20545 &mut cx,
20546 );
20547}
20548
20549#[gpui::test]
20550async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
20551 let (buffer_id, mut cx) = setup_indent_guides_editor(
20552 &"
20553 fn main() {
20554 let a = 1;
20555
20556 let c = 3;
20557 }"
20558 .unindent(),
20559 cx,
20560 )
20561 .await;
20562
20563 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
20564}
20565
20566#[gpui::test]
20567async fn test_indent_guide_complex(cx: &mut TestAppContext) {
20568 let (buffer_id, mut cx) = setup_indent_guides_editor(
20569 &"
20570 fn main() {
20571 let a = 1;
20572
20573 let c = 3;
20574
20575 if a == 3 {
20576 let b = 2;
20577 } else {
20578 let c = 3;
20579 }
20580 }"
20581 .unindent(),
20582 cx,
20583 )
20584 .await;
20585
20586 assert_indent_guides(
20587 0..11,
20588 vec![
20589 indent_guide(buffer_id, 1, 9, 0),
20590 indent_guide(buffer_id, 6, 6, 1),
20591 indent_guide(buffer_id, 8, 8, 1),
20592 ],
20593 None,
20594 &mut cx,
20595 );
20596}
20597
20598#[gpui::test]
20599async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
20600 let (buffer_id, mut cx) = setup_indent_guides_editor(
20601 &"
20602 fn main() {
20603 let a = 1;
20604
20605 let c = 3;
20606
20607 if a == 3 {
20608 let b = 2;
20609 } else {
20610 let c = 3;
20611 }
20612 }"
20613 .unindent(),
20614 cx,
20615 )
20616 .await;
20617
20618 assert_indent_guides(
20619 1..11,
20620 vec![
20621 indent_guide(buffer_id, 1, 9, 0),
20622 indent_guide(buffer_id, 6, 6, 1),
20623 indent_guide(buffer_id, 8, 8, 1),
20624 ],
20625 None,
20626 &mut cx,
20627 );
20628}
20629
20630#[gpui::test]
20631async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
20632 let (buffer_id, mut cx) = setup_indent_guides_editor(
20633 &"
20634 fn main() {
20635 let a = 1;
20636
20637 let c = 3;
20638
20639 if a == 3 {
20640 let b = 2;
20641 } else {
20642 let c = 3;
20643 }
20644 }"
20645 .unindent(),
20646 cx,
20647 )
20648 .await;
20649
20650 assert_indent_guides(
20651 1..10,
20652 vec![
20653 indent_guide(buffer_id, 1, 9, 0),
20654 indent_guide(buffer_id, 6, 6, 1),
20655 indent_guide(buffer_id, 8, 8, 1),
20656 ],
20657 None,
20658 &mut cx,
20659 );
20660}
20661
20662#[gpui::test]
20663async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
20664 let (buffer_id, mut cx) = setup_indent_guides_editor(
20665 &"
20666 fn main() {
20667 if a {
20668 b(
20669 c,
20670 d,
20671 )
20672 } else {
20673 e(
20674 f
20675 )
20676 }
20677 }"
20678 .unindent(),
20679 cx,
20680 )
20681 .await;
20682
20683 assert_indent_guides(
20684 0..11,
20685 vec![
20686 indent_guide(buffer_id, 1, 10, 0),
20687 indent_guide(buffer_id, 2, 5, 1),
20688 indent_guide(buffer_id, 7, 9, 1),
20689 indent_guide(buffer_id, 3, 4, 2),
20690 indent_guide(buffer_id, 8, 8, 2),
20691 ],
20692 None,
20693 &mut cx,
20694 );
20695
20696 cx.update_editor(|editor, window, cx| {
20697 editor.fold_at(MultiBufferRow(2), window, cx);
20698 assert_eq!(
20699 editor.display_text(cx),
20700 "
20701 fn main() {
20702 if a {
20703 b(⋯
20704 )
20705 } else {
20706 e(
20707 f
20708 )
20709 }
20710 }"
20711 .unindent()
20712 );
20713 });
20714
20715 assert_indent_guides(
20716 0..11,
20717 vec![
20718 indent_guide(buffer_id, 1, 10, 0),
20719 indent_guide(buffer_id, 2, 5, 1),
20720 indent_guide(buffer_id, 7, 9, 1),
20721 indent_guide(buffer_id, 8, 8, 2),
20722 ],
20723 None,
20724 &mut cx,
20725 );
20726}
20727
20728#[gpui::test]
20729async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
20730 let (buffer_id, mut cx) = setup_indent_guides_editor(
20731 &"
20732 block1
20733 block2
20734 block3
20735 block4
20736 block2
20737 block1
20738 block1"
20739 .unindent(),
20740 cx,
20741 )
20742 .await;
20743
20744 assert_indent_guides(
20745 1..10,
20746 vec![
20747 indent_guide(buffer_id, 1, 4, 0),
20748 indent_guide(buffer_id, 2, 3, 1),
20749 indent_guide(buffer_id, 3, 3, 2),
20750 ],
20751 None,
20752 &mut cx,
20753 );
20754}
20755
20756#[gpui::test]
20757async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
20758 let (buffer_id, mut cx) = setup_indent_guides_editor(
20759 &"
20760 block1
20761 block2
20762 block3
20763
20764 block1
20765 block1"
20766 .unindent(),
20767 cx,
20768 )
20769 .await;
20770
20771 assert_indent_guides(
20772 0..6,
20773 vec![
20774 indent_guide(buffer_id, 1, 2, 0),
20775 indent_guide(buffer_id, 2, 2, 1),
20776 ],
20777 None,
20778 &mut cx,
20779 );
20780}
20781
20782#[gpui::test]
20783async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
20784 let (buffer_id, mut cx) = setup_indent_guides_editor(
20785 &"
20786 function component() {
20787 \treturn (
20788 \t\t\t
20789 \t\t<div>
20790 \t\t\t<abc></abc>
20791 \t\t</div>
20792 \t)
20793 }"
20794 .unindent(),
20795 cx,
20796 )
20797 .await;
20798
20799 assert_indent_guides(
20800 0..8,
20801 vec![
20802 indent_guide(buffer_id, 1, 6, 0),
20803 indent_guide(buffer_id, 2, 5, 1),
20804 indent_guide(buffer_id, 4, 4, 2),
20805 ],
20806 None,
20807 &mut cx,
20808 );
20809}
20810
20811#[gpui::test]
20812async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
20813 let (buffer_id, mut cx) = setup_indent_guides_editor(
20814 &"
20815 function component() {
20816 \treturn (
20817 \t
20818 \t\t<div>
20819 \t\t\t<abc></abc>
20820 \t\t</div>
20821 \t)
20822 }"
20823 .unindent(),
20824 cx,
20825 )
20826 .await;
20827
20828 assert_indent_guides(
20829 0..8,
20830 vec![
20831 indent_guide(buffer_id, 1, 6, 0),
20832 indent_guide(buffer_id, 2, 5, 1),
20833 indent_guide(buffer_id, 4, 4, 2),
20834 ],
20835 None,
20836 &mut cx,
20837 );
20838}
20839
20840#[gpui::test]
20841async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
20842 let (buffer_id, mut cx) = setup_indent_guides_editor(
20843 &"
20844 block1
20845
20846
20847
20848 block2
20849 "
20850 .unindent(),
20851 cx,
20852 )
20853 .await;
20854
20855 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20856}
20857
20858#[gpui::test]
20859async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
20860 let (buffer_id, mut cx) = setup_indent_guides_editor(
20861 &"
20862 def a:
20863 \tb = 3
20864 \tif True:
20865 \t\tc = 4
20866 \t\td = 5
20867 \tprint(b)
20868 "
20869 .unindent(),
20870 cx,
20871 )
20872 .await;
20873
20874 assert_indent_guides(
20875 0..6,
20876 vec![
20877 indent_guide(buffer_id, 1, 5, 0),
20878 indent_guide(buffer_id, 3, 4, 1),
20879 ],
20880 None,
20881 &mut cx,
20882 );
20883}
20884
20885#[gpui::test]
20886async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
20887 let (buffer_id, mut cx) = setup_indent_guides_editor(
20888 &"
20889 fn main() {
20890 let a = 1;
20891 }"
20892 .unindent(),
20893 cx,
20894 )
20895 .await;
20896
20897 cx.update_editor(|editor, window, cx| {
20898 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20899 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20900 });
20901 });
20902
20903 assert_indent_guides(
20904 0..3,
20905 vec![indent_guide(buffer_id, 1, 1, 0)],
20906 Some(vec![0]),
20907 &mut cx,
20908 );
20909}
20910
20911#[gpui::test]
20912async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
20913 let (buffer_id, mut cx) = setup_indent_guides_editor(
20914 &"
20915 fn main() {
20916 if 1 == 2 {
20917 let a = 1;
20918 }
20919 }"
20920 .unindent(),
20921 cx,
20922 )
20923 .await;
20924
20925 cx.update_editor(|editor, window, cx| {
20926 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20927 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20928 });
20929 });
20930
20931 assert_indent_guides(
20932 0..4,
20933 vec![
20934 indent_guide(buffer_id, 1, 3, 0),
20935 indent_guide(buffer_id, 2, 2, 1),
20936 ],
20937 Some(vec![1]),
20938 &mut cx,
20939 );
20940
20941 cx.update_editor(|editor, window, cx| {
20942 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20943 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20944 });
20945 });
20946
20947 assert_indent_guides(
20948 0..4,
20949 vec![
20950 indent_guide(buffer_id, 1, 3, 0),
20951 indent_guide(buffer_id, 2, 2, 1),
20952 ],
20953 Some(vec![1]),
20954 &mut cx,
20955 );
20956
20957 cx.update_editor(|editor, window, cx| {
20958 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20959 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
20960 });
20961 });
20962
20963 assert_indent_guides(
20964 0..4,
20965 vec![
20966 indent_guide(buffer_id, 1, 3, 0),
20967 indent_guide(buffer_id, 2, 2, 1),
20968 ],
20969 Some(vec![0]),
20970 &mut cx,
20971 );
20972}
20973
20974#[gpui::test]
20975async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
20976 let (buffer_id, mut cx) = setup_indent_guides_editor(
20977 &"
20978 fn main() {
20979 let a = 1;
20980
20981 let b = 2;
20982 }"
20983 .unindent(),
20984 cx,
20985 )
20986 .await;
20987
20988 cx.update_editor(|editor, window, cx| {
20989 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20990 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20991 });
20992 });
20993
20994 assert_indent_guides(
20995 0..5,
20996 vec![indent_guide(buffer_id, 1, 3, 0)],
20997 Some(vec![0]),
20998 &mut cx,
20999 );
21000}
21001
21002#[gpui::test]
21003async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
21004 let (buffer_id, mut cx) = setup_indent_guides_editor(
21005 &"
21006 def m:
21007 a = 1
21008 pass"
21009 .unindent(),
21010 cx,
21011 )
21012 .await;
21013
21014 cx.update_editor(|editor, window, cx| {
21015 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21016 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
21017 });
21018 });
21019
21020 assert_indent_guides(
21021 0..3,
21022 vec![indent_guide(buffer_id, 1, 2, 0)],
21023 Some(vec![0]),
21024 &mut cx,
21025 );
21026}
21027
21028#[gpui::test]
21029async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
21030 init_test(cx, |_| {});
21031 let mut cx = EditorTestContext::new(cx).await;
21032 let text = indoc! {
21033 "
21034 impl A {
21035 fn b() {
21036 0;
21037 3;
21038 5;
21039 6;
21040 7;
21041 }
21042 }
21043 "
21044 };
21045 let base_text = indoc! {
21046 "
21047 impl A {
21048 fn b() {
21049 0;
21050 1;
21051 2;
21052 3;
21053 4;
21054 }
21055 fn c() {
21056 5;
21057 6;
21058 7;
21059 }
21060 }
21061 "
21062 };
21063
21064 cx.update_editor(|editor, window, cx| {
21065 editor.set_text(text, window, cx);
21066
21067 editor.buffer().update(cx, |multibuffer, cx| {
21068 let buffer = multibuffer.as_singleton().unwrap();
21069 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
21070
21071 multibuffer.set_all_diff_hunks_expanded(cx);
21072 multibuffer.add_diff(diff, cx);
21073
21074 buffer.read(cx).remote_id()
21075 })
21076 });
21077 cx.run_until_parked();
21078
21079 cx.assert_state_with_diff(
21080 indoc! { "
21081 impl A {
21082 fn b() {
21083 0;
21084 - 1;
21085 - 2;
21086 3;
21087 - 4;
21088 - }
21089 - fn c() {
21090 5;
21091 6;
21092 7;
21093 }
21094 }
21095 ˇ"
21096 }
21097 .to_string(),
21098 );
21099
21100 let mut actual_guides = cx.update_editor(|editor, window, cx| {
21101 editor
21102 .snapshot(window, cx)
21103 .buffer_snapshot()
21104 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
21105 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
21106 .collect::<Vec<_>>()
21107 });
21108 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
21109 assert_eq!(
21110 actual_guides,
21111 vec![
21112 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
21113 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
21114 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
21115 ]
21116 );
21117}
21118
21119#[gpui::test]
21120async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
21121 init_test(cx, |_| {});
21122 let mut cx = EditorTestContext::new(cx).await;
21123
21124 let diff_base = r#"
21125 a
21126 b
21127 c
21128 "#
21129 .unindent();
21130
21131 cx.set_state(
21132 &r#"
21133 ˇA
21134 b
21135 C
21136 "#
21137 .unindent(),
21138 );
21139 cx.set_head_text(&diff_base);
21140 cx.update_editor(|editor, window, cx| {
21141 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21142 });
21143 executor.run_until_parked();
21144
21145 let both_hunks_expanded = r#"
21146 - a
21147 + ˇA
21148 b
21149 - c
21150 + C
21151 "#
21152 .unindent();
21153
21154 cx.assert_state_with_diff(both_hunks_expanded.clone());
21155
21156 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21157 let snapshot = editor.snapshot(window, cx);
21158 let hunks = editor
21159 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21160 .collect::<Vec<_>>();
21161 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21162 let buffer_id = hunks[0].buffer_id;
21163 hunks
21164 .into_iter()
21165 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
21166 .collect::<Vec<_>>()
21167 });
21168 assert_eq!(hunk_ranges.len(), 2);
21169
21170 cx.update_editor(|editor, _, cx| {
21171 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21172 });
21173 executor.run_until_parked();
21174
21175 let second_hunk_expanded = r#"
21176 ˇA
21177 b
21178 - c
21179 + C
21180 "#
21181 .unindent();
21182
21183 cx.assert_state_with_diff(second_hunk_expanded);
21184
21185 cx.update_editor(|editor, _, cx| {
21186 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21187 });
21188 executor.run_until_parked();
21189
21190 cx.assert_state_with_diff(both_hunks_expanded.clone());
21191
21192 cx.update_editor(|editor, _, cx| {
21193 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21194 });
21195 executor.run_until_parked();
21196
21197 let first_hunk_expanded = r#"
21198 - a
21199 + ˇA
21200 b
21201 C
21202 "#
21203 .unindent();
21204
21205 cx.assert_state_with_diff(first_hunk_expanded);
21206
21207 cx.update_editor(|editor, _, cx| {
21208 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21209 });
21210 executor.run_until_parked();
21211
21212 cx.assert_state_with_diff(both_hunks_expanded);
21213
21214 cx.set_state(
21215 &r#"
21216 ˇA
21217 b
21218 "#
21219 .unindent(),
21220 );
21221 cx.run_until_parked();
21222
21223 // TODO this cursor position seems bad
21224 cx.assert_state_with_diff(
21225 r#"
21226 - ˇa
21227 + A
21228 b
21229 "#
21230 .unindent(),
21231 );
21232
21233 cx.update_editor(|editor, window, cx| {
21234 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21235 });
21236
21237 cx.assert_state_with_diff(
21238 r#"
21239 - ˇa
21240 + A
21241 b
21242 - c
21243 "#
21244 .unindent(),
21245 );
21246
21247 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21248 let snapshot = editor.snapshot(window, cx);
21249 let hunks = editor
21250 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21251 .collect::<Vec<_>>();
21252 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21253 let buffer_id = hunks[0].buffer_id;
21254 hunks
21255 .into_iter()
21256 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
21257 .collect::<Vec<_>>()
21258 });
21259 assert_eq!(hunk_ranges.len(), 2);
21260
21261 cx.update_editor(|editor, _, cx| {
21262 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21263 });
21264 executor.run_until_parked();
21265
21266 cx.assert_state_with_diff(
21267 r#"
21268 - ˇa
21269 + A
21270 b
21271 "#
21272 .unindent(),
21273 );
21274}
21275
21276#[gpui::test]
21277async fn test_toggle_deletion_hunk_at_start_of_file(
21278 executor: BackgroundExecutor,
21279 cx: &mut TestAppContext,
21280) {
21281 init_test(cx, |_| {});
21282 let mut cx = EditorTestContext::new(cx).await;
21283
21284 let diff_base = r#"
21285 a
21286 b
21287 c
21288 "#
21289 .unindent();
21290
21291 cx.set_state(
21292 &r#"
21293 ˇb
21294 c
21295 "#
21296 .unindent(),
21297 );
21298 cx.set_head_text(&diff_base);
21299 cx.update_editor(|editor, window, cx| {
21300 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21301 });
21302 executor.run_until_parked();
21303
21304 let hunk_expanded = r#"
21305 - a
21306 ˇb
21307 c
21308 "#
21309 .unindent();
21310
21311 cx.assert_state_with_diff(hunk_expanded.clone());
21312
21313 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21314 let snapshot = editor.snapshot(window, cx);
21315 let hunks = editor
21316 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21317 .collect::<Vec<_>>();
21318 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21319 let buffer_id = hunks[0].buffer_id;
21320 hunks
21321 .into_iter()
21322 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
21323 .collect::<Vec<_>>()
21324 });
21325 assert_eq!(hunk_ranges.len(), 1);
21326
21327 cx.update_editor(|editor, _, cx| {
21328 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21329 });
21330 executor.run_until_parked();
21331
21332 let hunk_collapsed = r#"
21333 ˇb
21334 c
21335 "#
21336 .unindent();
21337
21338 cx.assert_state_with_diff(hunk_collapsed);
21339
21340 cx.update_editor(|editor, _, cx| {
21341 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21342 });
21343 executor.run_until_parked();
21344
21345 cx.assert_state_with_diff(hunk_expanded);
21346}
21347
21348#[gpui::test]
21349async fn test_display_diff_hunks(cx: &mut TestAppContext) {
21350 init_test(cx, |_| {});
21351
21352 let fs = FakeFs::new(cx.executor());
21353 fs.insert_tree(
21354 path!("/test"),
21355 json!({
21356 ".git": {},
21357 "file-1": "ONE\n",
21358 "file-2": "TWO\n",
21359 "file-3": "THREE\n",
21360 }),
21361 )
21362 .await;
21363
21364 fs.set_head_for_repo(
21365 path!("/test/.git").as_ref(),
21366 &[
21367 ("file-1", "one\n".into()),
21368 ("file-2", "two\n".into()),
21369 ("file-3", "three\n".into()),
21370 ],
21371 "deadbeef",
21372 );
21373
21374 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
21375 let mut buffers = vec![];
21376 for i in 1..=3 {
21377 let buffer = project
21378 .update(cx, |project, cx| {
21379 let path = format!(path!("/test/file-{}"), i);
21380 project.open_local_buffer(path, cx)
21381 })
21382 .await
21383 .unwrap();
21384 buffers.push(buffer);
21385 }
21386
21387 let multibuffer = cx.new(|cx| {
21388 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
21389 multibuffer.set_all_diff_hunks_expanded(cx);
21390 for buffer in &buffers {
21391 let snapshot = buffer.read(cx).snapshot();
21392 multibuffer.set_excerpts_for_path(
21393 PathKey::with_sort_prefix(0, buffer.read(cx).file().unwrap().path().clone()),
21394 buffer.clone(),
21395 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
21396 2,
21397 cx,
21398 );
21399 }
21400 multibuffer
21401 });
21402
21403 let editor = cx.add_window(|window, cx| {
21404 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
21405 });
21406 cx.run_until_parked();
21407
21408 let snapshot = editor
21409 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21410 .unwrap();
21411 let hunks = snapshot
21412 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
21413 .map(|hunk| match hunk {
21414 DisplayDiffHunk::Unfolded {
21415 display_row_range, ..
21416 } => display_row_range,
21417 DisplayDiffHunk::Folded { .. } => unreachable!(),
21418 })
21419 .collect::<Vec<_>>();
21420 assert_eq!(
21421 hunks,
21422 [
21423 DisplayRow(2)..DisplayRow(4),
21424 DisplayRow(7)..DisplayRow(9),
21425 DisplayRow(12)..DisplayRow(14),
21426 ]
21427 );
21428}
21429
21430#[gpui::test]
21431async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
21432 init_test(cx, |_| {});
21433
21434 let mut cx = EditorTestContext::new(cx).await;
21435 cx.set_head_text(indoc! { "
21436 one
21437 two
21438 three
21439 four
21440 five
21441 "
21442 });
21443 cx.set_index_text(indoc! { "
21444 one
21445 two
21446 three
21447 four
21448 five
21449 "
21450 });
21451 cx.set_state(indoc! {"
21452 one
21453 TWO
21454 ˇTHREE
21455 FOUR
21456 five
21457 "});
21458 cx.run_until_parked();
21459 cx.update_editor(|editor, window, cx| {
21460 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21461 });
21462 cx.run_until_parked();
21463 cx.assert_index_text(Some(indoc! {"
21464 one
21465 TWO
21466 THREE
21467 FOUR
21468 five
21469 "}));
21470 cx.set_state(indoc! { "
21471 one
21472 TWO
21473 ˇTHREE-HUNDRED
21474 FOUR
21475 five
21476 "});
21477 cx.run_until_parked();
21478 cx.update_editor(|editor, window, cx| {
21479 let snapshot = editor.snapshot(window, cx);
21480 let hunks = editor
21481 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21482 .collect::<Vec<_>>();
21483 assert_eq!(hunks.len(), 1);
21484 assert_eq!(
21485 hunks[0].status(),
21486 DiffHunkStatus {
21487 kind: DiffHunkStatusKind::Modified,
21488 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
21489 }
21490 );
21491
21492 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21493 });
21494 cx.run_until_parked();
21495 cx.assert_index_text(Some(indoc! {"
21496 one
21497 TWO
21498 THREE-HUNDRED
21499 FOUR
21500 five
21501 "}));
21502}
21503
21504#[gpui::test]
21505fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
21506 init_test(cx, |_| {});
21507
21508 let editor = cx.add_window(|window, cx| {
21509 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
21510 build_editor(buffer, window, cx)
21511 });
21512
21513 let render_args = Arc::new(Mutex::new(None));
21514 let snapshot = editor
21515 .update(cx, |editor, window, cx| {
21516 let snapshot = editor.buffer().read(cx).snapshot(cx);
21517 let range =
21518 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
21519
21520 struct RenderArgs {
21521 row: MultiBufferRow,
21522 folded: bool,
21523 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
21524 }
21525
21526 let crease = Crease::inline(
21527 range,
21528 FoldPlaceholder::test(),
21529 {
21530 let toggle_callback = render_args.clone();
21531 move |row, folded, callback, _window, _cx| {
21532 *toggle_callback.lock() = Some(RenderArgs {
21533 row,
21534 folded,
21535 callback,
21536 });
21537 div()
21538 }
21539 },
21540 |_row, _folded, _window, _cx| div(),
21541 );
21542
21543 editor.insert_creases(Some(crease), cx);
21544 let snapshot = editor.snapshot(window, cx);
21545 let _div =
21546 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
21547 snapshot
21548 })
21549 .unwrap();
21550
21551 let render_args = render_args.lock().take().unwrap();
21552 assert_eq!(render_args.row, MultiBufferRow(1));
21553 assert!(!render_args.folded);
21554 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21555
21556 cx.update_window(*editor, |_, window, cx| {
21557 (render_args.callback)(true, window, cx)
21558 })
21559 .unwrap();
21560 let snapshot = editor
21561 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21562 .unwrap();
21563 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
21564
21565 cx.update_window(*editor, |_, window, cx| {
21566 (render_args.callback)(false, window, cx)
21567 })
21568 .unwrap();
21569 let snapshot = editor
21570 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21571 .unwrap();
21572 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21573}
21574
21575#[gpui::test]
21576async fn test_input_text(cx: &mut TestAppContext) {
21577 init_test(cx, |_| {});
21578 let mut cx = EditorTestContext::new(cx).await;
21579
21580 cx.set_state(
21581 &r#"ˇone
21582 two
21583
21584 three
21585 fourˇ
21586 five
21587
21588 siˇx"#
21589 .unindent(),
21590 );
21591
21592 cx.dispatch_action(HandleInput(String::new()));
21593 cx.assert_editor_state(
21594 &r#"ˇone
21595 two
21596
21597 three
21598 fourˇ
21599 five
21600
21601 siˇx"#
21602 .unindent(),
21603 );
21604
21605 cx.dispatch_action(HandleInput("AAAA".to_string()));
21606 cx.assert_editor_state(
21607 &r#"AAAAˇone
21608 two
21609
21610 three
21611 fourAAAAˇ
21612 five
21613
21614 siAAAAˇx"#
21615 .unindent(),
21616 );
21617}
21618
21619#[gpui::test]
21620async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
21621 init_test(cx, |_| {});
21622
21623 let mut cx = EditorTestContext::new(cx).await;
21624 cx.set_state(
21625 r#"let foo = 1;
21626let foo = 2;
21627let foo = 3;
21628let fooˇ = 4;
21629let foo = 5;
21630let foo = 6;
21631let foo = 7;
21632let foo = 8;
21633let foo = 9;
21634let foo = 10;
21635let foo = 11;
21636let foo = 12;
21637let foo = 13;
21638let foo = 14;
21639let foo = 15;"#,
21640 );
21641
21642 cx.update_editor(|e, window, cx| {
21643 assert_eq!(
21644 e.next_scroll_position,
21645 NextScrollCursorCenterTopBottom::Center,
21646 "Default next scroll direction is center",
21647 );
21648
21649 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21650 assert_eq!(
21651 e.next_scroll_position,
21652 NextScrollCursorCenterTopBottom::Top,
21653 "After center, next scroll direction should be top",
21654 );
21655
21656 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21657 assert_eq!(
21658 e.next_scroll_position,
21659 NextScrollCursorCenterTopBottom::Bottom,
21660 "After top, next scroll direction should be bottom",
21661 );
21662
21663 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21664 assert_eq!(
21665 e.next_scroll_position,
21666 NextScrollCursorCenterTopBottom::Center,
21667 "After bottom, scrolling should start over",
21668 );
21669
21670 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21671 assert_eq!(
21672 e.next_scroll_position,
21673 NextScrollCursorCenterTopBottom::Top,
21674 "Scrolling continues if retriggered fast enough"
21675 );
21676 });
21677
21678 cx.executor()
21679 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
21680 cx.executor().run_until_parked();
21681 cx.update_editor(|e, _, _| {
21682 assert_eq!(
21683 e.next_scroll_position,
21684 NextScrollCursorCenterTopBottom::Center,
21685 "If scrolling is not triggered fast enough, it should reset"
21686 );
21687 });
21688}
21689
21690#[gpui::test]
21691async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
21692 init_test(cx, |_| {});
21693 let mut cx = EditorLspTestContext::new_rust(
21694 lsp::ServerCapabilities {
21695 definition_provider: Some(lsp::OneOf::Left(true)),
21696 references_provider: Some(lsp::OneOf::Left(true)),
21697 ..lsp::ServerCapabilities::default()
21698 },
21699 cx,
21700 )
21701 .await;
21702
21703 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
21704 let go_to_definition = cx
21705 .lsp
21706 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21707 move |params, _| async move {
21708 if empty_go_to_definition {
21709 Ok(None)
21710 } else {
21711 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
21712 uri: params.text_document_position_params.text_document.uri,
21713 range: lsp::Range::new(
21714 lsp::Position::new(4, 3),
21715 lsp::Position::new(4, 6),
21716 ),
21717 })))
21718 }
21719 },
21720 );
21721 let references = cx
21722 .lsp
21723 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21724 Ok(Some(vec![lsp::Location {
21725 uri: params.text_document_position.text_document.uri,
21726 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
21727 }]))
21728 });
21729 (go_to_definition, references)
21730 };
21731
21732 cx.set_state(
21733 &r#"fn one() {
21734 let mut a = ˇtwo();
21735 }
21736
21737 fn two() {}"#
21738 .unindent(),
21739 );
21740 set_up_lsp_handlers(false, &mut cx);
21741 let navigated = cx
21742 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21743 .await
21744 .expect("Failed to navigate to definition");
21745 assert_eq!(
21746 navigated,
21747 Navigated::Yes,
21748 "Should have navigated to definition from the GetDefinition response"
21749 );
21750 cx.assert_editor_state(
21751 &r#"fn one() {
21752 let mut a = two();
21753 }
21754
21755 fn «twoˇ»() {}"#
21756 .unindent(),
21757 );
21758
21759 let editors = cx.update_workspace(|workspace, _, cx| {
21760 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21761 });
21762 cx.update_editor(|_, _, test_editor_cx| {
21763 assert_eq!(
21764 editors.len(),
21765 1,
21766 "Initially, only one, test, editor should be open in the workspace"
21767 );
21768 assert_eq!(
21769 test_editor_cx.entity(),
21770 editors.last().expect("Asserted len is 1").clone()
21771 );
21772 });
21773
21774 set_up_lsp_handlers(true, &mut cx);
21775 let navigated = cx
21776 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21777 .await
21778 .expect("Failed to navigate to lookup references");
21779 assert_eq!(
21780 navigated,
21781 Navigated::Yes,
21782 "Should have navigated to references as a fallback after empty GoToDefinition response"
21783 );
21784 // We should not change the selections in the existing file,
21785 // if opening another milti buffer with the references
21786 cx.assert_editor_state(
21787 &r#"fn one() {
21788 let mut a = two();
21789 }
21790
21791 fn «twoˇ»() {}"#
21792 .unindent(),
21793 );
21794 let editors = cx.update_workspace(|workspace, _, cx| {
21795 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21796 });
21797 cx.update_editor(|_, _, test_editor_cx| {
21798 assert_eq!(
21799 editors.len(),
21800 2,
21801 "After falling back to references search, we open a new editor with the results"
21802 );
21803 let references_fallback_text = editors
21804 .into_iter()
21805 .find(|new_editor| *new_editor != test_editor_cx.entity())
21806 .expect("Should have one non-test editor now")
21807 .read(test_editor_cx)
21808 .text(test_editor_cx);
21809 assert_eq!(
21810 references_fallback_text, "fn one() {\n let mut a = two();\n}",
21811 "Should use the range from the references response and not the GoToDefinition one"
21812 );
21813 });
21814}
21815
21816#[gpui::test]
21817async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
21818 init_test(cx, |_| {});
21819 cx.update(|cx| {
21820 let mut editor_settings = EditorSettings::get_global(cx).clone();
21821 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
21822 EditorSettings::override_global(editor_settings, cx);
21823 });
21824 let mut cx = EditorLspTestContext::new_rust(
21825 lsp::ServerCapabilities {
21826 definition_provider: Some(lsp::OneOf::Left(true)),
21827 references_provider: Some(lsp::OneOf::Left(true)),
21828 ..lsp::ServerCapabilities::default()
21829 },
21830 cx,
21831 )
21832 .await;
21833 let original_state = r#"fn one() {
21834 let mut a = ˇtwo();
21835 }
21836
21837 fn two() {}"#
21838 .unindent();
21839 cx.set_state(&original_state);
21840
21841 let mut go_to_definition = cx
21842 .lsp
21843 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21844 move |_, _| async move { Ok(None) },
21845 );
21846 let _references = cx
21847 .lsp
21848 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
21849 panic!("Should not call for references with no go to definition fallback")
21850 });
21851
21852 let navigated = cx
21853 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21854 .await
21855 .expect("Failed to navigate to lookup references");
21856 go_to_definition
21857 .next()
21858 .await
21859 .expect("Should have called the go_to_definition handler");
21860
21861 assert_eq!(
21862 navigated,
21863 Navigated::No,
21864 "Should have navigated to references as a fallback after empty GoToDefinition response"
21865 );
21866 cx.assert_editor_state(&original_state);
21867 let editors = cx.update_workspace(|workspace, _, cx| {
21868 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21869 });
21870 cx.update_editor(|_, _, _| {
21871 assert_eq!(
21872 editors.len(),
21873 1,
21874 "After unsuccessful fallback, no other editor should have been opened"
21875 );
21876 });
21877}
21878
21879#[gpui::test]
21880async fn test_find_all_references_editor_reuse(cx: &mut TestAppContext) {
21881 init_test(cx, |_| {});
21882 let mut cx = EditorLspTestContext::new_rust(
21883 lsp::ServerCapabilities {
21884 references_provider: Some(lsp::OneOf::Left(true)),
21885 ..lsp::ServerCapabilities::default()
21886 },
21887 cx,
21888 )
21889 .await;
21890
21891 cx.set_state(
21892 &r#"
21893 fn one() {
21894 let mut a = two();
21895 }
21896
21897 fn ˇtwo() {}"#
21898 .unindent(),
21899 );
21900 cx.lsp
21901 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21902 Ok(Some(vec![
21903 lsp::Location {
21904 uri: params.text_document_position.text_document.uri.clone(),
21905 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21906 },
21907 lsp::Location {
21908 uri: params.text_document_position.text_document.uri,
21909 range: lsp::Range::new(lsp::Position::new(4, 4), lsp::Position::new(4, 7)),
21910 },
21911 ]))
21912 });
21913 let navigated = cx
21914 .update_editor(|editor, window, cx| {
21915 editor.find_all_references(&FindAllReferences, window, cx)
21916 })
21917 .unwrap()
21918 .await
21919 .expect("Failed to navigate to references");
21920 assert_eq!(
21921 navigated,
21922 Navigated::Yes,
21923 "Should have navigated to references from the FindAllReferences response"
21924 );
21925 cx.assert_editor_state(
21926 &r#"fn one() {
21927 let mut a = two();
21928 }
21929
21930 fn ˇtwo() {}"#
21931 .unindent(),
21932 );
21933
21934 let editors = cx.update_workspace(|workspace, _, cx| {
21935 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21936 });
21937 cx.update_editor(|_, _, _| {
21938 assert_eq!(editors.len(), 2, "We should have opened a new multibuffer");
21939 });
21940
21941 cx.set_state(
21942 &r#"fn one() {
21943 let mut a = ˇtwo();
21944 }
21945
21946 fn two() {}"#
21947 .unindent(),
21948 );
21949 let navigated = cx
21950 .update_editor(|editor, window, cx| {
21951 editor.find_all_references(&FindAllReferences, window, cx)
21952 })
21953 .unwrap()
21954 .await
21955 .expect("Failed to navigate to references");
21956 assert_eq!(
21957 navigated,
21958 Navigated::Yes,
21959 "Should have navigated to references from the FindAllReferences response"
21960 );
21961 cx.assert_editor_state(
21962 &r#"fn one() {
21963 let mut a = ˇtwo();
21964 }
21965
21966 fn two() {}"#
21967 .unindent(),
21968 );
21969 let editors = cx.update_workspace(|workspace, _, cx| {
21970 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21971 });
21972 cx.update_editor(|_, _, _| {
21973 assert_eq!(
21974 editors.len(),
21975 2,
21976 "should have re-used the previous multibuffer"
21977 );
21978 });
21979
21980 cx.set_state(
21981 &r#"fn one() {
21982 let mut a = ˇtwo();
21983 }
21984 fn three() {}
21985 fn two() {}"#
21986 .unindent(),
21987 );
21988 cx.lsp
21989 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21990 Ok(Some(vec![
21991 lsp::Location {
21992 uri: params.text_document_position.text_document.uri.clone(),
21993 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21994 },
21995 lsp::Location {
21996 uri: params.text_document_position.text_document.uri,
21997 range: lsp::Range::new(lsp::Position::new(5, 4), lsp::Position::new(5, 7)),
21998 },
21999 ]))
22000 });
22001 let navigated = cx
22002 .update_editor(|editor, window, cx| {
22003 editor.find_all_references(&FindAllReferences, window, cx)
22004 })
22005 .unwrap()
22006 .await
22007 .expect("Failed to navigate to references");
22008 assert_eq!(
22009 navigated,
22010 Navigated::Yes,
22011 "Should have navigated to references from the FindAllReferences response"
22012 );
22013 cx.assert_editor_state(
22014 &r#"fn one() {
22015 let mut a = ˇtwo();
22016 }
22017 fn three() {}
22018 fn two() {}"#
22019 .unindent(),
22020 );
22021 let editors = cx.update_workspace(|workspace, _, cx| {
22022 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22023 });
22024 cx.update_editor(|_, _, _| {
22025 assert_eq!(
22026 editors.len(),
22027 3,
22028 "should have used a new multibuffer as offsets changed"
22029 );
22030 });
22031}
22032#[gpui::test]
22033async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
22034 init_test(cx, |_| {});
22035
22036 let language = Arc::new(Language::new(
22037 LanguageConfig::default(),
22038 Some(tree_sitter_rust::LANGUAGE.into()),
22039 ));
22040
22041 let text = r#"
22042 #[cfg(test)]
22043 mod tests() {
22044 #[test]
22045 fn runnable_1() {
22046 let a = 1;
22047 }
22048
22049 #[test]
22050 fn runnable_2() {
22051 let a = 1;
22052 let b = 2;
22053 }
22054 }
22055 "#
22056 .unindent();
22057
22058 let fs = FakeFs::new(cx.executor());
22059 fs.insert_file("/file.rs", Default::default()).await;
22060
22061 let project = Project::test(fs, ["/a".as_ref()], cx).await;
22062 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22063 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22064 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
22065 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
22066
22067 let editor = cx.new_window_entity(|window, cx| {
22068 Editor::new(
22069 EditorMode::full(),
22070 multi_buffer,
22071 Some(project.clone()),
22072 window,
22073 cx,
22074 )
22075 });
22076
22077 editor.update_in(cx, |editor, window, cx| {
22078 let snapshot = editor.buffer().read(cx).snapshot(cx);
22079 editor.tasks.insert(
22080 (buffer.read(cx).remote_id(), 3),
22081 RunnableTasks {
22082 templates: vec![],
22083 offset: snapshot.anchor_before(43),
22084 column: 0,
22085 extra_variables: HashMap::default(),
22086 context_range: BufferOffset(43)..BufferOffset(85),
22087 },
22088 );
22089 editor.tasks.insert(
22090 (buffer.read(cx).remote_id(), 8),
22091 RunnableTasks {
22092 templates: vec![],
22093 offset: snapshot.anchor_before(86),
22094 column: 0,
22095 extra_variables: HashMap::default(),
22096 context_range: BufferOffset(86)..BufferOffset(191),
22097 },
22098 );
22099
22100 // Test finding task when cursor is inside function body
22101 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22102 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
22103 });
22104 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
22105 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
22106
22107 // Test finding task when cursor is on function name
22108 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22109 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
22110 });
22111 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
22112 assert_eq!(row, 8, "Should find task when cursor is on function name");
22113 });
22114}
22115
22116#[gpui::test]
22117async fn test_folding_buffers(cx: &mut TestAppContext) {
22118 init_test(cx, |_| {});
22119
22120 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
22121 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
22122 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
22123
22124 let fs = FakeFs::new(cx.executor());
22125 fs.insert_tree(
22126 path!("/a"),
22127 json!({
22128 "first.rs": sample_text_1,
22129 "second.rs": sample_text_2,
22130 "third.rs": sample_text_3,
22131 }),
22132 )
22133 .await;
22134 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22135 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22136 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22137 let worktree = project.update(cx, |project, cx| {
22138 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22139 assert_eq!(worktrees.len(), 1);
22140 worktrees.pop().unwrap()
22141 });
22142 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22143
22144 let buffer_1 = project
22145 .update(cx, |project, cx| {
22146 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
22147 })
22148 .await
22149 .unwrap();
22150 let buffer_2 = project
22151 .update(cx, |project, cx| {
22152 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
22153 })
22154 .await
22155 .unwrap();
22156 let buffer_3 = project
22157 .update(cx, |project, cx| {
22158 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
22159 })
22160 .await
22161 .unwrap();
22162
22163 let multi_buffer = cx.new(|cx| {
22164 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22165 multi_buffer.push_excerpts(
22166 buffer_1.clone(),
22167 [
22168 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22169 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22170 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22171 ],
22172 cx,
22173 );
22174 multi_buffer.push_excerpts(
22175 buffer_2.clone(),
22176 [
22177 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22178 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22179 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22180 ],
22181 cx,
22182 );
22183 multi_buffer.push_excerpts(
22184 buffer_3.clone(),
22185 [
22186 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22187 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22188 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22189 ],
22190 cx,
22191 );
22192 multi_buffer
22193 });
22194 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22195 Editor::new(
22196 EditorMode::full(),
22197 multi_buffer.clone(),
22198 Some(project.clone()),
22199 window,
22200 cx,
22201 )
22202 });
22203
22204 assert_eq!(
22205 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22206 "\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",
22207 );
22208
22209 multi_buffer_editor.update(cx, |editor, cx| {
22210 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
22211 });
22212 assert_eq!(
22213 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22214 "\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",
22215 "After folding the first buffer, its text should not be displayed"
22216 );
22217
22218 multi_buffer_editor.update(cx, |editor, cx| {
22219 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
22220 });
22221 assert_eq!(
22222 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22223 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
22224 "After folding the second buffer, its text should not be displayed"
22225 );
22226
22227 multi_buffer_editor.update(cx, |editor, cx| {
22228 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
22229 });
22230 assert_eq!(
22231 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22232 "\n\n\n\n\n",
22233 "After folding the third buffer, its text should not be displayed"
22234 );
22235
22236 // Emulate selection inside the fold logic, that should work
22237 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22238 editor
22239 .snapshot(window, cx)
22240 .next_line_boundary(Point::new(0, 4));
22241 });
22242
22243 multi_buffer_editor.update(cx, |editor, cx| {
22244 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
22245 });
22246 assert_eq!(
22247 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22248 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
22249 "After unfolding the second buffer, its text should be displayed"
22250 );
22251
22252 // Typing inside of buffer 1 causes that buffer to be unfolded.
22253 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22254 assert_eq!(
22255 multi_buffer
22256 .read(cx)
22257 .snapshot(cx)
22258 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
22259 .collect::<String>(),
22260 "bbbb"
22261 );
22262 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
22263 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
22264 });
22265 editor.handle_input("B", window, cx);
22266 });
22267
22268 assert_eq!(
22269 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22270 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
22271 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
22272 );
22273
22274 multi_buffer_editor.update(cx, |editor, cx| {
22275 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
22276 });
22277 assert_eq!(
22278 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22279 "\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",
22280 "After unfolding the all buffers, all original text should be displayed"
22281 );
22282}
22283
22284#[gpui::test]
22285async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
22286 init_test(cx, |_| {});
22287
22288 let sample_text_1 = "1111\n2222\n3333".to_string();
22289 let sample_text_2 = "4444\n5555\n6666".to_string();
22290 let sample_text_3 = "7777\n8888\n9999".to_string();
22291
22292 let fs = FakeFs::new(cx.executor());
22293 fs.insert_tree(
22294 path!("/a"),
22295 json!({
22296 "first.rs": sample_text_1,
22297 "second.rs": sample_text_2,
22298 "third.rs": sample_text_3,
22299 }),
22300 )
22301 .await;
22302 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22303 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22304 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22305 let worktree = project.update(cx, |project, cx| {
22306 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22307 assert_eq!(worktrees.len(), 1);
22308 worktrees.pop().unwrap()
22309 });
22310 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22311
22312 let buffer_1 = project
22313 .update(cx, |project, cx| {
22314 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
22315 })
22316 .await
22317 .unwrap();
22318 let buffer_2 = project
22319 .update(cx, |project, cx| {
22320 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
22321 })
22322 .await
22323 .unwrap();
22324 let buffer_3 = project
22325 .update(cx, |project, cx| {
22326 project.open_buffer((worktree_id, rel_path("third.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(Point::new(0, 0)..Point::new(3, 0))],
22336 cx,
22337 );
22338 multi_buffer.push_excerpts(
22339 buffer_2.clone(),
22340 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22341 cx,
22342 );
22343 multi_buffer.push_excerpts(
22344 buffer_3.clone(),
22345 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22346 cx,
22347 );
22348 multi_buffer
22349 });
22350
22351 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22352 Editor::new(
22353 EditorMode::full(),
22354 multi_buffer,
22355 Some(project.clone()),
22356 window,
22357 cx,
22358 )
22359 });
22360
22361 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
22362 assert_eq!(
22363 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22364 full_text,
22365 );
22366
22367 multi_buffer_editor.update(cx, |editor, cx| {
22368 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
22369 });
22370 assert_eq!(
22371 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22372 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
22373 "After folding the first buffer, its text should not be displayed"
22374 );
22375
22376 multi_buffer_editor.update(cx, |editor, cx| {
22377 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
22378 });
22379
22380 assert_eq!(
22381 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22382 "\n\n\n\n\n\n7777\n8888\n9999",
22383 "After folding the second buffer, its text should not be displayed"
22384 );
22385
22386 multi_buffer_editor.update(cx, |editor, cx| {
22387 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
22388 });
22389 assert_eq!(
22390 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22391 "\n\n\n\n\n",
22392 "After folding the third buffer, its text should not be displayed"
22393 );
22394
22395 multi_buffer_editor.update(cx, |editor, cx| {
22396 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
22397 });
22398 assert_eq!(
22399 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22400 "\n\n\n\n4444\n5555\n6666\n\n",
22401 "After unfolding the second buffer, its text should be displayed"
22402 );
22403
22404 multi_buffer_editor.update(cx, |editor, cx| {
22405 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
22406 });
22407 assert_eq!(
22408 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22409 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
22410 "After unfolding the first buffer, its text should be displayed"
22411 );
22412
22413 multi_buffer_editor.update(cx, |editor, cx| {
22414 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
22415 });
22416 assert_eq!(
22417 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22418 full_text,
22419 "After unfolding all buffers, all original text should be displayed"
22420 );
22421}
22422
22423#[gpui::test]
22424async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
22425 init_test(cx, |_| {});
22426
22427 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
22428
22429 let fs = FakeFs::new(cx.executor());
22430 fs.insert_tree(
22431 path!("/a"),
22432 json!({
22433 "main.rs": sample_text,
22434 }),
22435 )
22436 .await;
22437 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22438 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22439 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22440 let worktree = project.update(cx, |project, cx| {
22441 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22442 assert_eq!(worktrees.len(), 1);
22443 worktrees.pop().unwrap()
22444 });
22445 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22446
22447 let buffer_1 = project
22448 .update(cx, |project, cx| {
22449 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22450 })
22451 .await
22452 .unwrap();
22453
22454 let multi_buffer = cx.new(|cx| {
22455 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22456 multi_buffer.push_excerpts(
22457 buffer_1.clone(),
22458 [ExcerptRange::new(
22459 Point::new(0, 0)
22460 ..Point::new(
22461 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
22462 0,
22463 ),
22464 )],
22465 cx,
22466 );
22467 multi_buffer
22468 });
22469 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22470 Editor::new(
22471 EditorMode::full(),
22472 multi_buffer,
22473 Some(project.clone()),
22474 window,
22475 cx,
22476 )
22477 });
22478
22479 let selection_range = Point::new(1, 0)..Point::new(2, 0);
22480 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22481 enum TestHighlight {}
22482 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
22483 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
22484 editor.highlight_text::<TestHighlight>(
22485 vec![highlight_range.clone()],
22486 HighlightStyle::color(Hsla::green()),
22487 cx,
22488 );
22489 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22490 s.select_ranges(Some(highlight_range))
22491 });
22492 });
22493
22494 let full_text = format!("\n\n{sample_text}");
22495 assert_eq!(
22496 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22497 full_text,
22498 );
22499}
22500
22501#[gpui::test]
22502async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
22503 init_test(cx, |_| {});
22504 cx.update(|cx| {
22505 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
22506 "keymaps/default-linux.json",
22507 cx,
22508 )
22509 .unwrap();
22510 cx.bind_keys(default_key_bindings);
22511 });
22512
22513 let (editor, cx) = cx.add_window_view(|window, cx| {
22514 let multi_buffer = MultiBuffer::build_multi(
22515 [
22516 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
22517 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
22518 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
22519 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
22520 ],
22521 cx,
22522 );
22523 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
22524
22525 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
22526 // fold all but the second buffer, so that we test navigating between two
22527 // adjacent folded buffers, as well as folded buffers at the start and
22528 // end the multibuffer
22529 editor.fold_buffer(buffer_ids[0], cx);
22530 editor.fold_buffer(buffer_ids[2], cx);
22531 editor.fold_buffer(buffer_ids[3], cx);
22532
22533 editor
22534 });
22535 cx.simulate_resize(size(px(1000.), px(1000.)));
22536
22537 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
22538 cx.assert_excerpts_with_selections(indoc! {"
22539 [EXCERPT]
22540 ˇ[FOLDED]
22541 [EXCERPT]
22542 a1
22543 b1
22544 [EXCERPT]
22545 [FOLDED]
22546 [EXCERPT]
22547 [FOLDED]
22548 "
22549 });
22550 cx.simulate_keystroke("down");
22551 cx.assert_excerpts_with_selections(indoc! {"
22552 [EXCERPT]
22553 [FOLDED]
22554 [EXCERPT]
22555 ˇa1
22556 b1
22557 [EXCERPT]
22558 [FOLDED]
22559 [EXCERPT]
22560 [FOLDED]
22561 "
22562 });
22563 cx.simulate_keystroke("down");
22564 cx.assert_excerpts_with_selections(indoc! {"
22565 [EXCERPT]
22566 [FOLDED]
22567 [EXCERPT]
22568 a1
22569 ˇb1
22570 [EXCERPT]
22571 [FOLDED]
22572 [EXCERPT]
22573 [FOLDED]
22574 "
22575 });
22576 cx.simulate_keystroke("down");
22577 cx.assert_excerpts_with_selections(indoc! {"
22578 [EXCERPT]
22579 [FOLDED]
22580 [EXCERPT]
22581 a1
22582 b1
22583 ˇ[EXCERPT]
22584 [FOLDED]
22585 [EXCERPT]
22586 [FOLDED]
22587 "
22588 });
22589 cx.simulate_keystroke("down");
22590 cx.assert_excerpts_with_selections(indoc! {"
22591 [EXCERPT]
22592 [FOLDED]
22593 [EXCERPT]
22594 a1
22595 b1
22596 [EXCERPT]
22597 ˇ[FOLDED]
22598 [EXCERPT]
22599 [FOLDED]
22600 "
22601 });
22602 for _ in 0..5 {
22603 cx.simulate_keystroke("down");
22604 cx.assert_excerpts_with_selections(indoc! {"
22605 [EXCERPT]
22606 [FOLDED]
22607 [EXCERPT]
22608 a1
22609 b1
22610 [EXCERPT]
22611 [FOLDED]
22612 [EXCERPT]
22613 ˇ[FOLDED]
22614 "
22615 });
22616 }
22617
22618 cx.simulate_keystroke("up");
22619 cx.assert_excerpts_with_selections(indoc! {"
22620 [EXCERPT]
22621 [FOLDED]
22622 [EXCERPT]
22623 a1
22624 b1
22625 [EXCERPT]
22626 ˇ[FOLDED]
22627 [EXCERPT]
22628 [FOLDED]
22629 "
22630 });
22631 cx.simulate_keystroke("up");
22632 cx.assert_excerpts_with_selections(indoc! {"
22633 [EXCERPT]
22634 [FOLDED]
22635 [EXCERPT]
22636 a1
22637 b1
22638 ˇ[EXCERPT]
22639 [FOLDED]
22640 [EXCERPT]
22641 [FOLDED]
22642 "
22643 });
22644 cx.simulate_keystroke("up");
22645 cx.assert_excerpts_with_selections(indoc! {"
22646 [EXCERPT]
22647 [FOLDED]
22648 [EXCERPT]
22649 a1
22650 ˇb1
22651 [EXCERPT]
22652 [FOLDED]
22653 [EXCERPT]
22654 [FOLDED]
22655 "
22656 });
22657 cx.simulate_keystroke("up");
22658 cx.assert_excerpts_with_selections(indoc! {"
22659 [EXCERPT]
22660 [FOLDED]
22661 [EXCERPT]
22662 ˇa1
22663 b1
22664 [EXCERPT]
22665 [FOLDED]
22666 [EXCERPT]
22667 [FOLDED]
22668 "
22669 });
22670 for _ in 0..5 {
22671 cx.simulate_keystroke("up");
22672 cx.assert_excerpts_with_selections(indoc! {"
22673 [EXCERPT]
22674 ˇ[FOLDED]
22675 [EXCERPT]
22676 a1
22677 b1
22678 [EXCERPT]
22679 [FOLDED]
22680 [EXCERPT]
22681 [FOLDED]
22682 "
22683 });
22684 }
22685}
22686
22687#[gpui::test]
22688async fn test_edit_prediction_text(cx: &mut TestAppContext) {
22689 init_test(cx, |_| {});
22690
22691 // Simple insertion
22692 assert_highlighted_edits(
22693 "Hello, world!",
22694 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
22695 true,
22696 cx,
22697 |highlighted_edits, cx| {
22698 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
22699 assert_eq!(highlighted_edits.highlights.len(), 1);
22700 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
22701 assert_eq!(
22702 highlighted_edits.highlights[0].1.background_color,
22703 Some(cx.theme().status().created_background)
22704 );
22705 },
22706 )
22707 .await;
22708
22709 // Replacement
22710 assert_highlighted_edits(
22711 "This is a test.",
22712 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
22713 false,
22714 cx,
22715 |highlighted_edits, cx| {
22716 assert_eq!(highlighted_edits.text, "That is a test.");
22717 assert_eq!(highlighted_edits.highlights.len(), 1);
22718 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
22719 assert_eq!(
22720 highlighted_edits.highlights[0].1.background_color,
22721 Some(cx.theme().status().created_background)
22722 );
22723 },
22724 )
22725 .await;
22726
22727 // Multiple edits
22728 assert_highlighted_edits(
22729 "Hello, world!",
22730 vec![
22731 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
22732 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
22733 ],
22734 false,
22735 cx,
22736 |highlighted_edits, cx| {
22737 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
22738 assert_eq!(highlighted_edits.highlights.len(), 2);
22739 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
22740 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
22741 assert_eq!(
22742 highlighted_edits.highlights[0].1.background_color,
22743 Some(cx.theme().status().created_background)
22744 );
22745 assert_eq!(
22746 highlighted_edits.highlights[1].1.background_color,
22747 Some(cx.theme().status().created_background)
22748 );
22749 },
22750 )
22751 .await;
22752
22753 // Multiple lines with edits
22754 assert_highlighted_edits(
22755 "First line\nSecond line\nThird line\nFourth line",
22756 vec![
22757 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
22758 (
22759 Point::new(2, 0)..Point::new(2, 10),
22760 "New third line".to_string(),
22761 ),
22762 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
22763 ],
22764 false,
22765 cx,
22766 |highlighted_edits, cx| {
22767 assert_eq!(
22768 highlighted_edits.text,
22769 "Second modified\nNew third line\nFourth updated line"
22770 );
22771 assert_eq!(highlighted_edits.highlights.len(), 3);
22772 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
22773 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
22774 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
22775 for highlight in &highlighted_edits.highlights {
22776 assert_eq!(
22777 highlight.1.background_color,
22778 Some(cx.theme().status().created_background)
22779 );
22780 }
22781 },
22782 )
22783 .await;
22784}
22785
22786#[gpui::test]
22787async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
22788 init_test(cx, |_| {});
22789
22790 // Deletion
22791 assert_highlighted_edits(
22792 "Hello, world!",
22793 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
22794 true,
22795 cx,
22796 |highlighted_edits, cx| {
22797 assert_eq!(highlighted_edits.text, "Hello, world!");
22798 assert_eq!(highlighted_edits.highlights.len(), 1);
22799 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
22800 assert_eq!(
22801 highlighted_edits.highlights[0].1.background_color,
22802 Some(cx.theme().status().deleted_background)
22803 );
22804 },
22805 )
22806 .await;
22807
22808 // Insertion
22809 assert_highlighted_edits(
22810 "Hello, world!",
22811 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
22812 true,
22813 cx,
22814 |highlighted_edits, cx| {
22815 assert_eq!(highlighted_edits.highlights.len(), 1);
22816 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
22817 assert_eq!(
22818 highlighted_edits.highlights[0].1.background_color,
22819 Some(cx.theme().status().created_background)
22820 );
22821 },
22822 )
22823 .await;
22824}
22825
22826async fn assert_highlighted_edits(
22827 text: &str,
22828 edits: Vec<(Range<Point>, String)>,
22829 include_deletions: bool,
22830 cx: &mut TestAppContext,
22831 assertion_fn: impl Fn(HighlightedText, &App),
22832) {
22833 let window = cx.add_window(|window, cx| {
22834 let buffer = MultiBuffer::build_simple(text, cx);
22835 Editor::new(EditorMode::full(), buffer, None, window, cx)
22836 });
22837 let cx = &mut VisualTestContext::from_window(*window, cx);
22838
22839 let (buffer, snapshot) = window
22840 .update(cx, |editor, _window, cx| {
22841 (
22842 editor.buffer().clone(),
22843 editor.buffer().read(cx).snapshot(cx),
22844 )
22845 })
22846 .unwrap();
22847
22848 let edits = edits
22849 .into_iter()
22850 .map(|(range, edit)| {
22851 (
22852 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
22853 edit,
22854 )
22855 })
22856 .collect::<Vec<_>>();
22857
22858 let text_anchor_edits = edits
22859 .clone()
22860 .into_iter()
22861 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit.into()))
22862 .collect::<Vec<_>>();
22863
22864 let edit_preview = window
22865 .update(cx, |_, _window, cx| {
22866 buffer
22867 .read(cx)
22868 .as_singleton()
22869 .unwrap()
22870 .read(cx)
22871 .preview_edits(text_anchor_edits.into(), cx)
22872 })
22873 .unwrap()
22874 .await;
22875
22876 cx.update(|_window, cx| {
22877 let highlighted_edits = edit_prediction_edit_text(
22878 snapshot.as_singleton().unwrap().2,
22879 &edits,
22880 &edit_preview,
22881 include_deletions,
22882 cx,
22883 );
22884 assertion_fn(highlighted_edits, cx)
22885 });
22886}
22887
22888#[track_caller]
22889fn assert_breakpoint(
22890 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
22891 path: &Arc<Path>,
22892 expected: Vec<(u32, Breakpoint)>,
22893) {
22894 if expected.is_empty() {
22895 assert!(!breakpoints.contains_key(path), "{}", path.display());
22896 } else {
22897 let mut breakpoint = breakpoints
22898 .get(path)
22899 .unwrap()
22900 .iter()
22901 .map(|breakpoint| {
22902 (
22903 breakpoint.row,
22904 Breakpoint {
22905 message: breakpoint.message.clone(),
22906 state: breakpoint.state,
22907 condition: breakpoint.condition.clone(),
22908 hit_condition: breakpoint.hit_condition.clone(),
22909 },
22910 )
22911 })
22912 .collect::<Vec<_>>();
22913
22914 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
22915
22916 assert_eq!(expected, breakpoint);
22917 }
22918}
22919
22920fn add_log_breakpoint_at_cursor(
22921 editor: &mut Editor,
22922 log_message: &str,
22923 window: &mut Window,
22924 cx: &mut Context<Editor>,
22925) {
22926 let (anchor, bp) = editor
22927 .breakpoints_at_cursors(window, cx)
22928 .first()
22929 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
22930 .unwrap_or_else(|| {
22931 let snapshot = editor.snapshot(window, cx);
22932 let cursor_position: Point =
22933 editor.selections.newest(&snapshot.display_snapshot).head();
22934
22935 let breakpoint_position = snapshot
22936 .buffer_snapshot()
22937 .anchor_before(Point::new(cursor_position.row, 0));
22938
22939 (breakpoint_position, Breakpoint::new_log(log_message))
22940 });
22941
22942 editor.edit_breakpoint_at_anchor(
22943 anchor,
22944 bp,
22945 BreakpointEditAction::EditLogMessage(log_message.into()),
22946 cx,
22947 );
22948}
22949
22950#[gpui::test]
22951async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
22952 init_test(cx, |_| {});
22953
22954 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22955 let fs = FakeFs::new(cx.executor());
22956 fs.insert_tree(
22957 path!("/a"),
22958 json!({
22959 "main.rs": sample_text,
22960 }),
22961 )
22962 .await;
22963 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22964 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22965 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22966
22967 let fs = FakeFs::new(cx.executor());
22968 fs.insert_tree(
22969 path!("/a"),
22970 json!({
22971 "main.rs": sample_text,
22972 }),
22973 )
22974 .await;
22975 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22976 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22977 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22978 let worktree_id = workspace
22979 .update(cx, |workspace, _window, cx| {
22980 workspace.project().update(cx, |project, cx| {
22981 project.worktrees(cx).next().unwrap().read(cx).id()
22982 })
22983 })
22984 .unwrap();
22985
22986 let buffer = project
22987 .update(cx, |project, cx| {
22988 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22989 })
22990 .await
22991 .unwrap();
22992
22993 let (editor, cx) = cx.add_window_view(|window, cx| {
22994 Editor::new(
22995 EditorMode::full(),
22996 MultiBuffer::build_from_buffer(buffer, cx),
22997 Some(project.clone()),
22998 window,
22999 cx,
23000 )
23001 });
23002
23003 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
23004 let abs_path = project.read_with(cx, |project, cx| {
23005 project
23006 .absolute_path(&project_path, cx)
23007 .map(Arc::from)
23008 .unwrap()
23009 });
23010
23011 // assert we can add breakpoint on the first line
23012 editor.update_in(cx, |editor, window, cx| {
23013 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23014 editor.move_to_end(&MoveToEnd, window, cx);
23015 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23016 });
23017
23018 let breakpoints = editor.update(cx, |editor, cx| {
23019 editor
23020 .breakpoint_store()
23021 .as_ref()
23022 .unwrap()
23023 .read(cx)
23024 .all_source_breakpoints(cx)
23025 });
23026
23027 assert_eq!(1, breakpoints.len());
23028 assert_breakpoint(
23029 &breakpoints,
23030 &abs_path,
23031 vec![
23032 (0, Breakpoint::new_standard()),
23033 (3, Breakpoint::new_standard()),
23034 ],
23035 );
23036
23037 editor.update_in(cx, |editor, window, cx| {
23038 editor.move_to_beginning(&MoveToBeginning, window, cx);
23039 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23040 });
23041
23042 let breakpoints = editor.update(cx, |editor, cx| {
23043 editor
23044 .breakpoint_store()
23045 .as_ref()
23046 .unwrap()
23047 .read(cx)
23048 .all_source_breakpoints(cx)
23049 });
23050
23051 assert_eq!(1, breakpoints.len());
23052 assert_breakpoint(
23053 &breakpoints,
23054 &abs_path,
23055 vec![(3, Breakpoint::new_standard())],
23056 );
23057
23058 editor.update_in(cx, |editor, window, cx| {
23059 editor.move_to_end(&MoveToEnd, window, cx);
23060 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23061 });
23062
23063 let breakpoints = editor.update(cx, |editor, cx| {
23064 editor
23065 .breakpoint_store()
23066 .as_ref()
23067 .unwrap()
23068 .read(cx)
23069 .all_source_breakpoints(cx)
23070 });
23071
23072 assert_eq!(0, breakpoints.len());
23073 assert_breakpoint(&breakpoints, &abs_path, vec![]);
23074}
23075
23076#[gpui::test]
23077async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
23078 init_test(cx, |_| {});
23079
23080 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
23081
23082 let fs = FakeFs::new(cx.executor());
23083 fs.insert_tree(
23084 path!("/a"),
23085 json!({
23086 "main.rs": sample_text,
23087 }),
23088 )
23089 .await;
23090 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23091 let (workspace, cx) =
23092 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23093
23094 let worktree_id = workspace.update(cx, |workspace, cx| {
23095 workspace.project().update(cx, |project, cx| {
23096 project.worktrees(cx).next().unwrap().read(cx).id()
23097 })
23098 });
23099
23100 let buffer = project
23101 .update(cx, |project, cx| {
23102 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23103 })
23104 .await
23105 .unwrap();
23106
23107 let (editor, cx) = cx.add_window_view(|window, cx| {
23108 Editor::new(
23109 EditorMode::full(),
23110 MultiBuffer::build_from_buffer(buffer, cx),
23111 Some(project.clone()),
23112 window,
23113 cx,
23114 )
23115 });
23116
23117 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
23118 let abs_path = project.read_with(cx, |project, cx| {
23119 project
23120 .absolute_path(&project_path, cx)
23121 .map(Arc::from)
23122 .unwrap()
23123 });
23124
23125 editor.update_in(cx, |editor, window, cx| {
23126 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
23127 });
23128
23129 let breakpoints = editor.update(cx, |editor, cx| {
23130 editor
23131 .breakpoint_store()
23132 .as_ref()
23133 .unwrap()
23134 .read(cx)
23135 .all_source_breakpoints(cx)
23136 });
23137
23138 assert_breakpoint(
23139 &breakpoints,
23140 &abs_path,
23141 vec![(0, Breakpoint::new_log("hello world"))],
23142 );
23143
23144 // Removing a log message from a log breakpoint should remove it
23145 editor.update_in(cx, |editor, window, cx| {
23146 add_log_breakpoint_at_cursor(editor, "", window, cx);
23147 });
23148
23149 let breakpoints = editor.update(cx, |editor, cx| {
23150 editor
23151 .breakpoint_store()
23152 .as_ref()
23153 .unwrap()
23154 .read(cx)
23155 .all_source_breakpoints(cx)
23156 });
23157
23158 assert_breakpoint(&breakpoints, &abs_path, vec![]);
23159
23160 editor.update_in(cx, |editor, window, cx| {
23161 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23162 editor.move_to_end(&MoveToEnd, window, cx);
23163 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23164 // Not adding a log message to a standard breakpoint shouldn't remove it
23165 add_log_breakpoint_at_cursor(editor, "", window, cx);
23166 });
23167
23168 let breakpoints = editor.update(cx, |editor, cx| {
23169 editor
23170 .breakpoint_store()
23171 .as_ref()
23172 .unwrap()
23173 .read(cx)
23174 .all_source_breakpoints(cx)
23175 });
23176
23177 assert_breakpoint(
23178 &breakpoints,
23179 &abs_path,
23180 vec![
23181 (0, Breakpoint::new_standard()),
23182 (3, Breakpoint::new_standard()),
23183 ],
23184 );
23185
23186 editor.update_in(cx, |editor, window, cx| {
23187 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
23188 });
23189
23190 let breakpoints = editor.update(cx, |editor, cx| {
23191 editor
23192 .breakpoint_store()
23193 .as_ref()
23194 .unwrap()
23195 .read(cx)
23196 .all_source_breakpoints(cx)
23197 });
23198
23199 assert_breakpoint(
23200 &breakpoints,
23201 &abs_path,
23202 vec![
23203 (0, Breakpoint::new_standard()),
23204 (3, Breakpoint::new_log("hello world")),
23205 ],
23206 );
23207
23208 editor.update_in(cx, |editor, window, cx| {
23209 add_log_breakpoint_at_cursor(editor, "hello Earth!!", 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 assert_breakpoint(
23222 &breakpoints,
23223 &abs_path,
23224 vec![
23225 (0, Breakpoint::new_standard()),
23226 (3, Breakpoint::new_log("hello Earth!!")),
23227 ],
23228 );
23229}
23230
23231/// This also tests that Editor::breakpoint_at_cursor_head is working properly
23232/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
23233/// or when breakpoints were placed out of order. This tests for a regression too
23234#[gpui::test]
23235async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
23236 init_test(cx, |_| {});
23237
23238 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
23239 let fs = FakeFs::new(cx.executor());
23240 fs.insert_tree(
23241 path!("/a"),
23242 json!({
23243 "main.rs": sample_text,
23244 }),
23245 )
23246 .await;
23247 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23248 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23249 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23250
23251 let fs = FakeFs::new(cx.executor());
23252 fs.insert_tree(
23253 path!("/a"),
23254 json!({
23255 "main.rs": sample_text,
23256 }),
23257 )
23258 .await;
23259 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23260 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23261 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23262 let worktree_id = workspace
23263 .update(cx, |workspace, _window, cx| {
23264 workspace.project().update(cx, |project, cx| {
23265 project.worktrees(cx).next().unwrap().read(cx).id()
23266 })
23267 })
23268 .unwrap();
23269
23270 let buffer = project
23271 .update(cx, |project, cx| {
23272 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23273 })
23274 .await
23275 .unwrap();
23276
23277 let (editor, cx) = cx.add_window_view(|window, cx| {
23278 Editor::new(
23279 EditorMode::full(),
23280 MultiBuffer::build_from_buffer(buffer, cx),
23281 Some(project.clone()),
23282 window,
23283 cx,
23284 )
23285 });
23286
23287 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
23288 let abs_path = project.read_with(cx, |project, cx| {
23289 project
23290 .absolute_path(&project_path, cx)
23291 .map(Arc::from)
23292 .unwrap()
23293 });
23294
23295 // assert we can add breakpoint on the first line
23296 editor.update_in(cx, |editor, window, cx| {
23297 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23298 editor.move_to_end(&MoveToEnd, window, cx);
23299 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23300 editor.move_up(&MoveUp, window, cx);
23301 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23302 });
23303
23304 let breakpoints = editor.update(cx, |editor, cx| {
23305 editor
23306 .breakpoint_store()
23307 .as_ref()
23308 .unwrap()
23309 .read(cx)
23310 .all_source_breakpoints(cx)
23311 });
23312
23313 assert_eq!(1, breakpoints.len());
23314 assert_breakpoint(
23315 &breakpoints,
23316 &abs_path,
23317 vec![
23318 (0, Breakpoint::new_standard()),
23319 (2, Breakpoint::new_standard()),
23320 (3, Breakpoint::new_standard()),
23321 ],
23322 );
23323
23324 editor.update_in(cx, |editor, window, cx| {
23325 editor.move_to_beginning(&MoveToBeginning, window, cx);
23326 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23327 editor.move_to_end(&MoveToEnd, window, cx);
23328 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23329 // Disabling a breakpoint that doesn't exist should do nothing
23330 editor.move_up(&MoveUp, window, cx);
23331 editor.move_up(&MoveUp, window, cx);
23332 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23333 });
23334
23335 let breakpoints = editor.update(cx, |editor, cx| {
23336 editor
23337 .breakpoint_store()
23338 .as_ref()
23339 .unwrap()
23340 .read(cx)
23341 .all_source_breakpoints(cx)
23342 });
23343
23344 let disable_breakpoint = {
23345 let mut bp = Breakpoint::new_standard();
23346 bp.state = BreakpointState::Disabled;
23347 bp
23348 };
23349
23350 assert_eq!(1, breakpoints.len());
23351 assert_breakpoint(
23352 &breakpoints,
23353 &abs_path,
23354 vec![
23355 (0, disable_breakpoint.clone()),
23356 (2, Breakpoint::new_standard()),
23357 (3, disable_breakpoint.clone()),
23358 ],
23359 );
23360
23361 editor.update_in(cx, |editor, window, cx| {
23362 editor.move_to_beginning(&MoveToBeginning, window, cx);
23363 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
23364 editor.move_to_end(&MoveToEnd, window, cx);
23365 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
23366 editor.move_up(&MoveUp, window, cx);
23367 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23368 });
23369
23370 let breakpoints = editor.update(cx, |editor, cx| {
23371 editor
23372 .breakpoint_store()
23373 .as_ref()
23374 .unwrap()
23375 .read(cx)
23376 .all_source_breakpoints(cx)
23377 });
23378
23379 assert_eq!(1, breakpoints.len());
23380 assert_breakpoint(
23381 &breakpoints,
23382 &abs_path,
23383 vec![
23384 (0, Breakpoint::new_standard()),
23385 (2, disable_breakpoint),
23386 (3, Breakpoint::new_standard()),
23387 ],
23388 );
23389}
23390
23391#[gpui::test]
23392async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
23393 init_test(cx, |_| {});
23394 let capabilities = lsp::ServerCapabilities {
23395 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
23396 prepare_provider: Some(true),
23397 work_done_progress_options: Default::default(),
23398 })),
23399 ..Default::default()
23400 };
23401 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23402
23403 cx.set_state(indoc! {"
23404 struct Fˇoo {}
23405 "});
23406
23407 cx.update_editor(|editor, _, cx| {
23408 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23409 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23410 editor.highlight_background::<DocumentHighlightRead>(
23411 &[highlight_range],
23412 |theme| theme.colors().editor_document_highlight_read_background,
23413 cx,
23414 );
23415 });
23416
23417 let mut prepare_rename_handler = cx
23418 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
23419 move |_, _, _| async move {
23420 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
23421 start: lsp::Position {
23422 line: 0,
23423 character: 7,
23424 },
23425 end: lsp::Position {
23426 line: 0,
23427 character: 10,
23428 },
23429 })))
23430 },
23431 );
23432 let prepare_rename_task = cx
23433 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23434 .expect("Prepare rename was not started");
23435 prepare_rename_handler.next().await.unwrap();
23436 prepare_rename_task.await.expect("Prepare rename failed");
23437
23438 let mut rename_handler =
23439 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23440 let edit = lsp::TextEdit {
23441 range: lsp::Range {
23442 start: lsp::Position {
23443 line: 0,
23444 character: 7,
23445 },
23446 end: lsp::Position {
23447 line: 0,
23448 character: 10,
23449 },
23450 },
23451 new_text: "FooRenamed".to_string(),
23452 };
23453 Ok(Some(lsp::WorkspaceEdit::new(
23454 // Specify the same edit twice
23455 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
23456 )))
23457 });
23458 let rename_task = cx
23459 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23460 .expect("Confirm rename was not started");
23461 rename_handler.next().await.unwrap();
23462 rename_task.await.expect("Confirm rename failed");
23463 cx.run_until_parked();
23464
23465 // Despite two edits, only one is actually applied as those are identical
23466 cx.assert_editor_state(indoc! {"
23467 struct FooRenamedˇ {}
23468 "});
23469}
23470
23471#[gpui::test]
23472async fn test_rename_without_prepare(cx: &mut TestAppContext) {
23473 init_test(cx, |_| {});
23474 // These capabilities indicate that the server does not support prepare rename.
23475 let capabilities = lsp::ServerCapabilities {
23476 rename_provider: Some(lsp::OneOf::Left(true)),
23477 ..Default::default()
23478 };
23479 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23480
23481 cx.set_state(indoc! {"
23482 struct Fˇoo {}
23483 "});
23484
23485 cx.update_editor(|editor, _window, cx| {
23486 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23487 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23488 editor.highlight_background::<DocumentHighlightRead>(
23489 &[highlight_range],
23490 |theme| theme.colors().editor_document_highlight_read_background,
23491 cx,
23492 );
23493 });
23494
23495 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23496 .expect("Prepare rename was not started")
23497 .await
23498 .expect("Prepare rename failed");
23499
23500 let mut rename_handler =
23501 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23502 let edit = lsp::TextEdit {
23503 range: lsp::Range {
23504 start: lsp::Position {
23505 line: 0,
23506 character: 7,
23507 },
23508 end: lsp::Position {
23509 line: 0,
23510 character: 10,
23511 },
23512 },
23513 new_text: "FooRenamed".to_string(),
23514 };
23515 Ok(Some(lsp::WorkspaceEdit::new(
23516 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
23517 )))
23518 });
23519 let rename_task = cx
23520 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23521 .expect("Confirm rename was not started");
23522 rename_handler.next().await.unwrap();
23523 rename_task.await.expect("Confirm rename failed");
23524 cx.run_until_parked();
23525
23526 // Correct range is renamed, as `surrounding_word` is used to find it.
23527 cx.assert_editor_state(indoc! {"
23528 struct FooRenamedˇ {}
23529 "});
23530}
23531
23532#[gpui::test]
23533async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
23534 init_test(cx, |_| {});
23535 let mut cx = EditorTestContext::new(cx).await;
23536
23537 let language = Arc::new(
23538 Language::new(
23539 LanguageConfig::default(),
23540 Some(tree_sitter_html::LANGUAGE.into()),
23541 )
23542 .with_brackets_query(
23543 r#"
23544 ("<" @open "/>" @close)
23545 ("</" @open ">" @close)
23546 ("<" @open ">" @close)
23547 ("\"" @open "\"" @close)
23548 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
23549 "#,
23550 )
23551 .unwrap(),
23552 );
23553 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23554
23555 cx.set_state(indoc! {"
23556 <span>ˇ</span>
23557 "});
23558 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23559 cx.assert_editor_state(indoc! {"
23560 <span>
23561 ˇ
23562 </span>
23563 "});
23564
23565 cx.set_state(indoc! {"
23566 <span><span></span>ˇ</span>
23567 "});
23568 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23569 cx.assert_editor_state(indoc! {"
23570 <span><span></span>
23571 ˇ</span>
23572 "});
23573
23574 cx.set_state(indoc! {"
23575 <span>ˇ
23576 </span>
23577 "});
23578 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23579 cx.assert_editor_state(indoc! {"
23580 <span>
23581 ˇ
23582 </span>
23583 "});
23584}
23585
23586#[gpui::test(iterations = 10)]
23587async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
23588 init_test(cx, |_| {});
23589
23590 let fs = FakeFs::new(cx.executor());
23591 fs.insert_tree(
23592 path!("/dir"),
23593 json!({
23594 "a.ts": "a",
23595 }),
23596 )
23597 .await;
23598
23599 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
23600 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23601 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23602
23603 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23604 language_registry.add(Arc::new(Language::new(
23605 LanguageConfig {
23606 name: "TypeScript".into(),
23607 matcher: LanguageMatcher {
23608 path_suffixes: vec!["ts".to_string()],
23609 ..Default::default()
23610 },
23611 ..Default::default()
23612 },
23613 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
23614 )));
23615 let mut fake_language_servers = language_registry.register_fake_lsp(
23616 "TypeScript",
23617 FakeLspAdapter {
23618 capabilities: lsp::ServerCapabilities {
23619 code_lens_provider: Some(lsp::CodeLensOptions {
23620 resolve_provider: Some(true),
23621 }),
23622 execute_command_provider: Some(lsp::ExecuteCommandOptions {
23623 commands: vec!["_the/command".to_string()],
23624 ..lsp::ExecuteCommandOptions::default()
23625 }),
23626 ..lsp::ServerCapabilities::default()
23627 },
23628 ..FakeLspAdapter::default()
23629 },
23630 );
23631
23632 let editor = workspace
23633 .update(cx, |workspace, window, cx| {
23634 workspace.open_abs_path(
23635 PathBuf::from(path!("/dir/a.ts")),
23636 OpenOptions::default(),
23637 window,
23638 cx,
23639 )
23640 })
23641 .unwrap()
23642 .await
23643 .unwrap()
23644 .downcast::<Editor>()
23645 .unwrap();
23646 cx.executor().run_until_parked();
23647
23648 let fake_server = fake_language_servers.next().await.unwrap();
23649
23650 let buffer = editor.update(cx, |editor, cx| {
23651 editor
23652 .buffer()
23653 .read(cx)
23654 .as_singleton()
23655 .expect("have opened a single file by path")
23656 });
23657
23658 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
23659 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
23660 drop(buffer_snapshot);
23661 let actions = cx
23662 .update_window(*workspace, |_, window, cx| {
23663 project.code_actions(&buffer, anchor..anchor, window, cx)
23664 })
23665 .unwrap();
23666
23667 fake_server
23668 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23669 Ok(Some(vec![
23670 lsp::CodeLens {
23671 range: lsp::Range::default(),
23672 command: Some(lsp::Command {
23673 title: "Code lens command".to_owned(),
23674 command: "_the/command".to_owned(),
23675 arguments: None,
23676 }),
23677 data: None,
23678 },
23679 lsp::CodeLens {
23680 range: lsp::Range::default(),
23681 command: Some(lsp::Command {
23682 title: "Command not in capabilities".to_owned(),
23683 command: "not in capabilities".to_owned(),
23684 arguments: None,
23685 }),
23686 data: None,
23687 },
23688 lsp::CodeLens {
23689 range: lsp::Range {
23690 start: lsp::Position {
23691 line: 1,
23692 character: 1,
23693 },
23694 end: lsp::Position {
23695 line: 1,
23696 character: 1,
23697 },
23698 },
23699 command: Some(lsp::Command {
23700 title: "Command not in range".to_owned(),
23701 command: "_the/command".to_owned(),
23702 arguments: None,
23703 }),
23704 data: None,
23705 },
23706 ]))
23707 })
23708 .next()
23709 .await;
23710
23711 let actions = actions.await.unwrap();
23712 assert_eq!(
23713 actions.len(),
23714 1,
23715 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
23716 );
23717 let action = actions[0].clone();
23718 let apply = project.update(cx, |project, cx| {
23719 project.apply_code_action(buffer.clone(), action, true, cx)
23720 });
23721
23722 // Resolving the code action does not populate its edits. In absence of
23723 // edits, we must execute the given command.
23724 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
23725 |mut lens, _| async move {
23726 let lens_command = lens.command.as_mut().expect("should have a command");
23727 assert_eq!(lens_command.title, "Code lens command");
23728 lens_command.arguments = Some(vec![json!("the-argument")]);
23729 Ok(lens)
23730 },
23731 );
23732
23733 // While executing the command, the language server sends the editor
23734 // a `workspaceEdit` request.
23735 fake_server
23736 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
23737 let fake = fake_server.clone();
23738 move |params, _| {
23739 assert_eq!(params.command, "_the/command");
23740 let fake = fake.clone();
23741 async move {
23742 fake.server
23743 .request::<lsp::request::ApplyWorkspaceEdit>(
23744 lsp::ApplyWorkspaceEditParams {
23745 label: None,
23746 edit: lsp::WorkspaceEdit {
23747 changes: Some(
23748 [(
23749 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
23750 vec![lsp::TextEdit {
23751 range: lsp::Range::new(
23752 lsp::Position::new(0, 0),
23753 lsp::Position::new(0, 0),
23754 ),
23755 new_text: "X".into(),
23756 }],
23757 )]
23758 .into_iter()
23759 .collect(),
23760 ),
23761 ..lsp::WorkspaceEdit::default()
23762 },
23763 },
23764 )
23765 .await
23766 .into_response()
23767 .unwrap();
23768 Ok(Some(json!(null)))
23769 }
23770 }
23771 })
23772 .next()
23773 .await;
23774
23775 // Applying the code lens command returns a project transaction containing the edits
23776 // sent by the language server in its `workspaceEdit` request.
23777 let transaction = apply.await.unwrap();
23778 assert!(transaction.0.contains_key(&buffer));
23779 buffer.update(cx, |buffer, cx| {
23780 assert_eq!(buffer.text(), "Xa");
23781 buffer.undo(cx);
23782 assert_eq!(buffer.text(), "a");
23783 });
23784
23785 let actions_after_edits = cx
23786 .update_window(*workspace, |_, window, cx| {
23787 project.code_actions(&buffer, anchor..anchor, window, cx)
23788 })
23789 .unwrap()
23790 .await
23791 .unwrap();
23792 assert_eq!(
23793 actions, actions_after_edits,
23794 "For the same selection, same code lens actions should be returned"
23795 );
23796
23797 let _responses =
23798 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23799 panic!("No more code lens requests are expected");
23800 });
23801 editor.update_in(cx, |editor, window, cx| {
23802 editor.select_all(&SelectAll, window, cx);
23803 });
23804 cx.executor().run_until_parked();
23805 let new_actions = cx
23806 .update_window(*workspace, |_, window, cx| {
23807 project.code_actions(&buffer, anchor..anchor, window, cx)
23808 })
23809 .unwrap()
23810 .await
23811 .unwrap();
23812 assert_eq!(
23813 actions, new_actions,
23814 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
23815 );
23816}
23817
23818#[gpui::test]
23819async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
23820 init_test(cx, |_| {});
23821
23822 let fs = FakeFs::new(cx.executor());
23823 let main_text = r#"fn main() {
23824println!("1");
23825println!("2");
23826println!("3");
23827println!("4");
23828println!("5");
23829}"#;
23830 let lib_text = "mod foo {}";
23831 fs.insert_tree(
23832 path!("/a"),
23833 json!({
23834 "lib.rs": lib_text,
23835 "main.rs": main_text,
23836 }),
23837 )
23838 .await;
23839
23840 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23841 let (workspace, cx) =
23842 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23843 let worktree_id = workspace.update(cx, |workspace, cx| {
23844 workspace.project().update(cx, |project, cx| {
23845 project.worktrees(cx).next().unwrap().read(cx).id()
23846 })
23847 });
23848
23849 let expected_ranges = vec![
23850 Point::new(0, 0)..Point::new(0, 0),
23851 Point::new(1, 0)..Point::new(1, 1),
23852 Point::new(2, 0)..Point::new(2, 2),
23853 Point::new(3, 0)..Point::new(3, 3),
23854 ];
23855
23856 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23857 let editor_1 = workspace
23858 .update_in(cx, |workspace, window, cx| {
23859 workspace.open_path(
23860 (worktree_id, rel_path("main.rs")),
23861 Some(pane_1.downgrade()),
23862 true,
23863 window,
23864 cx,
23865 )
23866 })
23867 .unwrap()
23868 .await
23869 .downcast::<Editor>()
23870 .unwrap();
23871 pane_1.update(cx, |pane, cx| {
23872 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23873 open_editor.update(cx, |editor, cx| {
23874 assert_eq!(
23875 editor.display_text(cx),
23876 main_text,
23877 "Original main.rs text on initial open",
23878 );
23879 assert_eq!(
23880 editor
23881 .selections
23882 .all::<Point>(&editor.display_snapshot(cx))
23883 .into_iter()
23884 .map(|s| s.range())
23885 .collect::<Vec<_>>(),
23886 vec![Point::zero()..Point::zero()],
23887 "Default selections on initial open",
23888 );
23889 })
23890 });
23891 editor_1.update_in(cx, |editor, window, cx| {
23892 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23893 s.select_ranges(expected_ranges.clone());
23894 });
23895 });
23896
23897 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
23898 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
23899 });
23900 let editor_2 = workspace
23901 .update_in(cx, |workspace, window, cx| {
23902 workspace.open_path(
23903 (worktree_id, rel_path("main.rs")),
23904 Some(pane_2.downgrade()),
23905 true,
23906 window,
23907 cx,
23908 )
23909 })
23910 .unwrap()
23911 .await
23912 .downcast::<Editor>()
23913 .unwrap();
23914 pane_2.update(cx, |pane, cx| {
23915 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23916 open_editor.update(cx, |editor, cx| {
23917 assert_eq!(
23918 editor.display_text(cx),
23919 main_text,
23920 "Original main.rs text on initial open in another panel",
23921 );
23922 assert_eq!(
23923 editor
23924 .selections
23925 .all::<Point>(&editor.display_snapshot(cx))
23926 .into_iter()
23927 .map(|s| s.range())
23928 .collect::<Vec<_>>(),
23929 vec![Point::zero()..Point::zero()],
23930 "Default selections on initial open in another panel",
23931 );
23932 })
23933 });
23934
23935 editor_2.update_in(cx, |editor, window, cx| {
23936 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
23937 });
23938
23939 let _other_editor_1 = workspace
23940 .update_in(cx, |workspace, window, cx| {
23941 workspace.open_path(
23942 (worktree_id, rel_path("lib.rs")),
23943 Some(pane_1.downgrade()),
23944 true,
23945 window,
23946 cx,
23947 )
23948 })
23949 .unwrap()
23950 .await
23951 .downcast::<Editor>()
23952 .unwrap();
23953 pane_1
23954 .update_in(cx, |pane, window, cx| {
23955 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23956 })
23957 .await
23958 .unwrap();
23959 drop(editor_1);
23960 pane_1.update(cx, |pane, cx| {
23961 pane.active_item()
23962 .unwrap()
23963 .downcast::<Editor>()
23964 .unwrap()
23965 .update(cx, |editor, cx| {
23966 assert_eq!(
23967 editor.display_text(cx),
23968 lib_text,
23969 "Other file should be open and active",
23970 );
23971 });
23972 assert_eq!(pane.items().count(), 1, "No other editors should be open");
23973 });
23974
23975 let _other_editor_2 = workspace
23976 .update_in(cx, |workspace, window, cx| {
23977 workspace.open_path(
23978 (worktree_id, rel_path("lib.rs")),
23979 Some(pane_2.downgrade()),
23980 true,
23981 window,
23982 cx,
23983 )
23984 })
23985 .unwrap()
23986 .await
23987 .downcast::<Editor>()
23988 .unwrap();
23989 pane_2
23990 .update_in(cx, |pane, window, cx| {
23991 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23992 })
23993 .await
23994 .unwrap();
23995 drop(editor_2);
23996 pane_2.update(cx, |pane, cx| {
23997 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23998 open_editor.update(cx, |editor, cx| {
23999 assert_eq!(
24000 editor.display_text(cx),
24001 lib_text,
24002 "Other file should be open and active in another panel too",
24003 );
24004 });
24005 assert_eq!(
24006 pane.items().count(),
24007 1,
24008 "No other editors should be open in another pane",
24009 );
24010 });
24011
24012 let _editor_1_reopened = workspace
24013 .update_in(cx, |workspace, window, cx| {
24014 workspace.open_path(
24015 (worktree_id, rel_path("main.rs")),
24016 Some(pane_1.downgrade()),
24017 true,
24018 window,
24019 cx,
24020 )
24021 })
24022 .unwrap()
24023 .await
24024 .downcast::<Editor>()
24025 .unwrap();
24026 let _editor_2_reopened = workspace
24027 .update_in(cx, |workspace, window, cx| {
24028 workspace.open_path(
24029 (worktree_id, rel_path("main.rs")),
24030 Some(pane_2.downgrade()),
24031 true,
24032 window,
24033 cx,
24034 )
24035 })
24036 .unwrap()
24037 .await
24038 .downcast::<Editor>()
24039 .unwrap();
24040 pane_1.update(cx, |pane, cx| {
24041 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24042 open_editor.update(cx, |editor, cx| {
24043 assert_eq!(
24044 editor.display_text(cx),
24045 main_text,
24046 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
24047 );
24048 assert_eq!(
24049 editor
24050 .selections
24051 .all::<Point>(&editor.display_snapshot(cx))
24052 .into_iter()
24053 .map(|s| s.range())
24054 .collect::<Vec<_>>(),
24055 expected_ranges,
24056 "Previous editor in the 1st panel had selections and should get them restored on reopen",
24057 );
24058 })
24059 });
24060 pane_2.update(cx, |pane, cx| {
24061 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24062 open_editor.update(cx, |editor, cx| {
24063 assert_eq!(
24064 editor.display_text(cx),
24065 r#"fn main() {
24066⋯rintln!("1");
24067⋯intln!("2");
24068⋯ntln!("3");
24069println!("4");
24070println!("5");
24071}"#,
24072 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
24073 );
24074 assert_eq!(
24075 editor
24076 .selections
24077 .all::<Point>(&editor.display_snapshot(cx))
24078 .into_iter()
24079 .map(|s| s.range())
24080 .collect::<Vec<_>>(),
24081 vec![Point::zero()..Point::zero()],
24082 "Previous editor in the 2nd pane had no selections changed hence should restore none",
24083 );
24084 })
24085 });
24086}
24087
24088#[gpui::test]
24089async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
24090 init_test(cx, |_| {});
24091
24092 let fs = FakeFs::new(cx.executor());
24093 let main_text = r#"fn main() {
24094println!("1");
24095println!("2");
24096println!("3");
24097println!("4");
24098println!("5");
24099}"#;
24100 let lib_text = "mod foo {}";
24101 fs.insert_tree(
24102 path!("/a"),
24103 json!({
24104 "lib.rs": lib_text,
24105 "main.rs": main_text,
24106 }),
24107 )
24108 .await;
24109
24110 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24111 let (workspace, cx) =
24112 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24113 let worktree_id = workspace.update(cx, |workspace, cx| {
24114 workspace.project().update(cx, |project, cx| {
24115 project.worktrees(cx).next().unwrap().read(cx).id()
24116 })
24117 });
24118
24119 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24120 let editor = workspace
24121 .update_in(cx, |workspace, window, cx| {
24122 workspace.open_path(
24123 (worktree_id, rel_path("main.rs")),
24124 Some(pane.downgrade()),
24125 true,
24126 window,
24127 cx,
24128 )
24129 })
24130 .unwrap()
24131 .await
24132 .downcast::<Editor>()
24133 .unwrap();
24134 pane.update(cx, |pane, cx| {
24135 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24136 open_editor.update(cx, |editor, cx| {
24137 assert_eq!(
24138 editor.display_text(cx),
24139 main_text,
24140 "Original main.rs text on initial open",
24141 );
24142 })
24143 });
24144 editor.update_in(cx, |editor, window, cx| {
24145 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
24146 });
24147
24148 cx.update_global(|store: &mut SettingsStore, cx| {
24149 store.update_user_settings(cx, |s| {
24150 s.workspace.restore_on_file_reopen = Some(false);
24151 });
24152 });
24153 editor.update_in(cx, |editor, window, cx| {
24154 editor.fold_ranges(
24155 vec![
24156 Point::new(1, 0)..Point::new(1, 1),
24157 Point::new(2, 0)..Point::new(2, 2),
24158 Point::new(3, 0)..Point::new(3, 3),
24159 ],
24160 false,
24161 window,
24162 cx,
24163 );
24164 });
24165 pane.update_in(cx, |pane, window, cx| {
24166 pane.close_all_items(&CloseAllItems::default(), window, cx)
24167 })
24168 .await
24169 .unwrap();
24170 pane.update(cx, |pane, _| {
24171 assert!(pane.active_item().is_none());
24172 });
24173 cx.update_global(|store: &mut SettingsStore, cx| {
24174 store.update_user_settings(cx, |s| {
24175 s.workspace.restore_on_file_reopen = Some(true);
24176 });
24177 });
24178
24179 let _editor_reopened = workspace
24180 .update_in(cx, |workspace, window, cx| {
24181 workspace.open_path(
24182 (worktree_id, rel_path("main.rs")),
24183 Some(pane.downgrade()),
24184 true,
24185 window,
24186 cx,
24187 )
24188 })
24189 .unwrap()
24190 .await
24191 .downcast::<Editor>()
24192 .unwrap();
24193 pane.update(cx, |pane, cx| {
24194 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24195 open_editor.update(cx, |editor, cx| {
24196 assert_eq!(
24197 editor.display_text(cx),
24198 main_text,
24199 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
24200 );
24201 })
24202 });
24203}
24204
24205#[gpui::test]
24206async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
24207 struct EmptyModalView {
24208 focus_handle: gpui::FocusHandle,
24209 }
24210 impl EventEmitter<DismissEvent> for EmptyModalView {}
24211 impl Render for EmptyModalView {
24212 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
24213 div()
24214 }
24215 }
24216 impl Focusable for EmptyModalView {
24217 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
24218 self.focus_handle.clone()
24219 }
24220 }
24221 impl workspace::ModalView for EmptyModalView {}
24222 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
24223 EmptyModalView {
24224 focus_handle: cx.focus_handle(),
24225 }
24226 }
24227
24228 init_test(cx, |_| {});
24229
24230 let fs = FakeFs::new(cx.executor());
24231 let project = Project::test(fs, [], cx).await;
24232 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24233 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
24234 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24235 let editor = cx.new_window_entity(|window, cx| {
24236 Editor::new(
24237 EditorMode::full(),
24238 buffer,
24239 Some(project.clone()),
24240 window,
24241 cx,
24242 )
24243 });
24244 workspace
24245 .update(cx, |workspace, window, cx| {
24246 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
24247 })
24248 .unwrap();
24249 editor.update_in(cx, |editor, window, cx| {
24250 editor.open_context_menu(&OpenContextMenu, window, cx);
24251 assert!(editor.mouse_context_menu.is_some());
24252 });
24253 workspace
24254 .update(cx, |workspace, window, cx| {
24255 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
24256 })
24257 .unwrap();
24258 cx.read(|cx| {
24259 assert!(editor.read(cx).mouse_context_menu.is_none());
24260 });
24261}
24262
24263fn set_linked_edit_ranges(
24264 opening: (Point, Point),
24265 closing: (Point, Point),
24266 editor: &mut Editor,
24267 cx: &mut Context<Editor>,
24268) {
24269 let Some((buffer, _)) = editor
24270 .buffer
24271 .read(cx)
24272 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
24273 else {
24274 panic!("Failed to get buffer for selection position");
24275 };
24276 let buffer = buffer.read(cx);
24277 let buffer_id = buffer.remote_id();
24278 let opening_range = buffer.anchor_before(opening.0)..buffer.anchor_after(opening.1);
24279 let closing_range = buffer.anchor_before(closing.0)..buffer.anchor_after(closing.1);
24280 let mut linked_ranges = HashMap::default();
24281 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
24282 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
24283}
24284
24285#[gpui::test]
24286async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
24287 init_test(cx, |_| {});
24288
24289 let fs = FakeFs::new(cx.executor());
24290 fs.insert_file(path!("/file.html"), Default::default())
24291 .await;
24292
24293 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
24294
24295 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24296 let html_language = Arc::new(Language::new(
24297 LanguageConfig {
24298 name: "HTML".into(),
24299 matcher: LanguageMatcher {
24300 path_suffixes: vec!["html".to_string()],
24301 ..LanguageMatcher::default()
24302 },
24303 brackets: BracketPairConfig {
24304 pairs: vec![BracketPair {
24305 start: "<".into(),
24306 end: ">".into(),
24307 close: true,
24308 ..Default::default()
24309 }],
24310 ..Default::default()
24311 },
24312 ..Default::default()
24313 },
24314 Some(tree_sitter_html::LANGUAGE.into()),
24315 ));
24316 language_registry.add(html_language);
24317 let mut fake_servers = language_registry.register_fake_lsp(
24318 "HTML",
24319 FakeLspAdapter {
24320 capabilities: lsp::ServerCapabilities {
24321 completion_provider: Some(lsp::CompletionOptions {
24322 resolve_provider: Some(true),
24323 ..Default::default()
24324 }),
24325 ..Default::default()
24326 },
24327 ..Default::default()
24328 },
24329 );
24330
24331 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24332 let cx = &mut VisualTestContext::from_window(*workspace, cx);
24333
24334 let worktree_id = workspace
24335 .update(cx, |workspace, _window, cx| {
24336 workspace.project().update(cx, |project, cx| {
24337 project.worktrees(cx).next().unwrap().read(cx).id()
24338 })
24339 })
24340 .unwrap();
24341 project
24342 .update(cx, |project, cx| {
24343 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
24344 })
24345 .await
24346 .unwrap();
24347 let editor = workspace
24348 .update(cx, |workspace, window, cx| {
24349 workspace.open_path((worktree_id, rel_path("file.html")), None, true, window, cx)
24350 })
24351 .unwrap()
24352 .await
24353 .unwrap()
24354 .downcast::<Editor>()
24355 .unwrap();
24356
24357 let fake_server = fake_servers.next().await.unwrap();
24358 editor.update_in(cx, |editor, window, cx| {
24359 editor.set_text("<ad></ad>", window, cx);
24360 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
24361 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
24362 });
24363 set_linked_edit_ranges(
24364 (Point::new(0, 1), Point::new(0, 3)),
24365 (Point::new(0, 6), Point::new(0, 8)),
24366 editor,
24367 cx,
24368 );
24369 });
24370 let mut completion_handle =
24371 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
24372 Ok(Some(lsp::CompletionResponse::Array(vec![
24373 lsp::CompletionItem {
24374 label: "head".to_string(),
24375 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
24376 lsp::InsertReplaceEdit {
24377 new_text: "head".to_string(),
24378 insert: lsp::Range::new(
24379 lsp::Position::new(0, 1),
24380 lsp::Position::new(0, 3),
24381 ),
24382 replace: lsp::Range::new(
24383 lsp::Position::new(0, 1),
24384 lsp::Position::new(0, 3),
24385 ),
24386 },
24387 )),
24388 ..Default::default()
24389 },
24390 ])))
24391 });
24392 editor.update_in(cx, |editor, window, cx| {
24393 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
24394 });
24395 cx.run_until_parked();
24396 completion_handle.next().await.unwrap();
24397 editor.update(cx, |editor, _| {
24398 assert!(
24399 editor.context_menu_visible(),
24400 "Completion menu should be visible"
24401 );
24402 });
24403 editor.update_in(cx, |editor, window, cx| {
24404 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
24405 });
24406 cx.executor().run_until_parked();
24407 editor.update(cx, |editor, cx| {
24408 assert_eq!(editor.text(cx), "<head></head>");
24409 });
24410}
24411
24412#[gpui::test]
24413async fn test_linked_edits_on_typing_punctuation(cx: &mut TestAppContext) {
24414 init_test(cx, |_| {});
24415
24416 let mut cx = EditorTestContext::new(cx).await;
24417 let language = Arc::new(Language::new(
24418 LanguageConfig {
24419 name: "TSX".into(),
24420 matcher: LanguageMatcher {
24421 path_suffixes: vec!["tsx".to_string()],
24422 ..LanguageMatcher::default()
24423 },
24424 brackets: BracketPairConfig {
24425 pairs: vec![BracketPair {
24426 start: "<".into(),
24427 end: ">".into(),
24428 close: true,
24429 ..Default::default()
24430 }],
24431 ..Default::default()
24432 },
24433 linked_edit_characters: HashSet::from_iter(['.']),
24434 ..Default::default()
24435 },
24436 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
24437 ));
24438 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24439
24440 // Test typing > does not extend linked pair
24441 cx.set_state("<divˇ<div></div>");
24442 cx.update_editor(|editor, _, cx| {
24443 set_linked_edit_ranges(
24444 (Point::new(0, 1), Point::new(0, 4)),
24445 (Point::new(0, 11), Point::new(0, 14)),
24446 editor,
24447 cx,
24448 );
24449 });
24450 cx.update_editor(|editor, window, cx| {
24451 editor.handle_input(">", window, cx);
24452 });
24453 cx.assert_editor_state("<div>ˇ<div></div>");
24454
24455 // Test typing . do extend linked pair
24456 cx.set_state("<Animatedˇ></Animated>");
24457 cx.update_editor(|editor, _, cx| {
24458 set_linked_edit_ranges(
24459 (Point::new(0, 1), Point::new(0, 9)),
24460 (Point::new(0, 12), Point::new(0, 20)),
24461 editor,
24462 cx,
24463 );
24464 });
24465 cx.update_editor(|editor, window, cx| {
24466 editor.handle_input(".", window, cx);
24467 });
24468 cx.assert_editor_state("<Animated.ˇ></Animated.>");
24469 cx.update_editor(|editor, _, cx| {
24470 set_linked_edit_ranges(
24471 (Point::new(0, 1), Point::new(0, 10)),
24472 (Point::new(0, 13), Point::new(0, 21)),
24473 editor,
24474 cx,
24475 );
24476 });
24477 cx.update_editor(|editor, window, cx| {
24478 editor.handle_input("V", window, cx);
24479 });
24480 cx.assert_editor_state("<Animated.Vˇ></Animated.V>");
24481}
24482
24483#[gpui::test]
24484async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
24485 init_test(cx, |_| {});
24486
24487 let fs = FakeFs::new(cx.executor());
24488 fs.insert_tree(
24489 path!("/root"),
24490 json!({
24491 "a": {
24492 "main.rs": "fn main() {}",
24493 },
24494 "foo": {
24495 "bar": {
24496 "external_file.rs": "pub mod external {}",
24497 }
24498 }
24499 }),
24500 )
24501 .await;
24502
24503 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
24504 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24505 language_registry.add(rust_lang());
24506 let _fake_servers = language_registry.register_fake_lsp(
24507 "Rust",
24508 FakeLspAdapter {
24509 ..FakeLspAdapter::default()
24510 },
24511 );
24512 let (workspace, cx) =
24513 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24514 let worktree_id = workspace.update(cx, |workspace, cx| {
24515 workspace.project().update(cx, |project, cx| {
24516 project.worktrees(cx).next().unwrap().read(cx).id()
24517 })
24518 });
24519
24520 let assert_language_servers_count =
24521 |expected: usize, context: &str, cx: &mut VisualTestContext| {
24522 project.update(cx, |project, cx| {
24523 let current = project
24524 .lsp_store()
24525 .read(cx)
24526 .as_local()
24527 .unwrap()
24528 .language_servers
24529 .len();
24530 assert_eq!(expected, current, "{context}");
24531 });
24532 };
24533
24534 assert_language_servers_count(
24535 0,
24536 "No servers should be running before any file is open",
24537 cx,
24538 );
24539 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24540 let main_editor = workspace
24541 .update_in(cx, |workspace, window, cx| {
24542 workspace.open_path(
24543 (worktree_id, rel_path("main.rs")),
24544 Some(pane.downgrade()),
24545 true,
24546 window,
24547 cx,
24548 )
24549 })
24550 .unwrap()
24551 .await
24552 .downcast::<Editor>()
24553 .unwrap();
24554 pane.update(cx, |pane, cx| {
24555 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24556 open_editor.update(cx, |editor, cx| {
24557 assert_eq!(
24558 editor.display_text(cx),
24559 "fn main() {}",
24560 "Original main.rs text on initial open",
24561 );
24562 });
24563 assert_eq!(open_editor, main_editor);
24564 });
24565 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
24566
24567 let external_editor = workspace
24568 .update_in(cx, |workspace, window, cx| {
24569 workspace.open_abs_path(
24570 PathBuf::from("/root/foo/bar/external_file.rs"),
24571 OpenOptions::default(),
24572 window,
24573 cx,
24574 )
24575 })
24576 .await
24577 .expect("opening external file")
24578 .downcast::<Editor>()
24579 .expect("downcasted external file's open element to editor");
24580 pane.update(cx, |pane, cx| {
24581 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24582 open_editor.update(cx, |editor, cx| {
24583 assert_eq!(
24584 editor.display_text(cx),
24585 "pub mod external {}",
24586 "External file is open now",
24587 );
24588 });
24589 assert_eq!(open_editor, external_editor);
24590 });
24591 assert_language_servers_count(
24592 1,
24593 "Second, external, *.rs file should join the existing server",
24594 cx,
24595 );
24596
24597 pane.update_in(cx, |pane, window, cx| {
24598 pane.close_active_item(&CloseActiveItem::default(), window, cx)
24599 })
24600 .await
24601 .unwrap();
24602 pane.update_in(cx, |pane, window, cx| {
24603 pane.navigate_backward(&Default::default(), window, cx);
24604 });
24605 cx.run_until_parked();
24606 pane.update(cx, |pane, cx| {
24607 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24608 open_editor.update(cx, |editor, cx| {
24609 assert_eq!(
24610 editor.display_text(cx),
24611 "pub mod external {}",
24612 "External file is open now",
24613 );
24614 });
24615 });
24616 assert_language_servers_count(
24617 1,
24618 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
24619 cx,
24620 );
24621
24622 cx.update(|_, cx| {
24623 workspace::reload(cx);
24624 });
24625 assert_language_servers_count(
24626 1,
24627 "After reloading the worktree with local and external files opened, only one project should be started",
24628 cx,
24629 );
24630}
24631
24632#[gpui::test]
24633async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
24634 init_test(cx, |_| {});
24635
24636 let mut cx = EditorTestContext::new(cx).await;
24637 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24638 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24639
24640 // test cursor move to start of each line on tab
24641 // for `if`, `elif`, `else`, `while`, `with` and `for`
24642 cx.set_state(indoc! {"
24643 def main():
24644 ˇ for item in items:
24645 ˇ while item.active:
24646 ˇ if item.value > 10:
24647 ˇ continue
24648 ˇ elif item.value < 0:
24649 ˇ break
24650 ˇ else:
24651 ˇ with item.context() as ctx:
24652 ˇ yield count
24653 ˇ else:
24654 ˇ log('while else')
24655 ˇ else:
24656 ˇ log('for else')
24657 "});
24658 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24659 cx.assert_editor_state(indoc! {"
24660 def main():
24661 ˇfor item in items:
24662 ˇwhile item.active:
24663 ˇif item.value > 10:
24664 ˇcontinue
24665 ˇelif item.value < 0:
24666 ˇbreak
24667 ˇelse:
24668 ˇwith item.context() as ctx:
24669 ˇyield count
24670 ˇelse:
24671 ˇlog('while else')
24672 ˇelse:
24673 ˇlog('for else')
24674 "});
24675 // test relative indent is preserved when tab
24676 // for `if`, `elif`, `else`, `while`, `with` and `for`
24677 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24678 cx.assert_editor_state(indoc! {"
24679 def main():
24680 ˇfor item in items:
24681 ˇwhile item.active:
24682 ˇif item.value > 10:
24683 ˇcontinue
24684 ˇelif item.value < 0:
24685 ˇbreak
24686 ˇelse:
24687 ˇwith item.context() as ctx:
24688 ˇyield count
24689 ˇelse:
24690 ˇlog('while else')
24691 ˇelse:
24692 ˇlog('for else')
24693 "});
24694
24695 // test cursor move to start of each line on tab
24696 // for `try`, `except`, `else`, `finally`, `match` and `def`
24697 cx.set_state(indoc! {"
24698 def main():
24699 ˇ try:
24700 ˇ fetch()
24701 ˇ except ValueError:
24702 ˇ handle_error()
24703 ˇ else:
24704 ˇ match value:
24705 ˇ case _:
24706 ˇ finally:
24707 ˇ def status():
24708 ˇ return 0
24709 "});
24710 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24711 cx.assert_editor_state(indoc! {"
24712 def main():
24713 ˇtry:
24714 ˇfetch()
24715 ˇexcept ValueError:
24716 ˇhandle_error()
24717 ˇelse:
24718 ˇmatch value:
24719 ˇcase _:
24720 ˇfinally:
24721 ˇdef status():
24722 ˇreturn 0
24723 "});
24724 // test relative indent is preserved when tab
24725 // for `try`, `except`, `else`, `finally`, `match` and `def`
24726 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24727 cx.assert_editor_state(indoc! {"
24728 def main():
24729 ˇtry:
24730 ˇfetch()
24731 ˇexcept ValueError:
24732 ˇhandle_error()
24733 ˇelse:
24734 ˇmatch value:
24735 ˇcase _:
24736 ˇfinally:
24737 ˇdef status():
24738 ˇreturn 0
24739 "});
24740}
24741
24742#[gpui::test]
24743async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
24744 init_test(cx, |_| {});
24745
24746 let mut cx = EditorTestContext::new(cx).await;
24747 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24748 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24749
24750 // test `else` auto outdents when typed inside `if` block
24751 cx.set_state(indoc! {"
24752 def main():
24753 if i == 2:
24754 return
24755 ˇ
24756 "});
24757 cx.update_editor(|editor, window, cx| {
24758 editor.handle_input("else:", window, cx);
24759 });
24760 cx.assert_editor_state(indoc! {"
24761 def main():
24762 if i == 2:
24763 return
24764 else:ˇ
24765 "});
24766
24767 // test `except` auto outdents when typed inside `try` block
24768 cx.set_state(indoc! {"
24769 def main():
24770 try:
24771 i = 2
24772 ˇ
24773 "});
24774 cx.update_editor(|editor, window, cx| {
24775 editor.handle_input("except:", window, cx);
24776 });
24777 cx.assert_editor_state(indoc! {"
24778 def main():
24779 try:
24780 i = 2
24781 except:ˇ
24782 "});
24783
24784 // test `else` auto outdents when typed inside `except` block
24785 cx.set_state(indoc! {"
24786 def main():
24787 try:
24788 i = 2
24789 except:
24790 j = 2
24791 ˇ
24792 "});
24793 cx.update_editor(|editor, window, cx| {
24794 editor.handle_input("else:", window, cx);
24795 });
24796 cx.assert_editor_state(indoc! {"
24797 def main():
24798 try:
24799 i = 2
24800 except:
24801 j = 2
24802 else:ˇ
24803 "});
24804
24805 // test `finally` auto outdents when typed inside `else` block
24806 cx.set_state(indoc! {"
24807 def main():
24808 try:
24809 i = 2
24810 except:
24811 j = 2
24812 else:
24813 k = 2
24814 ˇ
24815 "});
24816 cx.update_editor(|editor, window, cx| {
24817 editor.handle_input("finally:", window, cx);
24818 });
24819 cx.assert_editor_state(indoc! {"
24820 def main():
24821 try:
24822 i = 2
24823 except:
24824 j = 2
24825 else:
24826 k = 2
24827 finally:ˇ
24828 "});
24829
24830 // test `else` does not outdents when typed inside `except` block right after for block
24831 cx.set_state(indoc! {"
24832 def main():
24833 try:
24834 i = 2
24835 except:
24836 for i in range(n):
24837 pass
24838 ˇ
24839 "});
24840 cx.update_editor(|editor, window, cx| {
24841 editor.handle_input("else:", window, cx);
24842 });
24843 cx.assert_editor_state(indoc! {"
24844 def main():
24845 try:
24846 i = 2
24847 except:
24848 for i in range(n):
24849 pass
24850 else:ˇ
24851 "});
24852
24853 // test `finally` auto outdents when typed inside `else` block right after for block
24854 cx.set_state(indoc! {"
24855 def main():
24856 try:
24857 i = 2
24858 except:
24859 j = 2
24860 else:
24861 for i in range(n):
24862 pass
24863 ˇ
24864 "});
24865 cx.update_editor(|editor, window, cx| {
24866 editor.handle_input("finally:", window, cx);
24867 });
24868 cx.assert_editor_state(indoc! {"
24869 def main():
24870 try:
24871 i = 2
24872 except:
24873 j = 2
24874 else:
24875 for i in range(n):
24876 pass
24877 finally:ˇ
24878 "});
24879
24880 // test `except` outdents to inner "try" block
24881 cx.set_state(indoc! {"
24882 def main():
24883 try:
24884 i = 2
24885 if i == 2:
24886 try:
24887 i = 3
24888 ˇ
24889 "});
24890 cx.update_editor(|editor, window, cx| {
24891 editor.handle_input("except:", window, cx);
24892 });
24893 cx.assert_editor_state(indoc! {"
24894 def main():
24895 try:
24896 i = 2
24897 if i == 2:
24898 try:
24899 i = 3
24900 except:ˇ
24901 "});
24902
24903 // test `except` outdents to outer "try" block
24904 cx.set_state(indoc! {"
24905 def main():
24906 try:
24907 i = 2
24908 if i == 2:
24909 try:
24910 i = 3
24911 ˇ
24912 "});
24913 cx.update_editor(|editor, window, cx| {
24914 editor.handle_input("except:", window, cx);
24915 });
24916 cx.assert_editor_state(indoc! {"
24917 def main():
24918 try:
24919 i = 2
24920 if i == 2:
24921 try:
24922 i = 3
24923 except:ˇ
24924 "});
24925
24926 // test `else` stays at correct indent when typed after `for` block
24927 cx.set_state(indoc! {"
24928 def main():
24929 for i in range(10):
24930 if i == 3:
24931 break
24932 ˇ
24933 "});
24934 cx.update_editor(|editor, window, cx| {
24935 editor.handle_input("else:", window, cx);
24936 });
24937 cx.assert_editor_state(indoc! {"
24938 def main():
24939 for i in range(10):
24940 if i == 3:
24941 break
24942 else:ˇ
24943 "});
24944
24945 // test does not outdent on typing after line with square brackets
24946 cx.set_state(indoc! {"
24947 def f() -> list[str]:
24948 ˇ
24949 "});
24950 cx.update_editor(|editor, window, cx| {
24951 editor.handle_input("a", window, cx);
24952 });
24953 cx.assert_editor_state(indoc! {"
24954 def f() -> list[str]:
24955 aˇ
24956 "});
24957
24958 // test does not outdent on typing : after case keyword
24959 cx.set_state(indoc! {"
24960 match 1:
24961 caseˇ
24962 "});
24963 cx.update_editor(|editor, window, cx| {
24964 editor.handle_input(":", window, cx);
24965 });
24966 cx.assert_editor_state(indoc! {"
24967 match 1:
24968 case:ˇ
24969 "});
24970}
24971
24972#[gpui::test]
24973async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
24974 init_test(cx, |_| {});
24975 update_test_language_settings(cx, |settings| {
24976 settings.defaults.extend_comment_on_newline = Some(false);
24977 });
24978 let mut cx = EditorTestContext::new(cx).await;
24979 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24980 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24981
24982 // test correct indent after newline on comment
24983 cx.set_state(indoc! {"
24984 # COMMENT:ˇ
24985 "});
24986 cx.update_editor(|editor, window, cx| {
24987 editor.newline(&Newline, window, cx);
24988 });
24989 cx.assert_editor_state(indoc! {"
24990 # COMMENT:
24991 ˇ
24992 "});
24993
24994 // test correct indent after newline in brackets
24995 cx.set_state(indoc! {"
24996 {ˇ}
24997 "});
24998 cx.update_editor(|editor, window, cx| {
24999 editor.newline(&Newline, window, cx);
25000 });
25001 cx.run_until_parked();
25002 cx.assert_editor_state(indoc! {"
25003 {
25004 ˇ
25005 }
25006 "});
25007
25008 cx.set_state(indoc! {"
25009 (ˇ)
25010 "});
25011 cx.update_editor(|editor, window, cx| {
25012 editor.newline(&Newline, window, cx);
25013 });
25014 cx.run_until_parked();
25015 cx.assert_editor_state(indoc! {"
25016 (
25017 ˇ
25018 )
25019 "});
25020
25021 // do not indent after empty lists or dictionaries
25022 cx.set_state(indoc! {"
25023 a = []ˇ
25024 "});
25025 cx.update_editor(|editor, window, cx| {
25026 editor.newline(&Newline, window, cx);
25027 });
25028 cx.run_until_parked();
25029 cx.assert_editor_state(indoc! {"
25030 a = []
25031 ˇ
25032 "});
25033}
25034
25035#[gpui::test]
25036async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
25037 init_test(cx, |_| {});
25038
25039 let mut cx = EditorTestContext::new(cx).await;
25040 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25041 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25042
25043 // test cursor move to start of each line on tab
25044 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
25045 cx.set_state(indoc! {"
25046 function main() {
25047 ˇ for item in $items; do
25048 ˇ while [ -n \"$item\" ]; do
25049 ˇ if [ \"$value\" -gt 10 ]; then
25050 ˇ continue
25051 ˇ elif [ \"$value\" -lt 0 ]; then
25052 ˇ break
25053 ˇ else
25054 ˇ echo \"$item\"
25055 ˇ fi
25056 ˇ done
25057 ˇ done
25058 ˇ}
25059 "});
25060 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25061 cx.assert_editor_state(indoc! {"
25062 function main() {
25063 ˇfor item in $items; do
25064 ˇwhile [ -n \"$item\" ]; do
25065 ˇif [ \"$value\" -gt 10 ]; then
25066 ˇcontinue
25067 ˇelif [ \"$value\" -lt 0 ]; then
25068 ˇbreak
25069 ˇelse
25070 ˇecho \"$item\"
25071 ˇfi
25072 ˇdone
25073 ˇdone
25074 ˇ}
25075 "});
25076 // test relative indent is preserved when tab
25077 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25078 cx.assert_editor_state(indoc! {"
25079 function main() {
25080 ˇfor item in $items; do
25081 ˇwhile [ -n \"$item\" ]; do
25082 ˇif [ \"$value\" -gt 10 ]; then
25083 ˇcontinue
25084 ˇelif [ \"$value\" -lt 0 ]; then
25085 ˇbreak
25086 ˇelse
25087 ˇecho \"$item\"
25088 ˇfi
25089 ˇdone
25090 ˇdone
25091 ˇ}
25092 "});
25093
25094 // test cursor move to start of each line on tab
25095 // for `case` statement with patterns
25096 cx.set_state(indoc! {"
25097 function handle() {
25098 ˇ case \"$1\" in
25099 ˇ start)
25100 ˇ echo \"a\"
25101 ˇ ;;
25102 ˇ stop)
25103 ˇ echo \"b\"
25104 ˇ ;;
25105 ˇ *)
25106 ˇ echo \"c\"
25107 ˇ ;;
25108 ˇ esac
25109 ˇ}
25110 "});
25111 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25112 cx.assert_editor_state(indoc! {"
25113 function handle() {
25114 ˇcase \"$1\" in
25115 ˇstart)
25116 ˇecho \"a\"
25117 ˇ;;
25118 ˇstop)
25119 ˇecho \"b\"
25120 ˇ;;
25121 ˇ*)
25122 ˇecho \"c\"
25123 ˇ;;
25124 ˇesac
25125 ˇ}
25126 "});
25127}
25128
25129#[gpui::test]
25130async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
25131 init_test(cx, |_| {});
25132
25133 let mut cx = EditorTestContext::new(cx).await;
25134 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25135 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25136
25137 // test indents on comment insert
25138 cx.set_state(indoc! {"
25139 function main() {
25140 ˇ for item in $items; do
25141 ˇ while [ -n \"$item\" ]; do
25142 ˇ if [ \"$value\" -gt 10 ]; then
25143 ˇ continue
25144 ˇ elif [ \"$value\" -lt 0 ]; then
25145 ˇ break
25146 ˇ else
25147 ˇ echo \"$item\"
25148 ˇ fi
25149 ˇ done
25150 ˇ done
25151 ˇ}
25152 "});
25153 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
25154 cx.assert_editor_state(indoc! {"
25155 function main() {
25156 #ˇ for item in $items; do
25157 #ˇ while [ -n \"$item\" ]; do
25158 #ˇ if [ \"$value\" -gt 10 ]; then
25159 #ˇ continue
25160 #ˇ elif [ \"$value\" -lt 0 ]; then
25161 #ˇ break
25162 #ˇ else
25163 #ˇ echo \"$item\"
25164 #ˇ fi
25165 #ˇ done
25166 #ˇ done
25167 #ˇ}
25168 "});
25169}
25170
25171#[gpui::test]
25172async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
25173 init_test(cx, |_| {});
25174
25175 let mut cx = EditorTestContext::new(cx).await;
25176 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25177 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25178
25179 // test `else` auto outdents when typed inside `if` block
25180 cx.set_state(indoc! {"
25181 if [ \"$1\" = \"test\" ]; then
25182 echo \"foo bar\"
25183 ˇ
25184 "});
25185 cx.update_editor(|editor, window, cx| {
25186 editor.handle_input("else", window, cx);
25187 });
25188 cx.assert_editor_state(indoc! {"
25189 if [ \"$1\" = \"test\" ]; then
25190 echo \"foo bar\"
25191 elseˇ
25192 "});
25193
25194 // test `elif` auto outdents when typed inside `if` block
25195 cx.set_state(indoc! {"
25196 if [ \"$1\" = \"test\" ]; then
25197 echo \"foo bar\"
25198 ˇ
25199 "});
25200 cx.update_editor(|editor, window, cx| {
25201 editor.handle_input("elif", window, cx);
25202 });
25203 cx.assert_editor_state(indoc! {"
25204 if [ \"$1\" = \"test\" ]; then
25205 echo \"foo bar\"
25206 elifˇ
25207 "});
25208
25209 // test `fi` auto outdents when typed inside `else` block
25210 cx.set_state(indoc! {"
25211 if [ \"$1\" = \"test\" ]; then
25212 echo \"foo bar\"
25213 else
25214 echo \"bar baz\"
25215 ˇ
25216 "});
25217 cx.update_editor(|editor, window, cx| {
25218 editor.handle_input("fi", window, cx);
25219 });
25220 cx.assert_editor_state(indoc! {"
25221 if [ \"$1\" = \"test\" ]; then
25222 echo \"foo bar\"
25223 else
25224 echo \"bar baz\"
25225 fiˇ
25226 "});
25227
25228 // test `done` auto outdents when typed inside `while` block
25229 cx.set_state(indoc! {"
25230 while read line; do
25231 echo \"$line\"
25232 ˇ
25233 "});
25234 cx.update_editor(|editor, window, cx| {
25235 editor.handle_input("done", window, cx);
25236 });
25237 cx.assert_editor_state(indoc! {"
25238 while read line; do
25239 echo \"$line\"
25240 doneˇ
25241 "});
25242
25243 // test `done` auto outdents when typed inside `for` block
25244 cx.set_state(indoc! {"
25245 for file in *.txt; do
25246 cat \"$file\"
25247 ˇ
25248 "});
25249 cx.update_editor(|editor, window, cx| {
25250 editor.handle_input("done", window, cx);
25251 });
25252 cx.assert_editor_state(indoc! {"
25253 for file in *.txt; do
25254 cat \"$file\"
25255 doneˇ
25256 "});
25257
25258 // test `esac` auto outdents when typed inside `case` block
25259 cx.set_state(indoc! {"
25260 case \"$1\" in
25261 start)
25262 echo \"foo bar\"
25263 ;;
25264 stop)
25265 echo \"bar baz\"
25266 ;;
25267 ˇ
25268 "});
25269 cx.update_editor(|editor, window, cx| {
25270 editor.handle_input("esac", window, cx);
25271 });
25272 cx.assert_editor_state(indoc! {"
25273 case \"$1\" in
25274 start)
25275 echo \"foo bar\"
25276 ;;
25277 stop)
25278 echo \"bar baz\"
25279 ;;
25280 esacˇ
25281 "});
25282
25283 // test `*)` auto outdents when typed inside `case` block
25284 cx.set_state(indoc! {"
25285 case \"$1\" in
25286 start)
25287 echo \"foo bar\"
25288 ;;
25289 ˇ
25290 "});
25291 cx.update_editor(|editor, window, cx| {
25292 editor.handle_input("*)", window, cx);
25293 });
25294 cx.assert_editor_state(indoc! {"
25295 case \"$1\" in
25296 start)
25297 echo \"foo bar\"
25298 ;;
25299 *)ˇ
25300 "});
25301
25302 // test `fi` outdents to correct level with nested if blocks
25303 cx.set_state(indoc! {"
25304 if [ \"$1\" = \"test\" ]; then
25305 echo \"outer if\"
25306 if [ \"$2\" = \"debug\" ]; then
25307 echo \"inner if\"
25308 ˇ
25309 "});
25310 cx.update_editor(|editor, window, cx| {
25311 editor.handle_input("fi", window, cx);
25312 });
25313 cx.assert_editor_state(indoc! {"
25314 if [ \"$1\" = \"test\" ]; then
25315 echo \"outer if\"
25316 if [ \"$2\" = \"debug\" ]; then
25317 echo \"inner if\"
25318 fiˇ
25319 "});
25320}
25321
25322#[gpui::test]
25323async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
25324 init_test(cx, |_| {});
25325 update_test_language_settings(cx, |settings| {
25326 settings.defaults.extend_comment_on_newline = Some(false);
25327 });
25328 let mut cx = EditorTestContext::new(cx).await;
25329 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25330 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25331
25332 // test correct indent after newline on comment
25333 cx.set_state(indoc! {"
25334 # COMMENT:ˇ
25335 "});
25336 cx.update_editor(|editor, window, cx| {
25337 editor.newline(&Newline, window, cx);
25338 });
25339 cx.assert_editor_state(indoc! {"
25340 # COMMENT:
25341 ˇ
25342 "});
25343
25344 // test correct indent after newline after `then`
25345 cx.set_state(indoc! {"
25346
25347 if [ \"$1\" = \"test\" ]; thenˇ
25348 "});
25349 cx.update_editor(|editor, window, cx| {
25350 editor.newline(&Newline, window, cx);
25351 });
25352 cx.run_until_parked();
25353 cx.assert_editor_state(indoc! {"
25354
25355 if [ \"$1\" = \"test\" ]; then
25356 ˇ
25357 "});
25358
25359 // test correct indent after newline after `else`
25360 cx.set_state(indoc! {"
25361 if [ \"$1\" = \"test\" ]; then
25362 elseˇ
25363 "});
25364 cx.update_editor(|editor, window, cx| {
25365 editor.newline(&Newline, window, cx);
25366 });
25367 cx.run_until_parked();
25368 cx.assert_editor_state(indoc! {"
25369 if [ \"$1\" = \"test\" ]; then
25370 else
25371 ˇ
25372 "});
25373
25374 // test correct indent after newline after `elif`
25375 cx.set_state(indoc! {"
25376 if [ \"$1\" = \"test\" ]; then
25377 elifˇ
25378 "});
25379 cx.update_editor(|editor, window, cx| {
25380 editor.newline(&Newline, window, cx);
25381 });
25382 cx.run_until_parked();
25383 cx.assert_editor_state(indoc! {"
25384 if [ \"$1\" = \"test\" ]; then
25385 elif
25386 ˇ
25387 "});
25388
25389 // test correct indent after newline after `do`
25390 cx.set_state(indoc! {"
25391 for file in *.txt; doˇ
25392 "});
25393 cx.update_editor(|editor, window, cx| {
25394 editor.newline(&Newline, window, cx);
25395 });
25396 cx.run_until_parked();
25397 cx.assert_editor_state(indoc! {"
25398 for file in *.txt; do
25399 ˇ
25400 "});
25401
25402 // test correct indent after newline after case pattern
25403 cx.set_state(indoc! {"
25404 case \"$1\" in
25405 start)ˇ
25406 "});
25407 cx.update_editor(|editor, window, cx| {
25408 editor.newline(&Newline, window, cx);
25409 });
25410 cx.run_until_parked();
25411 cx.assert_editor_state(indoc! {"
25412 case \"$1\" in
25413 start)
25414 ˇ
25415 "});
25416
25417 // test correct indent after newline after case pattern
25418 cx.set_state(indoc! {"
25419 case \"$1\" in
25420 start)
25421 ;;
25422 *)ˇ
25423 "});
25424 cx.update_editor(|editor, window, cx| {
25425 editor.newline(&Newline, window, cx);
25426 });
25427 cx.run_until_parked();
25428 cx.assert_editor_state(indoc! {"
25429 case \"$1\" in
25430 start)
25431 ;;
25432 *)
25433 ˇ
25434 "});
25435
25436 // test correct indent after newline after function opening brace
25437 cx.set_state(indoc! {"
25438 function test() {ˇ}
25439 "});
25440 cx.update_editor(|editor, window, cx| {
25441 editor.newline(&Newline, window, cx);
25442 });
25443 cx.run_until_parked();
25444 cx.assert_editor_state(indoc! {"
25445 function test() {
25446 ˇ
25447 }
25448 "});
25449
25450 // test no extra indent after semicolon on same line
25451 cx.set_state(indoc! {"
25452 echo \"test\";ˇ
25453 "});
25454 cx.update_editor(|editor, window, cx| {
25455 editor.newline(&Newline, window, cx);
25456 });
25457 cx.run_until_parked();
25458 cx.assert_editor_state(indoc! {"
25459 echo \"test\";
25460 ˇ
25461 "});
25462}
25463
25464fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
25465 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
25466 point..point
25467}
25468
25469#[track_caller]
25470fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
25471 let (text, ranges) = marked_text_ranges(marked_text, true);
25472 assert_eq!(editor.text(cx), text);
25473 assert_eq!(
25474 editor.selections.ranges(&editor.display_snapshot(cx)),
25475 ranges,
25476 "Assert selections are {}",
25477 marked_text
25478 );
25479}
25480
25481pub fn handle_signature_help_request(
25482 cx: &mut EditorLspTestContext,
25483 mocked_response: lsp::SignatureHelp,
25484) -> impl Future<Output = ()> + use<> {
25485 let mut request =
25486 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
25487 let mocked_response = mocked_response.clone();
25488 async move { Ok(Some(mocked_response)) }
25489 });
25490
25491 async move {
25492 request.next().await;
25493 }
25494}
25495
25496#[track_caller]
25497pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
25498 cx.update_editor(|editor, _, _| {
25499 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
25500 let entries = menu.entries.borrow();
25501 let entries = entries
25502 .iter()
25503 .map(|entry| entry.string.as_str())
25504 .collect::<Vec<_>>();
25505 assert_eq!(entries, expected);
25506 } else {
25507 panic!("Expected completions menu");
25508 }
25509 });
25510}
25511
25512/// Handle completion request passing a marked string specifying where the completion
25513/// should be triggered from using '|' character, what range should be replaced, and what completions
25514/// should be returned using '<' and '>' to delimit the range.
25515///
25516/// Also see `handle_completion_request_with_insert_and_replace`.
25517#[track_caller]
25518pub fn handle_completion_request(
25519 marked_string: &str,
25520 completions: Vec<&'static str>,
25521 is_incomplete: bool,
25522 counter: Arc<AtomicUsize>,
25523 cx: &mut EditorLspTestContext,
25524) -> impl Future<Output = ()> {
25525 let complete_from_marker: TextRangeMarker = '|'.into();
25526 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25527 let (_, mut marked_ranges) = marked_text_ranges_by(
25528 marked_string,
25529 vec![complete_from_marker.clone(), replace_range_marker.clone()],
25530 );
25531
25532 let complete_from_position =
25533 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25534 let replace_range =
25535 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25536
25537 let mut request =
25538 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25539 let completions = completions.clone();
25540 counter.fetch_add(1, atomic::Ordering::Release);
25541 async move {
25542 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25543 assert_eq!(
25544 params.text_document_position.position,
25545 complete_from_position
25546 );
25547 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
25548 is_incomplete,
25549 item_defaults: None,
25550 items: completions
25551 .iter()
25552 .map(|completion_text| lsp::CompletionItem {
25553 label: completion_text.to_string(),
25554 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
25555 range: replace_range,
25556 new_text: completion_text.to_string(),
25557 })),
25558 ..Default::default()
25559 })
25560 .collect(),
25561 })))
25562 }
25563 });
25564
25565 async move {
25566 request.next().await;
25567 }
25568}
25569
25570/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
25571/// given instead, which also contains an `insert` range.
25572///
25573/// This function uses markers to define ranges:
25574/// - `|` marks the cursor position
25575/// - `<>` marks the replace range
25576/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
25577pub fn handle_completion_request_with_insert_and_replace(
25578 cx: &mut EditorLspTestContext,
25579 marked_string: &str,
25580 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
25581 counter: Arc<AtomicUsize>,
25582) -> impl Future<Output = ()> {
25583 let complete_from_marker: TextRangeMarker = '|'.into();
25584 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25585 let insert_range_marker: TextRangeMarker = ('{', '}').into();
25586
25587 let (_, mut marked_ranges) = marked_text_ranges_by(
25588 marked_string,
25589 vec![
25590 complete_from_marker.clone(),
25591 replace_range_marker.clone(),
25592 insert_range_marker.clone(),
25593 ],
25594 );
25595
25596 let complete_from_position =
25597 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25598 let replace_range =
25599 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25600
25601 let insert_range = match marked_ranges.remove(&insert_range_marker) {
25602 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
25603 _ => lsp::Range {
25604 start: replace_range.start,
25605 end: complete_from_position,
25606 },
25607 };
25608
25609 let mut request =
25610 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25611 let completions = completions.clone();
25612 counter.fetch_add(1, atomic::Ordering::Release);
25613 async move {
25614 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25615 assert_eq!(
25616 params.text_document_position.position, complete_from_position,
25617 "marker `|` position doesn't match",
25618 );
25619 Ok(Some(lsp::CompletionResponse::Array(
25620 completions
25621 .iter()
25622 .map(|(label, new_text)| lsp::CompletionItem {
25623 label: label.to_string(),
25624 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
25625 lsp::InsertReplaceEdit {
25626 insert: insert_range,
25627 replace: replace_range,
25628 new_text: new_text.to_string(),
25629 },
25630 )),
25631 ..Default::default()
25632 })
25633 .collect(),
25634 )))
25635 }
25636 });
25637
25638 async move {
25639 request.next().await;
25640 }
25641}
25642
25643fn handle_resolve_completion_request(
25644 cx: &mut EditorLspTestContext,
25645 edits: Option<Vec<(&'static str, &'static str)>>,
25646) -> impl Future<Output = ()> {
25647 let edits = edits.map(|edits| {
25648 edits
25649 .iter()
25650 .map(|(marked_string, new_text)| {
25651 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
25652 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
25653 lsp::TextEdit::new(replace_range, new_text.to_string())
25654 })
25655 .collect::<Vec<_>>()
25656 });
25657
25658 let mut request =
25659 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
25660 let edits = edits.clone();
25661 async move {
25662 Ok(lsp::CompletionItem {
25663 additional_text_edits: edits,
25664 ..Default::default()
25665 })
25666 }
25667 });
25668
25669 async move {
25670 request.next().await;
25671 }
25672}
25673
25674pub(crate) fn update_test_language_settings(
25675 cx: &mut TestAppContext,
25676 f: impl Fn(&mut AllLanguageSettingsContent),
25677) {
25678 cx.update(|cx| {
25679 SettingsStore::update_global(cx, |store, cx| {
25680 store.update_user_settings(cx, |settings| f(&mut settings.project.all_languages));
25681 });
25682 });
25683}
25684
25685pub(crate) fn update_test_project_settings(
25686 cx: &mut TestAppContext,
25687 f: impl Fn(&mut ProjectSettingsContent),
25688) {
25689 cx.update(|cx| {
25690 SettingsStore::update_global(cx, |store, cx| {
25691 store.update_user_settings(cx, |settings| f(&mut settings.project));
25692 });
25693 });
25694}
25695
25696pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
25697 cx.update(|cx| {
25698 assets::Assets.load_test_fonts(cx);
25699 let store = SettingsStore::test(cx);
25700 cx.set_global(store);
25701 theme::init(theme::LoadThemes::JustBase, cx);
25702 release_channel::init(SemanticVersion::default(), cx);
25703 crate::init(cx);
25704 });
25705 zlog::init_test();
25706 update_test_language_settings(cx, f);
25707}
25708
25709#[track_caller]
25710fn assert_hunk_revert(
25711 not_reverted_text_with_selections: &str,
25712 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
25713 expected_reverted_text_with_selections: &str,
25714 base_text: &str,
25715 cx: &mut EditorLspTestContext,
25716) {
25717 cx.set_state(not_reverted_text_with_selections);
25718 cx.set_head_text(base_text);
25719 cx.executor().run_until_parked();
25720
25721 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
25722 let snapshot = editor.snapshot(window, cx);
25723 let reverted_hunk_statuses = snapshot
25724 .buffer_snapshot()
25725 .diff_hunks_in_range(0..snapshot.buffer_snapshot().len())
25726 .map(|hunk| hunk.status().kind)
25727 .collect::<Vec<_>>();
25728
25729 editor.git_restore(&Default::default(), window, cx);
25730 reverted_hunk_statuses
25731 });
25732 cx.executor().run_until_parked();
25733 cx.assert_editor_state(expected_reverted_text_with_selections);
25734 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
25735}
25736
25737#[gpui::test(iterations = 10)]
25738async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
25739 init_test(cx, |_| {});
25740
25741 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
25742 let counter = diagnostic_requests.clone();
25743
25744 let fs = FakeFs::new(cx.executor());
25745 fs.insert_tree(
25746 path!("/a"),
25747 json!({
25748 "first.rs": "fn main() { let a = 5; }",
25749 "second.rs": "// Test file",
25750 }),
25751 )
25752 .await;
25753
25754 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25755 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25756 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25757
25758 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25759 language_registry.add(rust_lang());
25760 let mut fake_servers = language_registry.register_fake_lsp(
25761 "Rust",
25762 FakeLspAdapter {
25763 capabilities: lsp::ServerCapabilities {
25764 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
25765 lsp::DiagnosticOptions {
25766 identifier: None,
25767 inter_file_dependencies: true,
25768 workspace_diagnostics: true,
25769 work_done_progress_options: Default::default(),
25770 },
25771 )),
25772 ..Default::default()
25773 },
25774 ..Default::default()
25775 },
25776 );
25777
25778 let editor = workspace
25779 .update(cx, |workspace, window, cx| {
25780 workspace.open_abs_path(
25781 PathBuf::from(path!("/a/first.rs")),
25782 OpenOptions::default(),
25783 window,
25784 cx,
25785 )
25786 })
25787 .unwrap()
25788 .await
25789 .unwrap()
25790 .downcast::<Editor>()
25791 .unwrap();
25792 let fake_server = fake_servers.next().await.unwrap();
25793 let server_id = fake_server.server.server_id();
25794 let mut first_request = fake_server
25795 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
25796 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
25797 let result_id = Some(new_result_id.to_string());
25798 assert_eq!(
25799 params.text_document.uri,
25800 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25801 );
25802 async move {
25803 Ok(lsp::DocumentDiagnosticReportResult::Report(
25804 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
25805 related_documents: None,
25806 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
25807 items: Vec::new(),
25808 result_id,
25809 },
25810 }),
25811 ))
25812 }
25813 });
25814
25815 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
25816 project.update(cx, |project, cx| {
25817 let buffer_id = editor
25818 .read(cx)
25819 .buffer()
25820 .read(cx)
25821 .as_singleton()
25822 .expect("created a singleton buffer")
25823 .read(cx)
25824 .remote_id();
25825 let buffer_result_id = project
25826 .lsp_store()
25827 .read(cx)
25828 .result_id(server_id, buffer_id, cx);
25829 assert_eq!(expected, buffer_result_id);
25830 });
25831 };
25832
25833 ensure_result_id(None, cx);
25834 cx.executor().advance_clock(Duration::from_millis(60));
25835 cx.executor().run_until_parked();
25836 assert_eq!(
25837 diagnostic_requests.load(atomic::Ordering::Acquire),
25838 1,
25839 "Opening file should trigger diagnostic request"
25840 );
25841 first_request
25842 .next()
25843 .await
25844 .expect("should have sent the first diagnostics pull request");
25845 ensure_result_id(Some("1".to_string()), cx);
25846
25847 // Editing should trigger diagnostics
25848 editor.update_in(cx, |editor, window, cx| {
25849 editor.handle_input("2", window, cx)
25850 });
25851 cx.executor().advance_clock(Duration::from_millis(60));
25852 cx.executor().run_until_parked();
25853 assert_eq!(
25854 diagnostic_requests.load(atomic::Ordering::Acquire),
25855 2,
25856 "Editing should trigger diagnostic request"
25857 );
25858 ensure_result_id(Some("2".to_string()), cx);
25859
25860 // Moving cursor should not trigger diagnostic request
25861 editor.update_in(cx, |editor, window, cx| {
25862 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25863 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
25864 });
25865 });
25866 cx.executor().advance_clock(Duration::from_millis(60));
25867 cx.executor().run_until_parked();
25868 assert_eq!(
25869 diagnostic_requests.load(atomic::Ordering::Acquire),
25870 2,
25871 "Cursor movement should not trigger diagnostic request"
25872 );
25873 ensure_result_id(Some("2".to_string()), cx);
25874 // Multiple rapid edits should be debounced
25875 for _ in 0..5 {
25876 editor.update_in(cx, |editor, window, cx| {
25877 editor.handle_input("x", window, cx)
25878 });
25879 }
25880 cx.executor().advance_clock(Duration::from_millis(60));
25881 cx.executor().run_until_parked();
25882
25883 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
25884 assert!(
25885 final_requests <= 4,
25886 "Multiple rapid edits should be debounced (got {final_requests} requests)",
25887 );
25888 ensure_result_id(Some(final_requests.to_string()), cx);
25889}
25890
25891#[gpui::test]
25892async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
25893 // Regression test for issue #11671
25894 // Previously, adding a cursor after moving multiple cursors would reset
25895 // the cursor count instead of adding to the existing cursors.
25896 init_test(cx, |_| {});
25897 let mut cx = EditorTestContext::new(cx).await;
25898
25899 // Create a simple buffer with cursor at start
25900 cx.set_state(indoc! {"
25901 ˇaaaa
25902 bbbb
25903 cccc
25904 dddd
25905 eeee
25906 ffff
25907 gggg
25908 hhhh"});
25909
25910 // Add 2 cursors below (so we have 3 total)
25911 cx.update_editor(|editor, window, cx| {
25912 editor.add_selection_below(&Default::default(), window, cx);
25913 editor.add_selection_below(&Default::default(), window, cx);
25914 });
25915
25916 // Verify we have 3 cursors
25917 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
25918 assert_eq!(
25919 initial_count, 3,
25920 "Should have 3 cursors after adding 2 below"
25921 );
25922
25923 // Move down one line
25924 cx.update_editor(|editor, window, cx| {
25925 editor.move_down(&MoveDown, window, cx);
25926 });
25927
25928 // Add another cursor below
25929 cx.update_editor(|editor, window, cx| {
25930 editor.add_selection_below(&Default::default(), window, cx);
25931 });
25932
25933 // Should now have 4 cursors (3 original + 1 new)
25934 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
25935 assert_eq!(
25936 final_count, 4,
25937 "Should have 4 cursors after moving and adding another"
25938 );
25939}
25940
25941#[gpui::test]
25942async fn test_add_selection_skip_soft_wrap_option(cx: &mut TestAppContext) {
25943 init_test(cx, |_| {});
25944
25945 let mut cx = EditorTestContext::new(cx).await;
25946
25947 cx.set_state(indoc!(
25948 r#"ˇThis is a very long line that will be wrapped when soft wrapping is enabled
25949 Second line here"#
25950 ));
25951
25952 cx.update_editor(|editor, window, cx| {
25953 // Enable soft wrapping with a narrow width to force soft wrapping and
25954 // confirm that more than 2 rows are being displayed.
25955 editor.set_wrap_width(Some(100.0.into()), cx);
25956 assert!(editor.display_text(cx).lines().count() > 2);
25957
25958 editor.add_selection_below(
25959 &AddSelectionBelow {
25960 skip_soft_wrap: true,
25961 },
25962 window,
25963 cx,
25964 );
25965
25966 assert_eq!(
25967 display_ranges(editor, cx),
25968 &[
25969 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
25970 DisplayPoint::new(DisplayRow(8), 0)..DisplayPoint::new(DisplayRow(8), 0),
25971 ]
25972 );
25973
25974 editor.add_selection_above(
25975 &AddSelectionAbove {
25976 skip_soft_wrap: true,
25977 },
25978 window,
25979 cx,
25980 );
25981
25982 assert_eq!(
25983 display_ranges(editor, cx),
25984 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
25985 );
25986
25987 editor.add_selection_below(
25988 &AddSelectionBelow {
25989 skip_soft_wrap: false,
25990 },
25991 window,
25992 cx,
25993 );
25994
25995 assert_eq!(
25996 display_ranges(editor, cx),
25997 &[
25998 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
25999 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
26000 ]
26001 );
26002
26003 editor.add_selection_above(
26004 &AddSelectionAbove {
26005 skip_soft_wrap: false,
26006 },
26007 window,
26008 cx,
26009 );
26010
26011 assert_eq!(
26012 display_ranges(editor, cx),
26013 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
26014 );
26015 });
26016}
26017
26018#[gpui::test(iterations = 10)]
26019async fn test_document_colors(cx: &mut TestAppContext) {
26020 let expected_color = Rgba {
26021 r: 0.33,
26022 g: 0.33,
26023 b: 0.33,
26024 a: 0.33,
26025 };
26026
26027 init_test(cx, |_| {});
26028
26029 let fs = FakeFs::new(cx.executor());
26030 fs.insert_tree(
26031 path!("/a"),
26032 json!({
26033 "first.rs": "fn main() { let a = 5; }",
26034 }),
26035 )
26036 .await;
26037
26038 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
26039 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
26040 let cx = &mut VisualTestContext::from_window(*workspace, cx);
26041
26042 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
26043 language_registry.add(rust_lang());
26044 let mut fake_servers = language_registry.register_fake_lsp(
26045 "Rust",
26046 FakeLspAdapter {
26047 capabilities: lsp::ServerCapabilities {
26048 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
26049 ..lsp::ServerCapabilities::default()
26050 },
26051 name: "rust-analyzer",
26052 ..FakeLspAdapter::default()
26053 },
26054 );
26055 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
26056 "Rust",
26057 FakeLspAdapter {
26058 capabilities: lsp::ServerCapabilities {
26059 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
26060 ..lsp::ServerCapabilities::default()
26061 },
26062 name: "not-rust-analyzer",
26063 ..FakeLspAdapter::default()
26064 },
26065 );
26066
26067 let editor = workspace
26068 .update(cx, |workspace, window, cx| {
26069 workspace.open_abs_path(
26070 PathBuf::from(path!("/a/first.rs")),
26071 OpenOptions::default(),
26072 window,
26073 cx,
26074 )
26075 })
26076 .unwrap()
26077 .await
26078 .unwrap()
26079 .downcast::<Editor>()
26080 .unwrap();
26081 let fake_language_server = fake_servers.next().await.unwrap();
26082 let fake_language_server_without_capabilities =
26083 fake_servers_without_capabilities.next().await.unwrap();
26084 let requests_made = Arc::new(AtomicUsize::new(0));
26085 let closure_requests_made = Arc::clone(&requests_made);
26086 let mut color_request_handle = fake_language_server
26087 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
26088 let requests_made = Arc::clone(&closure_requests_made);
26089 async move {
26090 assert_eq!(
26091 params.text_document.uri,
26092 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
26093 );
26094 requests_made.fetch_add(1, atomic::Ordering::Release);
26095 Ok(vec![
26096 lsp::ColorInformation {
26097 range: lsp::Range {
26098 start: lsp::Position {
26099 line: 0,
26100 character: 0,
26101 },
26102 end: lsp::Position {
26103 line: 0,
26104 character: 1,
26105 },
26106 },
26107 color: lsp::Color {
26108 red: 0.33,
26109 green: 0.33,
26110 blue: 0.33,
26111 alpha: 0.33,
26112 },
26113 },
26114 lsp::ColorInformation {
26115 range: lsp::Range {
26116 start: lsp::Position {
26117 line: 0,
26118 character: 0,
26119 },
26120 end: lsp::Position {
26121 line: 0,
26122 character: 1,
26123 },
26124 },
26125 color: lsp::Color {
26126 red: 0.33,
26127 green: 0.33,
26128 blue: 0.33,
26129 alpha: 0.33,
26130 },
26131 },
26132 ])
26133 }
26134 });
26135
26136 let _handle = fake_language_server_without_capabilities
26137 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
26138 panic!("Should not be called");
26139 });
26140 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26141 color_request_handle.next().await.unwrap();
26142 cx.run_until_parked();
26143 assert_eq!(
26144 1,
26145 requests_made.load(atomic::Ordering::Acquire),
26146 "Should query for colors once per editor open"
26147 );
26148 editor.update_in(cx, |editor, _, cx| {
26149 assert_eq!(
26150 vec![expected_color],
26151 extract_color_inlays(editor, cx),
26152 "Should have an initial inlay"
26153 );
26154 });
26155
26156 // opening another file in a split should not influence the LSP query counter
26157 workspace
26158 .update(cx, |workspace, window, cx| {
26159 assert_eq!(
26160 workspace.panes().len(),
26161 1,
26162 "Should have one pane with one editor"
26163 );
26164 workspace.move_item_to_pane_in_direction(
26165 &MoveItemToPaneInDirection {
26166 direction: SplitDirection::Right,
26167 focus: false,
26168 clone: true,
26169 },
26170 window,
26171 cx,
26172 );
26173 })
26174 .unwrap();
26175 cx.run_until_parked();
26176 workspace
26177 .update(cx, |workspace, _, cx| {
26178 let panes = workspace.panes();
26179 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
26180 for pane in panes {
26181 let editor = pane
26182 .read(cx)
26183 .active_item()
26184 .and_then(|item| item.downcast::<Editor>())
26185 .expect("Should have opened an editor in each split");
26186 let editor_file = editor
26187 .read(cx)
26188 .buffer()
26189 .read(cx)
26190 .as_singleton()
26191 .expect("test deals with singleton buffers")
26192 .read(cx)
26193 .file()
26194 .expect("test buffese should have a file")
26195 .path();
26196 assert_eq!(
26197 editor_file.as_ref(),
26198 rel_path("first.rs"),
26199 "Both editors should be opened for the same file"
26200 )
26201 }
26202 })
26203 .unwrap();
26204
26205 cx.executor().advance_clock(Duration::from_millis(500));
26206 let save = editor.update_in(cx, |editor, window, cx| {
26207 editor.move_to_end(&MoveToEnd, window, cx);
26208 editor.handle_input("dirty", window, cx);
26209 editor.save(
26210 SaveOptions {
26211 format: true,
26212 autosave: true,
26213 },
26214 project.clone(),
26215 window,
26216 cx,
26217 )
26218 });
26219 save.await.unwrap();
26220
26221 color_request_handle.next().await.unwrap();
26222 cx.run_until_parked();
26223 assert_eq!(
26224 2,
26225 requests_made.load(atomic::Ordering::Acquire),
26226 "Should query for colors once per save (deduplicated) and once per formatting after save"
26227 );
26228
26229 drop(editor);
26230 let close = workspace
26231 .update(cx, |workspace, window, cx| {
26232 workspace.active_pane().update(cx, |pane, cx| {
26233 pane.close_active_item(&CloseActiveItem::default(), window, cx)
26234 })
26235 })
26236 .unwrap();
26237 close.await.unwrap();
26238 let close = workspace
26239 .update(cx, |workspace, window, cx| {
26240 workspace.active_pane().update(cx, |pane, cx| {
26241 pane.close_active_item(&CloseActiveItem::default(), window, cx)
26242 })
26243 })
26244 .unwrap();
26245 close.await.unwrap();
26246 assert_eq!(
26247 2,
26248 requests_made.load(atomic::Ordering::Acquire),
26249 "After saving and closing all editors, no extra requests should be made"
26250 );
26251 workspace
26252 .update(cx, |workspace, _, cx| {
26253 assert!(
26254 workspace.active_item(cx).is_none(),
26255 "Should close all editors"
26256 )
26257 })
26258 .unwrap();
26259
26260 workspace
26261 .update(cx, |workspace, window, cx| {
26262 workspace.active_pane().update(cx, |pane, cx| {
26263 pane.navigate_backward(&workspace::GoBack, window, cx);
26264 })
26265 })
26266 .unwrap();
26267 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26268 cx.run_until_parked();
26269 let editor = workspace
26270 .update(cx, |workspace, _, cx| {
26271 workspace
26272 .active_item(cx)
26273 .expect("Should have reopened the editor again after navigating back")
26274 .downcast::<Editor>()
26275 .expect("Should be an editor")
26276 })
26277 .unwrap();
26278
26279 assert_eq!(
26280 2,
26281 requests_made.load(atomic::Ordering::Acquire),
26282 "Cache should be reused on buffer close and reopen"
26283 );
26284 editor.update(cx, |editor, cx| {
26285 assert_eq!(
26286 vec![expected_color],
26287 extract_color_inlays(editor, cx),
26288 "Should have an initial inlay"
26289 );
26290 });
26291
26292 drop(color_request_handle);
26293 let closure_requests_made = Arc::clone(&requests_made);
26294 let mut empty_color_request_handle = fake_language_server
26295 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
26296 let requests_made = Arc::clone(&closure_requests_made);
26297 async move {
26298 assert_eq!(
26299 params.text_document.uri,
26300 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
26301 );
26302 requests_made.fetch_add(1, atomic::Ordering::Release);
26303 Ok(Vec::new())
26304 }
26305 });
26306 let save = editor.update_in(cx, |editor, window, cx| {
26307 editor.move_to_end(&MoveToEnd, window, cx);
26308 editor.handle_input("dirty_again", window, cx);
26309 editor.save(
26310 SaveOptions {
26311 format: false,
26312 autosave: true,
26313 },
26314 project.clone(),
26315 window,
26316 cx,
26317 )
26318 });
26319 save.await.unwrap();
26320
26321 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26322 empty_color_request_handle.next().await.unwrap();
26323 cx.run_until_parked();
26324 assert_eq!(
26325 3,
26326 requests_made.load(atomic::Ordering::Acquire),
26327 "Should query for colors once per save only, as formatting was not requested"
26328 );
26329 editor.update(cx, |editor, cx| {
26330 assert_eq!(
26331 Vec::<Rgba>::new(),
26332 extract_color_inlays(editor, cx),
26333 "Should clear all colors when the server returns an empty response"
26334 );
26335 });
26336}
26337
26338#[gpui::test]
26339async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
26340 init_test(cx, |_| {});
26341 let (editor, cx) = cx.add_window_view(Editor::single_line);
26342 editor.update_in(cx, |editor, window, cx| {
26343 editor.set_text("oops\n\nwow\n", window, cx)
26344 });
26345 cx.run_until_parked();
26346 editor.update(cx, |editor, cx| {
26347 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
26348 });
26349 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
26350 cx.run_until_parked();
26351 editor.update(cx, |editor, cx| {
26352 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
26353 });
26354}
26355
26356#[gpui::test]
26357async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
26358 init_test(cx, |_| {});
26359
26360 cx.update(|cx| {
26361 register_project_item::<Editor>(cx);
26362 });
26363
26364 let fs = FakeFs::new(cx.executor());
26365 fs.insert_tree("/root1", json!({})).await;
26366 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
26367 .await;
26368
26369 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
26370 let (workspace, cx) =
26371 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
26372
26373 let worktree_id = project.update(cx, |project, cx| {
26374 project.worktrees(cx).next().unwrap().read(cx).id()
26375 });
26376
26377 let handle = workspace
26378 .update_in(cx, |workspace, window, cx| {
26379 let project_path = (worktree_id, rel_path("one.pdf"));
26380 workspace.open_path(project_path, None, true, window, cx)
26381 })
26382 .await
26383 .unwrap();
26384
26385 assert_eq!(
26386 handle.to_any().entity_type(),
26387 TypeId::of::<InvalidItemView>()
26388 );
26389}
26390
26391#[gpui::test]
26392async fn test_select_next_prev_syntax_node(cx: &mut TestAppContext) {
26393 init_test(cx, |_| {});
26394
26395 let language = Arc::new(Language::new(
26396 LanguageConfig::default(),
26397 Some(tree_sitter_rust::LANGUAGE.into()),
26398 ));
26399
26400 // Test hierarchical sibling navigation
26401 let text = r#"
26402 fn outer() {
26403 if condition {
26404 let a = 1;
26405 }
26406 let b = 2;
26407 }
26408
26409 fn another() {
26410 let c = 3;
26411 }
26412 "#;
26413
26414 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
26415 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
26416 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
26417
26418 // Wait for parsing to complete
26419 editor
26420 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
26421 .await;
26422
26423 editor.update_in(cx, |editor, window, cx| {
26424 // Start by selecting "let a = 1;" inside the if block
26425 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26426 s.select_display_ranges([
26427 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 26)
26428 ]);
26429 });
26430
26431 let initial_selection = editor
26432 .selections
26433 .display_ranges(&editor.display_snapshot(cx));
26434 assert_eq!(initial_selection.len(), 1, "Should have one selection");
26435
26436 // Test select next sibling - should move up levels to find the next sibling
26437 // Since "let a = 1;" has no siblings in the if block, it should move up
26438 // to find "let b = 2;" which is a sibling of the if block
26439 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
26440 let next_selection = editor
26441 .selections
26442 .display_ranges(&editor.display_snapshot(cx));
26443
26444 // Should have a selection and it should be different from the initial
26445 assert_eq!(
26446 next_selection.len(),
26447 1,
26448 "Should have one selection after next"
26449 );
26450 assert_ne!(
26451 next_selection[0], initial_selection[0],
26452 "Next sibling selection should be different"
26453 );
26454
26455 // Test hierarchical navigation by going to the end of the current function
26456 // and trying to navigate to the next function
26457 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26458 s.select_display_ranges([
26459 DisplayPoint::new(DisplayRow(5), 12)..DisplayPoint::new(DisplayRow(5), 22)
26460 ]);
26461 });
26462
26463 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
26464 let function_next_selection = editor
26465 .selections
26466 .display_ranges(&editor.display_snapshot(cx));
26467
26468 // Should move to the next function
26469 assert_eq!(
26470 function_next_selection.len(),
26471 1,
26472 "Should have one selection after function next"
26473 );
26474
26475 // Test select previous sibling navigation
26476 editor.select_prev_syntax_node(&SelectPreviousSyntaxNode, window, cx);
26477 let prev_selection = editor
26478 .selections
26479 .display_ranges(&editor.display_snapshot(cx));
26480
26481 // Should have a selection and it should be different
26482 assert_eq!(
26483 prev_selection.len(),
26484 1,
26485 "Should have one selection after prev"
26486 );
26487 assert_ne!(
26488 prev_selection[0], function_next_selection[0],
26489 "Previous sibling selection should be different from next"
26490 );
26491 });
26492}
26493
26494#[gpui::test]
26495async fn test_next_prev_document_highlight(cx: &mut TestAppContext) {
26496 init_test(cx, |_| {});
26497
26498 let mut cx = EditorTestContext::new(cx).await;
26499 cx.set_state(
26500 "let ˇvariable = 42;
26501let another = variable + 1;
26502let result = variable * 2;",
26503 );
26504
26505 // Set up document highlights manually (simulating LSP response)
26506 cx.update_editor(|editor, _window, cx| {
26507 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
26508
26509 // Create highlights for "variable" occurrences
26510 let highlight_ranges = [
26511 Point::new(0, 4)..Point::new(0, 12), // First "variable"
26512 Point::new(1, 14)..Point::new(1, 22), // Second "variable"
26513 Point::new(2, 13)..Point::new(2, 21), // Third "variable"
26514 ];
26515
26516 let anchor_ranges: Vec<_> = highlight_ranges
26517 .iter()
26518 .map(|range| range.clone().to_anchors(&buffer_snapshot))
26519 .collect();
26520
26521 editor.highlight_background::<DocumentHighlightRead>(
26522 &anchor_ranges,
26523 |theme| theme.colors().editor_document_highlight_read_background,
26524 cx,
26525 );
26526 });
26527
26528 // Go to next highlight - should move to second "variable"
26529 cx.update_editor(|editor, window, cx| {
26530 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26531 });
26532 cx.assert_editor_state(
26533 "let variable = 42;
26534let another = ˇvariable + 1;
26535let result = variable * 2;",
26536 );
26537
26538 // Go to next highlight - should move to third "variable"
26539 cx.update_editor(|editor, window, cx| {
26540 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26541 });
26542 cx.assert_editor_state(
26543 "let variable = 42;
26544let another = variable + 1;
26545let result = ˇvariable * 2;",
26546 );
26547
26548 // Go to next highlight - should stay at third "variable" (no wrap-around)
26549 cx.update_editor(|editor, window, cx| {
26550 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26551 });
26552 cx.assert_editor_state(
26553 "let variable = 42;
26554let another = variable + 1;
26555let result = ˇvariable * 2;",
26556 );
26557
26558 // Now test going backwards from third position
26559 cx.update_editor(|editor, window, cx| {
26560 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26561 });
26562 cx.assert_editor_state(
26563 "let variable = 42;
26564let another = ˇvariable + 1;
26565let result = variable * 2;",
26566 );
26567
26568 // Go to previous highlight - should move to first "variable"
26569 cx.update_editor(|editor, window, cx| {
26570 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26571 });
26572 cx.assert_editor_state(
26573 "let ˇvariable = 42;
26574let another = variable + 1;
26575let result = variable * 2;",
26576 );
26577
26578 // Go to previous highlight - should stay on first "variable"
26579 cx.update_editor(|editor, window, cx| {
26580 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26581 });
26582 cx.assert_editor_state(
26583 "let ˇvariable = 42;
26584let another = variable + 1;
26585let result = variable * 2;",
26586 );
26587}
26588
26589#[gpui::test]
26590async fn test_paste_url_from_other_app_creates_markdown_link_over_selected_text(
26591 cx: &mut gpui::TestAppContext,
26592) {
26593 init_test(cx, |_| {});
26594
26595 let url = "https://zed.dev";
26596
26597 let markdown_language = Arc::new(Language::new(
26598 LanguageConfig {
26599 name: "Markdown".into(),
26600 ..LanguageConfig::default()
26601 },
26602 None,
26603 ));
26604
26605 let mut cx = EditorTestContext::new(cx).await;
26606 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26607 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)");
26608
26609 cx.update_editor(|editor, window, cx| {
26610 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26611 editor.paste(&Paste, window, cx);
26612 });
26613
26614 cx.assert_editor_state(&format!(
26615 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)"
26616 ));
26617}
26618
26619#[gpui::test]
26620async fn test_paste_url_from_zed_copy_creates_markdown_link_over_selected_text(
26621 cx: &mut gpui::TestAppContext,
26622) {
26623 init_test(cx, |_| {});
26624
26625 let url = "https://zed.dev";
26626
26627 let markdown_language = Arc::new(Language::new(
26628 LanguageConfig {
26629 name: "Markdown".into(),
26630 ..LanguageConfig::default()
26631 },
26632 None,
26633 ));
26634
26635 let mut cx = EditorTestContext::new(cx).await;
26636 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26637 cx.set_state(&format!(
26638 "Hello, editor.\nZed is great (see this link: )\n«{url}ˇ»"
26639 ));
26640
26641 cx.update_editor(|editor, window, cx| {
26642 editor.copy(&Copy, window, cx);
26643 });
26644
26645 cx.set_state(&format!(
26646 "Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)\n{url}"
26647 ));
26648
26649 cx.update_editor(|editor, window, cx| {
26650 editor.paste(&Paste, window, cx);
26651 });
26652
26653 cx.assert_editor_state(&format!(
26654 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)\n{url}"
26655 ));
26656}
26657
26658#[gpui::test]
26659async fn test_paste_url_from_other_app_replaces_existing_url_without_creating_markdown_link(
26660 cx: &mut gpui::TestAppContext,
26661) {
26662 init_test(cx, |_| {});
26663
26664 let url = "https://zed.dev";
26665
26666 let markdown_language = Arc::new(Language::new(
26667 LanguageConfig {
26668 name: "Markdown".into(),
26669 ..LanguageConfig::default()
26670 },
26671 None,
26672 ));
26673
26674 let mut cx = EditorTestContext::new(cx).await;
26675 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26676 cx.set_state("Please visit zed's homepage: «https://www.apple.comˇ»");
26677
26678 cx.update_editor(|editor, window, cx| {
26679 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26680 editor.paste(&Paste, window, cx);
26681 });
26682
26683 cx.assert_editor_state(&format!("Please visit zed's homepage: {url}ˇ"));
26684}
26685
26686#[gpui::test]
26687async fn test_paste_plain_text_from_other_app_replaces_selection_without_creating_markdown_link(
26688 cx: &mut gpui::TestAppContext,
26689) {
26690 init_test(cx, |_| {});
26691
26692 let text = "Awesome";
26693
26694 let markdown_language = Arc::new(Language::new(
26695 LanguageConfig {
26696 name: "Markdown".into(),
26697 ..LanguageConfig::default()
26698 },
26699 None,
26700 ));
26701
26702 let mut cx = EditorTestContext::new(cx).await;
26703 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26704 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat»");
26705
26706 cx.update_editor(|editor, window, cx| {
26707 cx.write_to_clipboard(ClipboardItem::new_string(text.to_string()));
26708 editor.paste(&Paste, window, cx);
26709 });
26710
26711 cx.assert_editor_state(&format!("Hello, {text}ˇ.\nZed is {text}ˇ"));
26712}
26713
26714#[gpui::test]
26715async fn test_paste_url_from_other_app_without_creating_markdown_link_in_non_markdown_language(
26716 cx: &mut gpui::TestAppContext,
26717) {
26718 init_test(cx, |_| {});
26719
26720 let url = "https://zed.dev";
26721
26722 let markdown_language = Arc::new(Language::new(
26723 LanguageConfig {
26724 name: "Rust".into(),
26725 ..LanguageConfig::default()
26726 },
26727 None,
26728 ));
26729
26730 let mut cx = EditorTestContext::new(cx).await;
26731 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26732 cx.set_state("// Hello, «editorˇ».\n// Zed is «ˇgreat» (see this link: ˇ)");
26733
26734 cx.update_editor(|editor, window, cx| {
26735 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26736 editor.paste(&Paste, window, cx);
26737 });
26738
26739 cx.assert_editor_state(&format!(
26740 "// Hello, {url}ˇ.\n// Zed is {url}ˇ (see this link: {url}ˇ)"
26741 ));
26742}
26743
26744#[gpui::test]
26745async fn test_paste_url_from_other_app_creates_markdown_link_selectively_in_multi_buffer(
26746 cx: &mut TestAppContext,
26747) {
26748 init_test(cx, |_| {});
26749
26750 let url = "https://zed.dev";
26751
26752 let markdown_language = Arc::new(Language::new(
26753 LanguageConfig {
26754 name: "Markdown".into(),
26755 ..LanguageConfig::default()
26756 },
26757 None,
26758 ));
26759
26760 let (editor, cx) = cx.add_window_view(|window, cx| {
26761 let multi_buffer = MultiBuffer::build_multi(
26762 [
26763 ("this will embed -> link", vec![Point::row_range(0..1)]),
26764 ("this will replace -> link", vec![Point::row_range(0..1)]),
26765 ],
26766 cx,
26767 );
26768 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
26769 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26770 s.select_ranges(vec![
26771 Point::new(0, 19)..Point::new(0, 23),
26772 Point::new(1, 21)..Point::new(1, 25),
26773 ])
26774 });
26775 let first_buffer_id = multi_buffer
26776 .read(cx)
26777 .excerpt_buffer_ids()
26778 .into_iter()
26779 .next()
26780 .unwrap();
26781 let first_buffer = multi_buffer.read(cx).buffer(first_buffer_id).unwrap();
26782 first_buffer.update(cx, |buffer, cx| {
26783 buffer.set_language(Some(markdown_language.clone()), cx);
26784 });
26785
26786 editor
26787 });
26788 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
26789
26790 cx.update_editor(|editor, window, cx| {
26791 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26792 editor.paste(&Paste, window, cx);
26793 });
26794
26795 cx.assert_editor_state(&format!(
26796 "this will embed -> [link]({url})ˇ\nthis will replace -> {url}ˇ"
26797 ));
26798}
26799
26800#[gpui::test]
26801async fn test_race_in_multibuffer_save(cx: &mut TestAppContext) {
26802 init_test(cx, |_| {});
26803
26804 let fs = FakeFs::new(cx.executor());
26805 fs.insert_tree(
26806 path!("/project"),
26807 json!({
26808 "first.rs": "# First Document\nSome content here.",
26809 "second.rs": "Plain text content for second file.",
26810 }),
26811 )
26812 .await;
26813
26814 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
26815 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
26816 let cx = &mut VisualTestContext::from_window(*workspace, cx);
26817
26818 let language = rust_lang();
26819 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
26820 language_registry.add(language.clone());
26821 let mut fake_servers = language_registry.register_fake_lsp(
26822 "Rust",
26823 FakeLspAdapter {
26824 ..FakeLspAdapter::default()
26825 },
26826 );
26827
26828 let buffer1 = project
26829 .update(cx, |project, cx| {
26830 project.open_local_buffer(PathBuf::from(path!("/project/first.rs")), cx)
26831 })
26832 .await
26833 .unwrap();
26834 let buffer2 = project
26835 .update(cx, |project, cx| {
26836 project.open_local_buffer(PathBuf::from(path!("/project/second.rs")), cx)
26837 })
26838 .await
26839 .unwrap();
26840
26841 let multi_buffer = cx.new(|cx| {
26842 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
26843 multi_buffer.set_excerpts_for_path(
26844 PathKey::for_buffer(&buffer1, cx),
26845 buffer1.clone(),
26846 [Point::zero()..buffer1.read(cx).max_point()],
26847 3,
26848 cx,
26849 );
26850 multi_buffer.set_excerpts_for_path(
26851 PathKey::for_buffer(&buffer2, cx),
26852 buffer2.clone(),
26853 [Point::zero()..buffer1.read(cx).max_point()],
26854 3,
26855 cx,
26856 );
26857 multi_buffer
26858 });
26859
26860 let (editor, cx) = cx.add_window_view(|window, cx| {
26861 Editor::new(
26862 EditorMode::full(),
26863 multi_buffer,
26864 Some(project.clone()),
26865 window,
26866 cx,
26867 )
26868 });
26869
26870 let fake_language_server = fake_servers.next().await.unwrap();
26871
26872 buffer1.update(cx, |buffer, cx| buffer.edit([(0..0, "hello!")], None, cx));
26873
26874 let save = editor.update_in(cx, |editor, window, cx| {
26875 assert!(editor.is_dirty(cx));
26876
26877 editor.save(
26878 SaveOptions {
26879 format: true,
26880 autosave: true,
26881 },
26882 project,
26883 window,
26884 cx,
26885 )
26886 });
26887 let (start_edit_tx, start_edit_rx) = oneshot::channel();
26888 let (done_edit_tx, done_edit_rx) = oneshot::channel();
26889 let mut done_edit_rx = Some(done_edit_rx);
26890 let mut start_edit_tx = Some(start_edit_tx);
26891
26892 fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| {
26893 start_edit_tx.take().unwrap().send(()).unwrap();
26894 let done_edit_rx = done_edit_rx.take().unwrap();
26895 async move {
26896 done_edit_rx.await.unwrap();
26897 Ok(None)
26898 }
26899 });
26900
26901 start_edit_rx.await.unwrap();
26902 buffer2
26903 .update(cx, |buffer, cx| buffer.edit([(0..0, "world!")], None, cx))
26904 .unwrap();
26905
26906 done_edit_tx.send(()).unwrap();
26907
26908 save.await.unwrap();
26909 cx.update(|_, cx| assert!(editor.is_dirty(cx)));
26910}
26911
26912#[track_caller]
26913fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
26914 editor
26915 .all_inlays(cx)
26916 .into_iter()
26917 .filter_map(|inlay| inlay.get_color())
26918 .map(Rgba::from)
26919 .collect()
26920}
26921
26922#[gpui::test]
26923fn test_duplicate_line_up_on_last_line_without_newline(cx: &mut TestAppContext) {
26924 init_test(cx, |_| {});
26925
26926 let editor = cx.add_window(|window, cx| {
26927 let buffer = MultiBuffer::build_simple("line1\nline2", cx);
26928 build_editor(buffer, window, cx)
26929 });
26930
26931 editor
26932 .update(cx, |editor, window, cx| {
26933 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26934 s.select_display_ranges([
26935 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
26936 ])
26937 });
26938
26939 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
26940
26941 assert_eq!(
26942 editor.display_text(cx),
26943 "line1\nline2\nline2",
26944 "Duplicating last line upward should create duplicate above, not on same line"
26945 );
26946
26947 assert_eq!(
26948 editor
26949 .selections
26950 .display_ranges(&editor.display_snapshot(cx)),
26951 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)],
26952 "Selection should move to the duplicated line"
26953 );
26954 })
26955 .unwrap();
26956}
26957
26958#[gpui::test]
26959async fn test_copy_line_without_trailing_newline(cx: &mut TestAppContext) {
26960 init_test(cx, |_| {});
26961
26962 let mut cx = EditorTestContext::new(cx).await;
26963
26964 cx.set_state("line1\nline2ˇ");
26965
26966 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
26967
26968 let clipboard_text = cx
26969 .read_from_clipboard()
26970 .and_then(|item| item.text().as_deref().map(str::to_string));
26971
26972 assert_eq!(
26973 clipboard_text,
26974 Some("line2\n".to_string()),
26975 "Copying a line without trailing newline should include a newline"
26976 );
26977
26978 cx.set_state("line1\nˇ");
26979
26980 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
26981
26982 cx.assert_editor_state("line1\nline2\nˇ");
26983}
26984
26985#[gpui::test]
26986async fn test_end_of_editor_context(cx: &mut TestAppContext) {
26987 init_test(cx, |_| {});
26988
26989 let mut cx = EditorTestContext::new(cx).await;
26990
26991 cx.set_state("line1\nline2ˇ");
26992 cx.update_editor(|e, window, cx| {
26993 e.set_mode(EditorMode::SingleLine);
26994 assert!(e.key_context(window, cx).contains("end_of_input"));
26995 });
26996 cx.set_state("ˇline1\nline2");
26997 cx.update_editor(|e, window, cx| {
26998 assert!(!e.key_context(window, cx).contains("end_of_input"));
26999 });
27000 cx.set_state("line1ˇ\nline2");
27001 cx.update_editor(|e, window, cx| {
27002 assert!(!e.key_context(window, cx).contains("end_of_input"));
27003 });
27004}
27005
27006#[gpui::test]
27007async fn test_next_prev_reference(cx: &mut TestAppContext) {
27008 const CYCLE_POSITIONS: &[&'static str] = &[
27009 indoc! {"
27010 fn foo() {
27011 let ˇabc = 123;
27012 let x = abc + 1;
27013 let y = abc + 2;
27014 let z = abc + 2;
27015 }
27016 "},
27017 indoc! {"
27018 fn foo() {
27019 let abc = 123;
27020 let x = ˇabc + 1;
27021 let y = abc + 2;
27022 let z = abc + 2;
27023 }
27024 "},
27025 indoc! {"
27026 fn foo() {
27027 let abc = 123;
27028 let x = abc + 1;
27029 let y = ˇabc + 2;
27030 let z = abc + 2;
27031 }
27032 "},
27033 indoc! {"
27034 fn foo() {
27035 let abc = 123;
27036 let x = abc + 1;
27037 let y = abc + 2;
27038 let z = ˇabc + 2;
27039 }
27040 "},
27041 ];
27042
27043 init_test(cx, |_| {});
27044
27045 let mut cx = EditorLspTestContext::new_rust(
27046 lsp::ServerCapabilities {
27047 references_provider: Some(lsp::OneOf::Left(true)),
27048 ..Default::default()
27049 },
27050 cx,
27051 )
27052 .await;
27053
27054 // importantly, the cursor is in the middle
27055 cx.set_state(indoc! {"
27056 fn foo() {
27057 let aˇbc = 123;
27058 let x = abc + 1;
27059 let y = abc + 2;
27060 let z = abc + 2;
27061 }
27062 "});
27063
27064 let reference_ranges = [
27065 lsp::Position::new(1, 8),
27066 lsp::Position::new(2, 12),
27067 lsp::Position::new(3, 12),
27068 lsp::Position::new(4, 12),
27069 ]
27070 .map(|start| lsp::Range::new(start, lsp::Position::new(start.line, start.character + 3)));
27071
27072 cx.lsp
27073 .set_request_handler::<lsp::request::References, _, _>(move |params, _cx| async move {
27074 Ok(Some(
27075 reference_ranges
27076 .map(|range| lsp::Location {
27077 uri: params.text_document_position.text_document.uri.clone(),
27078 range,
27079 })
27080 .to_vec(),
27081 ))
27082 });
27083
27084 let _move = async |direction, count, cx: &mut EditorLspTestContext| {
27085 cx.update_editor(|editor, window, cx| {
27086 editor.go_to_reference_before_or_after_position(direction, count, window, cx)
27087 })
27088 .unwrap()
27089 .await
27090 .unwrap()
27091 };
27092
27093 _move(Direction::Next, 1, &mut cx).await;
27094 cx.assert_editor_state(CYCLE_POSITIONS[1]);
27095
27096 _move(Direction::Next, 1, &mut cx).await;
27097 cx.assert_editor_state(CYCLE_POSITIONS[2]);
27098
27099 _move(Direction::Next, 1, &mut cx).await;
27100 cx.assert_editor_state(CYCLE_POSITIONS[3]);
27101
27102 // loops back to the start
27103 _move(Direction::Next, 1, &mut cx).await;
27104 cx.assert_editor_state(CYCLE_POSITIONS[0]);
27105
27106 // loops back to the end
27107 _move(Direction::Prev, 1, &mut cx).await;
27108 cx.assert_editor_state(CYCLE_POSITIONS[3]);
27109
27110 _move(Direction::Prev, 1, &mut cx).await;
27111 cx.assert_editor_state(CYCLE_POSITIONS[2]);
27112
27113 _move(Direction::Prev, 1, &mut cx).await;
27114 cx.assert_editor_state(CYCLE_POSITIONS[1]);
27115
27116 _move(Direction::Prev, 1, &mut cx).await;
27117 cx.assert_editor_state(CYCLE_POSITIONS[0]);
27118
27119 _move(Direction::Next, 3, &mut cx).await;
27120 cx.assert_editor_state(CYCLE_POSITIONS[3]);
27121
27122 _move(Direction::Prev, 2, &mut cx).await;
27123 cx.assert_editor_state(CYCLE_POSITIONS[1]);
27124}